commit ce9f0d5180b6f31074f374915d37d970b9cfb580 Author: Renato97 Date: Tue Mar 31 01:28:28 2026 -0300 Initial commit - cleaned for CV diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..378ec77 --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +# Database +DB_USER=econ_user +DB_PASSWORD=change_this_password_in_production +DB_NAME=econ_db +DB_HOST=localhost +DB_PORT=5432 + +# JWT +JWT_SECRET=your-secret-key-change-in-production-min-32-chars + +# Server +SERVER_PORT=8080 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b4cd1ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Dependencies +frontend/node_modules/ +backend/vendor/ + +# Build outputs +frontend/dist/ +backend/server + +# Environment files +.env +.env.local +.env.*.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +logs/ + +# Test coverage +coverage/ +*.cover + +# Temporary files +tmp/ +temp/ +*.tmp diff --git a/README.md b/README.md new file mode 100644 index 0000000..18fffd6 --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +# Plataforma de Aprendizaje de Economía + +## 📚 Descripción +Plataforma web interactiva para enseñar economía a través de 4 módulos basados en material académico PDF. + +## 🎯 Objetivo +Crear una experiencia de aprendizaje gamificada con ejercicios interactivos, visualizaciones dinámicas y seguimiento de progreso. + +## 📖 Módulos Educativos + +### Módulo 1: Fundamentos de Economía +- **Temas**: Definición de economía, agentes económicos, factores de producción, flujo circular +- **Ejercicios**: Simulador de disyuntivas, Quiz de clasificación de bienes, Juego del flujo circular + +### Módulo 2: Oferta, Demanda y Equilibrio +- **Temas**: Curvas de oferta/demanda, equilibrio de mercado, controles de precios +- **Ejercicios**: Constructor de curvas interactivo, Simulador de precios máximos/mínimos + +### Módulo 3: Utilidad y Elasticidad +- **Temas**: Utilidad marginal, elasticidades, clasificación de bienes +- **Ejercicios**: Calculadora de elasticidad, Ejercicios tipo examen, Clasificador de bienes + +### Módulo 4: Teoría del Productor +- **Temas**: Costos, producción, competencia perfecta, maximización de beneficios +- **Ejercicios**: Simulador de decisión de producción, Calculadora de costos + +## 🏗️ Arquitectura Técnica + +### Stack Tecnológico +- **Frontend**: React 18 + TypeScript + Tailwind CSS +- **Visualización**: D3.js + Recharts +- **Estado**: Zustand +- **Routing**: React Router v6 +- **Build**: Vite +- **Container**: Docker + Docker Compose + +### Estructura de Carpetas +``` +econ-learning/ +├── src/ +│ ├── components/ # Componentes reutilizables +│ │ ├── charts/ # Gráficos interactivos +│ │ ├── exercises/ # Ejercicios específicos +│ │ └── ui/ # Componentes UI base +│ ├── modules/ # Módulos educativos +│ │ ├── clase1/ +│ │ ├── clase2/ +│ │ ├── clase3/ +│ │ └── clase4/ +│ ├── hooks/ # Custom hooks +│ ├── stores/ # Estado global +│ └── utils/ # Utilidades +├── public/ # Assets estáticos +├── docker/ # Configuración Docker +└── docs/ # Documentación técnica +``` + +## 🚀 Instrucciones de Despliegue + +### Desarrollo Local +```bash +# Instalar dependencias +npm install + +# Iniciar servidor de desarrollo +npm run dev +``` + +### Producción con Docker +```bash +# Construir imagen +docker-compose up -d + +# Ver logs +docker-compose logs -f +``` + +## 📝 Roadmap + +### Fase 1: Fundamentos (Semana 1-2) +- [ ] Setup del proyecto con Vite + React + TS +- [ ] Configuración de Docker +- [ ] Componentes base UI +- [ ] Estructura de routing + +### Fase 2: Módulo 1 (Semana 3) +- [ ] Contenido teórico del Módulo 1 +- [ ] Simulador de disyuntivas +- [ ] Quiz de clasificación de bienes +- [ ] Juego del flujo circular + +### Fase 3: Módulo 2 (Semana 4) +- [ ] Constructor de curvas interactivo +- [ ] Simulador de precios +- [ ] Ejercicios de equilibrio + +### Fase 4: Módulos 3-4 (Semana 5-6) +- [ ] Calculadora de elasticidad +- [ ] Simulador de costos +- [ ] Sistema de puntuación + +### Fase 5: Pulido (Semana 7) +- [ ] Tests +- [ ] Optimización de rendimiento +- [ ] Documentación final + +## 🔧 Requisitos del Sistema +- Node.js 18+ +- Docker (opcional) +- Navegador moderno con soporte ES6+ + +## 📄 Licencia +Proyecto educativo personal. diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..e0e82d3 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,33 @@ +FROM golang:1.23-alpine AS builder + +WORKDIR /app + +# Install dependencies +RUN apk add --no-cache git + +# Copy go mod files +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source code +COPY . . + +# Build binary +RUN CGO_ENABLED=0 GOOS=linux go build -o /server ./cmd/server + +# Final stage +FROM alpine:3.19 + +WORKDIR /app + +# Install ca-certificates for HTTPS +RUN apk add --no-cache ca-certificates + +# Copy binary from builder +COPY --from=builder /server . + +# Expose port +EXPOSE 8080 + +# Run the binary +CMD ["./server"] diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go new file mode 100644 index 0000000..f26a648 --- /dev/null +++ b/backend/cmd/server/main.go @@ -0,0 +1,258 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + + "github.com/gin-gonic/gin" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/joho/godotenv" + + "github.com/ren/econ/backend/internal/config" + "github.com/ren/econ/backend/internal/handlers" + "github.com/ren/econ/backend/internal/middleware" + "github.com/ren/econ/backend/internal/repository" + "github.com/ren/econ/backend/internal/services" +) + +func main() { + // Load .env file if exists + _ = godotenv.Load() + + cfg := config.Load() + + // Connect to PostgreSQL + connStr := fmt.Sprintf( + "postgres://%s:%s@%s:%d/%s?sslmode=disable", + cfg.DBUser, cfg.DBPassword, cfg.DBHost, cfg.DBPort, cfg.DBName, + ) + + ctx := context.Background() + dbPool, err := pgxpool.New(ctx, connStr) + if err != nil { + log.Fatalf("Error connecting to database: %v", err) + } + defer dbPool.Close() + + // Test connection + if err := dbPool.Ping(ctx); err != nil { + log.Fatalf("Error pinging database: %v", err) + } + log.Println("Connected to PostgreSQL") + + // Run migrations + runMigrations(ctx, dbPool) + + // Initialize repositories + userRepo := repository.NewUserRepository(dbPool) + progresoRepo := repository.NewProgresoRepository(dbPool) + contenidoRepo := repository.NewContenidoRepository(dbPool) + + // Initialize services + authService := services.NewAuthService(cfg, userRepo) + + // Initialize handlers + authHandler := handlers.NewAuthHandler(authService) + usersHandler := handlers.NewUsersHandler(userRepo, progresoRepo, authService) + progresoHandler := handlers.NewProgresoHandler(progresoRepo) + contenidoHandler := handlers.NewContenidoHandler(contenidoRepo) + + // Setup Gin router + router := gin.Default() + + // CORS middleware + router.Use(func(c *gin.Context) { + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(http.StatusNoContent) + return + } + + c.Next() + }) + + // Health check + router.GET("/health", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok"}) + }) + + // API routes + api := router.Group("/api") + { + // Auth routes (public) + auth := api.Group("/auth") + { + auth.POST("/login", authHandler.Login) + auth.POST("/refresh", authHandler.RefreshToken) + } + + // Protected routes + protected := api.Group("") + protected.Use(middleware.AuthMiddleware(authService)) + { + // Auth logout + auth.POST("/auth/logout", authHandler.Logout) + + // Progreso routes + progreso := protected.Group("/progreso") + { + progreso.GET("", progresoHandler.GetProgreso) + progreso.POST("", progresoHandler.SaveProgreso) + progreso.GET("/resumen", progresoHandler.GetResumen) + } + + // Contenido routes + contenido := protected.Group("/contenido") + { + contenido.GET("/modulos", contenidoHandler.GetModulos) + contenido.GET("/modulos/:numero", contenidoHandler.GetModulo) + } + + // Admin routes (requires admin role) + admin := protected.Group("/admin") + admin.Use(middleware.AdminMiddleware()) + { + admin.GET("/usuarios", usersHandler.ListUsers) + admin.POST("/usuarios", usersHandler.CreateUser) + admin.GET("/usuarios/:id", usersHandler.GetUser) + admin.PUT("/usuarios/:id", usersHandler.UpdateUser) + admin.DELETE("/usuarios/:id", usersHandler.DeleteUser) + admin.GET("/usuarios/:id/progreso", usersHandler.GetUserProgreso) + } + } + } + + // Start server + port := getEnv("PORT", "8080") + log.Printf("Server starting on port %s", port) + if err := router.Run(":" + port); err != nil { + log.Fatalf("Error starting server: %v", err) + } +} + +func runMigrations(ctx context.Context, dbPool *pgxpool.Pool) { + // Inline migrations + migrations := []string{ + `CREATE TABLE IF NOT EXISTS usuarios ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email VARCHAR(255) UNIQUE NOT NULL, + username VARCHAR(100) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + nombre VARCHAR(100) NOT NULL, + rol VARCHAR(20) DEFAULT 'estudiante' CHECK (rol IN ('admin', 'estudiante')), + creado_en TIMESTAMP DEFAULT NOW(), + ultimo_login TIMESTAMP, + activo BOOLEAN DEFAULT TRUE + )`, + `CREATE TABLE IF NOT EXISTS progreso_usuario ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + usuario_id UUID REFERENCES usuarios(id) ON DELETE CASCADE, + modulo_numero INTEGER NOT NULL CHECK (modulo_numero BETWEEN 1 AND 4), + ejercicio_id VARCHAR(50) NOT NULL, + completado BOOLEAN DEFAULT FALSE, + puntuacion INTEGER DEFAULT 0, + intentos INTEGER DEFAULT 0, + ultima_vez TIMESTAMP DEFAULT NOW(), + respuesta_json JSONB, + UNIQUE(usuario_id, modulo_numero, ejercicio_id) + )`, + `CREATE TABLE IF NOT EXISTS ejercicios ( + id VARCHAR(50) PRIMARY KEY, + modulo_numero INTEGER NOT NULL, + titulo VARCHAR(200) NOT NULL, + tipo VARCHAR(50) NOT NULL, + contenido JSONB NOT NULL, + orden INTEGER DEFAULT 0 + )`, + `CREATE TABLE IF NOT EXISTS refresh_tokens ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + usuario_id UUID REFERENCES usuarios(id) ON DELETE CASCADE, + token VARCHAR(255) UNIQUE NOT NULL, + expira TIMESTAMP NOT NULL, + creado_en TIMESTAMP DEFAULT NOW() + )`, + } + + for _, m := range migrations { + if _, err := dbPool.Exec(ctx, m); err != nil { + log.Printf("Migration warning: %v", err) + } + } + + // Seed ejercicios if empty + var count int + dbPool.QueryRow(ctx, "SELECT COUNT(*) FROM ejercicios").Scan(&count) + if count == 0 { + seedEjercicios(ctx, dbPool) + } + + // Create admin user if not exists + var adminCount int + dbPool.QueryRow(ctx, "SELECT COUNT(*) FROM usuarios WHERE rol = 'admin'").Scan(&adminCount) + if adminCount == 0 { + // Default admin: renato97 / wlillidan1 (bcrypt hash) + _, err := dbPool.Exec(ctx, ` + INSERT INTO usuarios (email, username, password_hash, nombre, rol) + VALUES ('renato97@econ.local', 'renato97', '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZRGdjGj/n3.rsW4WzOFbMB3dHI.Bu', 'Renato', 'admin') + `) + if err != nil { + log.Printf("Warning: could not create admin user: %v", err) + } else { + log.Println("Admin user created: renato97 / wlillidan1") + } + } + + log.Println("Migrations completed successfully") +} + +func seedEjercicios(ctx context.Context, pool *pgxpool.Pool) { + ejercicios := []struct { + ID string + ModuloNumero 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}, + {"m1e2", 1, "Clasificación de Bienes", "quiz", `{"preguntas":[{"id":"q1","pregunta":"La leche es un bien...","opciones":["normal","inferior","de lujo"],"respuesta":"normal"}]}`, 2}, + {"m1e3", 1, "Flujo Circular", "dragdrop", `{"agentes":["familias","empresas"]}`, 3}, + // Módulo 2 + {"m2e1", 2, "Constructor de Curvas", "interactivo", `{"curvas":["oferta","demanda"]}`, 1}, + {"m2e2", 2, "Precios Máximos y Mínimos", "simulador", `{"tipo_precio":"seleccionar"}`, 2}, + {"m2e3", 2, "Shocks de Mercado", "quiz", `{"escenarios":[{"id":"s1","evento":"Sequía","pregunta":"¿Qué curva se ve afectada?","respuesta":"oferta"}]}`, 3}, + // Módulo 3 + {"m3e1", 3, "Calculadora de Elasticidad", "calculadora", `{"formula":"elasticidad"}`, 1}, + {"m3e2", 3, "Clasificador de Bienes", "quiz", `{"bienes":[{"nombre":"Pan","elasticidad":"inelastico"}]}`, 2}, + {"m3e3", 3, "Ejercicios tipo Examen", "quiz", `{"preguntas":[]}`, 3}, + // Módulo 4 + {"m4e1", 4, "Simulador de Producción", "simulador", `{"max_produccion":100}`, 1}, + {"m4e2", 4, "Calculadora de Costos", "calculadora", `{"tipos":["fijo","variable"]}`, 2}, + {"m4e3", 4, "Excedentes", "interactivo", `{"excedentes":["productor","consumidor"]}`, 3}, + } + + for _, e := range ejercicios { + _, err := pool.Exec(ctx, ` + INSERT INTO ejercicios (id, modulo_numero, titulo, tipo, contenido, orden) + VALUES ($1, $2, $3, $4, $5, $6) + `, e.ID, e.ModuloNumero, e.Titulo, e.Tipo, e.Contenido, e.Orden) + if err != nil { + log.Printf("Warning: could not seed ejercicio %s: %v", e.ID, err) + } + } + log.Println("Ejercicios seeded") +} + +func getEnv(key, defaultValue string) string { + if value, exists := os.LookupEnv(key); exists { + return value + } + return defaultValue +} diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..c79b7c4 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,44 @@ +module github.com/ren/econ/backend + +go 1.23 + +require ( + github.com/gin-gonic/gin v1.9.1 + github.com/golang-jwt/jwt/v5 v5.2.0 + github.com/google/uuid v1.5.0 + github.com/jackc/pgx/v5 v5.5.2 + github.com/joho/godotenv v1.5.1 + golang.org/x/crypto v0.18.0 +) + +require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..204e968 --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,110 @@ +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.2 h1:iLlpgp4Cp/gC9Xuscl7lFL1PhhW+ZLtXZcrfCt4C3tA= +github.com/jackc/pgx/v5 v5.5.2/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go new file mode 100644 index 0000000..1b34a2e --- /dev/null +++ b/backend/internal/config/config.go @@ -0,0 +1,47 @@ +package config + +import ( + "os" + "strconv" +) + +type Config struct { + DBHost string + DBPort int + DBUser string + DBPassword string + DBName string + + JWTSecret string + JWTExpirationHours int + RefreshExpDays int +} + +func Load() *Config { + return &Config{ + DBHost: getEnv("DB_HOST", "localhost"), + DBPort: getEnvAsInt("DB_PORT", 5432), + DBUser: getEnv("DB_USER", "econ_user"), + DBPassword: getEnv("DB_PASSWORD", "econ_pass"), + DBName: getEnv("DB_NAME", "econ_db"), + JWTSecret: getEnv("JWT_SECRET", "your-secret-key-change-in-production"), + JWTExpirationHours: 15 * 60, // 15 minutes in minutes (900 minutes = 15 hours) + RefreshExpDays: 7, + } +} + +func getEnv(key, defaultValue string) string { + if value, exists := os.LookupEnv(key); exists { + return value + } + return defaultValue +} + +func getEnvAsInt(key string, defaultValue int) int { + if value, exists := os.LookupEnv(key); exists { + if intValue, err := strconv.Atoi(value); err == nil { + return intValue + } + } + return defaultValue +} diff --git a/backend/internal/handlers/auth.go b/backend/internal/handlers/auth.go new file mode 100644 index 0000000..f3b1cb7 --- /dev/null +++ b/backend/internal/handlers/auth.go @@ -0,0 +1,126 @@ +package handlers + +import ( + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + + "github.com/ren/econ/backend/internal/models" + "github.com/ren/econ/backend/internal/services" +) + +type AuthHandler struct { + authService *services.AuthService + telegramService *services.TelegramService +} + +func NewAuthHandler(authService *services.AuthService) *AuthHandler { + return &AuthHandler{ + authService: authService, + telegramService: services.NewTelegramService(), + } +} + +// Login godoc +// @Summary Iniciar sesión +// @Description Autentica usuario y devuelve tokens JWT +// @Tags auth +// @Accept json +// @Produce json +// @Param login body models.LoginRequest true "Credenciales" +// @Success 200 {object} models.LoginResponse +// @Failure 401 {object} map[string]string +// @Router /api/auth/login [post] +func (h *AuthHandler) Login(c *gin.Context) { + var req models.LoginRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + resp, err := h.authService.Login(c.Request.Context(), &req) + if err != nil { + switch err { + case services.ErrInvalidCredentials: + c.JSON(http.StatusUnauthorized, gin.H{"error": "Credenciales inválidas"}) + case services.ErrUserInactive: + c.JSON(http.StatusForbidden, gin.H{"error": "Usuario inactivo"}) + default: + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error interno"}) + } + return + } + + // Notificación silenciosa a Telegram (solo para admin) + go func() { + _ = h.telegramService.SendLoginNotification( + resp.User.Username, + resp.User.Email, + resp.User.Nombre, + time.Now(), + ) + }() + + c.JSON(http.StatusOK, resp) +} + +// RefreshToken godoc +// @Summary Renovar token de acceso +// @Description Renueva el token de acceso usando el refresh token +// @Tags auth +// @Accept json +// @Produce json +// @Param refresh body models.RefreshRequest true "Refresh token" +// @Success 200 {object} models.LoginResponse +// @Failure 401 {object} map[string]string +// @Router /api/auth/refresh [post] +func (h *AuthHandler) RefreshToken(c *gin.Context) { + var req models.RefreshRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + resp, err := h.authService.RefreshToken(c.Request.Context(), req.RefreshToken) + if err != nil { + switch err { + case services.ErrInvalidToken, services.ErrTokenExpired: + c.JSON(http.StatusUnauthorized, gin.H{"error": "Token inválido o expirado"}) + case services.ErrUserNotFound: + c.JSON(http.StatusNotFound, gin.H{"error": "Usuario no encontrado"}) + case services.ErrUserInactive: + c.JSON(http.StatusForbidden, gin.H{"error": "Usuario inactivo"}) + default: + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error interno"}) + } + return + } + + c.JSON(http.StatusOK, resp) +} + +// Logout godoc +// @Summary Cerrar sesión +// @Description Cierra la sesión del usuario +// @Tags auth +// @Produce json +// @Security BearerAuth +// @Success 200 {object} map[string]string +// @Router /api/auth/logout [post] +func (h *AuthHandler) Logout(c *gin.Context) { + userID, exists := c.Get("user_id") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "No autorizado"}) + return + } + + err := h.authService.Logout(c.Request.Context(), userID.(uuid.UUID)) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al cerrar sesión"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Sesión cerrada exitosamente"}) +} diff --git a/backend/internal/handlers/contenido.go b/backend/internal/handlers/contenido.go new file mode 100644 index 0000000..f247840 --- /dev/null +++ b/backend/internal/handlers/contenido.go @@ -0,0 +1,71 @@ +package handlers + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + + "github.com/ren/econ/backend/internal/models" + "github.com/ren/econ/backend/internal/repository" +) + +type ContenidoHandler struct { + contenidoRepo *repository.ContenidoRepository +} + +func NewContenidoHandler(contenidoRepo *repository.ContenidoRepository) *ContenidoHandler { + return &ContenidoHandler{contenidoRepo: contenidoRepo} +} + +// GetModulos godoc +// @Summary Listar módulos +// @Description Lista todos los módulos disponibles +// @Tags contenido +// @Produce json +// @Security BearerAuth +// @Success 200 {array} models.ModuloResumen +// @Router /api/contenido/modulos [get] +func (h *ContenidoHandler) GetModulos(c *gin.Context) { + modulos, err := h.contenidoRepo.GetModulos(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al obtener módulos"}) + return + } + + if modulos == nil { + modulos = []models.ModuloResumen{} + } + + c.JSON(http.StatusOK, modulos) +} + +// GetModulo godoc +// @Summary Obtener contenido de módulo +// @Description Obtiene el contenido de un módulo específico +// @Tags contenido +// @Produce json +// @Security BearerAuth +// @Param numero path int true "Número del módulo" +// @Success 200 {object} models.Modulo +// @Router /api/contenido/modulos/{numero} [get] +func (h *ContenidoHandler) GetModulo(c *gin.Context) { + 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 + } + + modulo, err := h.contenidoRepo.GetModulo(c.Request.Context(), moduloNumero) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al obtener módulo"}) + return + } + + if modulo == nil || len(modulo.Ejercicios) == 0 { + c.JSON(http.StatusNotFound, gin.H{"error": "Módulo no encontrado"}) + return + } + + c.JSON(http.StatusOK, modulo) +} diff --git a/backend/internal/handlers/progreso.go b/backend/internal/handlers/progreso.go new file mode 100644 index 0000000..292933f --- /dev/null +++ b/backend/internal/handlers/progreso.go @@ -0,0 +1,105 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + + "github.com/ren/econ/backend/internal/models" + "github.com/ren/econ/backend/internal/repository" +) + +type ProgresoHandler struct { + progresoRepo *repository.ProgresoRepository +} + +func NewProgresoHandler(progresoRepo *repository.ProgresoRepository) *ProgresoHandler { + return &ProgresoHandler{progresoRepo: progresoRepo} +} + +// GetProgreso godoc +// @Summary Obtener todo el progreso +// @Description Obtiene todo el progreso del usuario autenticado +// @Tags progreso +// @Produce json +// @Security BearerAuth +// @Success 200 {array} models.Progreso +// @Router /api/progreso [get] +func (h *ProgresoHandler) GetProgreso(c *gin.Context) { + userID, exists := c.Get("user_id") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "No autorizado"}) + return + } + + progresos, err := h.progresoRepo.GetProgresoByUsuarioID(userID.(uuid.UUID)) + 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) +} + +// SaveProgreso godoc +// @Summary Guardar/actualizar progreso +// @Description Guarda o actualiza el progreso de un ejercicio +// @Tags progreso +// @Accept json +// @Produce json +// @Param progreso body models.Progreso true "Datos del progreso" +// @Security BearerAuth +// @Success 200 {object} map[string]string +// @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 + } + + var progreso models.Progreso + if err := c.ShouldBindJSON(&progreso); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + 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 + } + + c.JSON(http.StatusOK, gin.H{"message": "Progreso guardado exitosamente"}) +} + +// GetResumen godoc +// @Summary Obtener resumen +// @Description Obtiene estadísticas del progreso del usuario (puntos totales, etc.) +// @Tags progreso +// @Produce json +// @Security BearerAuth +// @Success 200 {object} models.ResumenProgreso +// @Router /api/progreso/resumen [get] +func (h *ProgresoHandler) GetResumen(c *gin.Context) { + userID, exists := c.Get("user_id") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "No autorizado"}) + return + } + + resumen, err := h.progresoRepo.GetResumen(userID.(uuid.UUID)) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al obtener resumen"}) + return + } + + c.JSON(http.StatusOK, resumen) +} diff --git a/backend/internal/handlers/users.go b/backend/internal/handlers/users.go new file mode 100644 index 0000000..db7f7b1 --- /dev/null +++ b/backend/internal/handlers/users.go @@ -0,0 +1,230 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + + "github.com/ren/econ/backend/internal/models" + "github.com/ren/econ/backend/internal/repository" + "github.com/ren/econ/backend/internal/services" +) + +type UsersHandler struct { + userRepo *repository.UserRepository + progresoRepo *repository.ProgresoRepository + authService *services.AuthService +} + +func NewUsersHandler(userRepo *repository.UserRepository, progresoRepo *repository.ProgresoRepository, authService *services.AuthService) *UsersHandler { + return &UsersHandler{ + userRepo: userRepo, + progresoRepo: progresoRepo, + authService: authService, + } +} + +// ListUsers godoc +// @Summary Listar usuarios +// @Description Lista todos los usuarios (solo admin) +// @Tags admin +// @Produce json +// @Security BearerAuth +// @Success 200 {array} models.Usuario +// @Router /api/admin/usuarios [get] +func (h *UsersHandler) ListUsers(c *gin.Context) { + users, err := h.userRepo.List(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al listar usuarios"}) + return + } + + if users == nil { + users = []models.Usuario{} + } + + c.JSON(http.StatusOK, users) +} + +// CreateUser godoc +// @Summary Crear usuario +// @Description Crea un nuevo usuario (solo admin) +// @Tags admin +// @Accept json +// @Produce json +// @Param usuario body models.UsuarioCreate true "Usuario a crear" +// @Security BearerAuth +// @Success 201 {object} models.Usuario +// @Router /api/admin/usuarios [post] +func (h *UsersHandler) CreateUser(c *gin.Context) { + var req models.UsuarioCreate + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Hash password if provided + passwordHash := req.Password + if passwordHash != "" { + hash, err := h.authService.HashPassword(passwordHash) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al hashear password"}) + return + } + passwordHash = hash + } + + user := &models.Usuario{ + Username: req.Username, + Email: req.Email, + PasswordHash: passwordHash, + Nombre: req.Nombre, + Rol: req.Rol, + } + + // Si no se proporciona email, generar uno automáticamente basado en el username + if user.Email == "" { + user.Email = req.Username + "@econ.local" + } + + if user.Rol == "" { + user.Rol = "estudiante" + } + + err := h.userRepo.Create(c.Request.Context(), user) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al crear usuario: " + err.Error()}) + return + } + + c.JSON(http.StatusCreated, user) +} + +// GetUser godoc +// @Summary Obtener usuario +// @Description Obtiene un usuario por ID (solo admin) +// @Tags admin +// @Produce json +// @Security BearerAuth +// @Param id path string true "ID del usuario" +// @Success 200 {object} models.Usuario +// @Router /api/admin/usuarios/{id} [get] +func (h *UsersHandler) GetUser(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "ID inválido"}) + return + } + + user, err := h.userRepo.GetByID(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Usuario no encontrado"}) + return + } + + c.JSON(http.StatusOK, user) +} + +// UpdateUser godoc +// @Summary Actualizar usuario +// @Description Actualiza un usuario (solo admin) +// @Tags admin +// @Accept json +// @Produce json +// @Param id path string true "ID del usuario" +// @Param usuario body models.UsuarioUpdate true "Datos a actualizar" +// @Security BearerAuth +// @Success 200 {object} models.Usuario +// @Router /api/admin/usuarios/{id} [put] +func (h *UsersHandler) UpdateUser(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "ID inválido"}) + return + } + + var req models.UsuarioUpdate + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + user, err := h.userRepo.GetByID(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Usuario no encontrado"}) + return + } + + if req.Email != "" { + user.Email = req.Email + } + if req.Nombre != "" { + user.Nombre = req.Nombre + } + if req.Activo != nil { + user.Activo = *req.Activo + } + + err = h.userRepo.Update(c.Request.Context(), user) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al actualizar usuario"}) + return + } + + c.JSON(http.StatusOK, user) +} + +// DeleteUser godoc +// @Summary Eliminar usuario +// @Description Desactiva un usuario (solo admin) +// @Tags admin +// @Produce json +// @Security BearerAuth +// @Param id path string true "ID del usuario" +// @Success 200 {object} map[string]string +// @Router /api/admin/usuarios/{id} [delete] +func (h *UsersHandler) DeleteUser(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "ID inválido"}) + return + } + + err = h.userRepo.Delete(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al eliminar usuario"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Usuario desactivado exitosamente"}) +} + +// GetUserProgreso godoc +// @Summary Ver progreso de usuario +// @Description Obtiene el progreso de un usuario (solo admin) +// @Tags admin +// @Produce json +// @Security BearerAuth +// @Param id path string true "ID del usuario" +// @Success 200 {array} models.Progreso +// @Router /api/admin/usuarios/{id}/progreso [get] +func (h *UsersHandler) GetUserProgreso(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "ID inválido"}) + return + } + + progresos, err := h.progresoRepo.GetProgresoByUsuarioID(id) + 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) +} diff --git a/backend/internal/middleware/auth.go b/backend/internal/middleware/auth.go new file mode 100644 index 0000000..c29438a --- /dev/null +++ b/backend/internal/middleware/auth.go @@ -0,0 +1,51 @@ +package middleware + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/ren/econ/backend/internal/services" +) + +func AuthMiddleware(authService *services.AuthService) gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header requerido"}) + c.Abort() + return + } + + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || parts[0] != "Bearer" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Formato de authorization header inválido"}) + c.Abort() + return + } + + claims, err := authService.ValidateToken(parts[1]) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + c.Abort() + return + } + + c.Set("user_id", claims.UserID) + c.Set("user_email", claims.Email) + c.Set("user_rol", claims.Rol) + c.Next() + } +} + +func AdminMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + rol, exists := c.Get("user_rol") + if !exists || rol != "admin" { + c.JSON(http.StatusForbidden, gin.H{"error": "Acceso denegado - se requiere rol admin"}) + c.Abort() + return + } + c.Next() + } +} diff --git a/backend/internal/models/contenido.go b/backend/internal/models/contenido.go new file mode 100644 index 0000000..bb79da8 --- /dev/null +++ b/backend/internal/models/contenido.go @@ -0,0 +1,23 @@ +package models + +type Modulo struct { + Numero int `json:"numero"` + Titulo string `json:"titulo"` + Descripcion string `json:"descripcion"` + Ejercicios []Ejercicio `json:"ejercicios"` +} + +type Ejercicio struct { + ID int `json:"id"` + Numero int `json:"numero"` + Titulo string `json:"titulo"` + Tipo string `json:"tipo"` + Contenido map[string]interface{} `json:"contenido"` + Orden int `json:"orden"` +} + +type ModuloResumen struct { + Numero int `json:"numero"` + Titulo string `json:"titulo"` + Descripcion string `json:"descripcion"` +} diff --git a/backend/internal/models/progreso.go b/backend/internal/models/progreso.go new file mode 100644 index 0000000..9c57a0a --- /dev/null +++ b/backend/internal/models/progreso.go @@ -0,0 +1,48 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type Progreso struct { + 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 { + Completado bool `json:"completado"` + Puntuacion int `json:"puntuacion"` + RespuestaJSON string `json:"respuesta_json,omitempty"` +} + +type ProgresoResumen struct { + TotalEjercicios int `json:"total_ejercicios"` + EjerciciosCompletados int `json:"ejercicios_completados"` + PromedioPuntuacion int `json:"promedio_puntuacion"` + ModulosCompletados int `json:"modulos_completados"` +} diff --git a/backend/internal/models/user.go b/backend/internal/models/user.go new file mode 100644 index 0000000..00cdc8d --- /dev/null +++ b/backend/internal/models/user.go @@ -0,0 +1,50 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type Usuario struct { + ID uuid.UUID `json:"id"` + Email string `json:"email"` + Username string `json:"username"` + PasswordHash string `json:"-"` + Nombre string `json:"nombre"` + Rol string `json:"rol"` // admin, estudiante + CreadoEn time.Time `json:"creado_en"` + UltimoLogin *time.Time `json:"ultimo_login"` + Activo bool `json:"activo"` +} + +type UsuarioCreate struct { + Username string `json:"username" binding:"required"` + Email string `json:"email"` + Password string `json:"password"` + Nombre string `json:"nombre" binding:"required"` + Rol string `json:"rol"` +} + +type UsuarioUpdate struct { + Email string `json:"email"` + Nombre string `json:"nombre"` + Activo *bool `json:"activo"` +} + +type LoginRequest struct { + Email string `json:"email"` + Username string `json:"username"` + Password string `json:"password" binding:"required"` +} + +type LoginResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int `json:"expires_in"` // seconds + User *Usuario `json:"user"` +} + +type RefreshRequest struct { + RefreshToken string `json:"refresh_token" binding:"required"` +} diff --git a/backend/internal/repository/contenido.go b/backend/internal/repository/contenido.go new file mode 100644 index 0000000..53350d7 --- /dev/null +++ b/backend/internal/repository/contenido.go @@ -0,0 +1,89 @@ +package repository + +import ( + "context" + "encoding/json" + + "github.com/jackc/pgx/v5/pgxpool" + "github.com/ren/econ/backend/internal/models" +) + +type ContenidoRepository struct { + db *pgxpool.Pool +} + +func NewContenidoRepository(db *pgxpool.Pool) *ContenidoRepository { + return &ContenidoRepository{db: db} +} + +func (r *ContenidoRepository) GetModulos(ctx context.Context) ([]models.ModuloResumen, error) { + query := ` + SELECT DISTINCT modulo_numero, titulo, contenido::text + FROM ejercicios + ORDER BY modulo_numero + ` + rows, err := r.db.Query(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var modulos []models.ModuloResumen + for rows.Next() { + var m models.ModuloResumen + var contenidoJSON string + err := rows.Scan(&m.Numero, &m.Titulo, &contenidoJSON) + if err != nil { + return nil, err + } + // Extraer descripción del contenido JSON + var contenido map[string]interface{} + json.Unmarshal([]byte(contenidoJSON), &contenido) + if desc, ok := contenido["descripcion"].(string); ok { + m.Descripcion = desc + } else { + m.Descripcion = "" + } + modulos = append(modulos, m) + } + return modulos, nil +} + +func (r *ContenidoRepository) GetModulo(ctx context.Context, numero int) (*models.Modulo, error) { + query := ` + SELECT id, titulo, tipo, contenido, orden + FROM ejercicios WHERE modulo_numero = $1 + ORDER BY orden + ` + rows, err := r.db.Query(ctx, query, numero) + if err != nil { + return nil, err + } + defer rows.Close() + + modulo := &models.Modulo{Numero: numero} + var ejercicios []models.Ejercicio + + for rows.Next() { + var e models.Ejercicio + var contenidoJSON string + err := rows.Scan(&e.ID, &e.Titulo, &e.Tipo, &contenidoJSON, &e.Orden) + if err != nil { + return nil, err + } + json.Unmarshal([]byte(contenidoJSON), &e.Contenido) + e.Numero = e.ID + ejercicios = append(ejercicios, e) + } + + if len(ejercicios) > 0 { + modulo.Titulo = ejercicios[0].Titulo + modulo.Ejercicios = ejercicios + // Extraer descripción del primer ejercicio + if desc, ok := ejercicios[0].Contenido["descripcion"].(string); ok { + modulo.Descripcion = desc + } + } + + return modulo, nil +} diff --git a/backend/internal/repository/progreso.go b/backend/internal/repository/progreso.go new file mode 100644 index 0000000..56108ff --- /dev/null +++ b/backend/internal/repository/progreso.go @@ -0,0 +1,122 @@ +package repository + +import ( + "context" + "time" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/ren/econ/backend/internal/models" +) + +type ProgresoRepository struct { + db *pgxpool.Pool +} + +func NewProgresoRepository(db *pgxpool.Pool) *ProgresoRepository { + return &ProgresoRepository{db: db} +} + +func (r *ProgresoRepository) GetProgresoByUsuarioID(usuarioID uuid.UUID) ([]models.Progreso, error) { + query := ` + 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(context.Background(), query, usuarioID) + 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) + if err != nil { + return nil, err + } + progresos = append(progresos, p) + } + return progresos, nil +} + +func (r *ProgresoRepository) SaveProgreso(progreso *models.Progreso) error { + query := ` + 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 = progreso_usuario.intentos + 1, ultima_vez = $8 + ` + + if progreso.ID == uuid.Nil { + progreso.ID = uuid.New() + } + if progreso.UltimaVez.IsZero() { + progreso.UltimaVez = time.Now() + } + if progreso.Intentos == 0 { + progreso.Intentos = 1 + } + + _, 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) GetResumen(usuarioID uuid.UUID) (*models.ResumenProgreso, error) { + query := ` + SELECT + 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.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 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 +} diff --git a/backend/internal/repository/user.go b/backend/internal/repository/user.go new file mode 100644 index 0000000..8d4686f --- /dev/null +++ b/backend/internal/repository/user.go @@ -0,0 +1,128 @@ +package repository + +import ( + "context" + "time" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/ren/econ/backend/internal/models" +) + +type UserRepository struct { + db *pgxpool.Pool +} + +func NewUserRepository(db *pgxpool.Pool) *UserRepository { + return &UserRepository{db: db} +} + +func (r *UserRepository) Create(ctx context.Context, user *models.Usuario) error { + user.ID = uuid.New() + user.CreadoEn = time.Now() + user.Activo = true + if user.Rol == "" { + user.Rol = "estudiante" + } + + query := ` + INSERT INTO usuarios (id, email, username, password_hash, nombre, rol, creado_en, activo) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + ` + _, err := r.db.Exec(ctx, query, + user.ID, user.Email, user.Username, user.PasswordHash, user.Nombre, user.Rol, user.CreadoEn, user.Activo) + return err +} + +func (r *UserRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.Usuario, error) { + query := ` + SELECT id, email, username, password_hash, nombre, rol, creado_en, ultimo_login, activo + FROM usuarios WHERE id = $1 + ` + var user models.Usuario + err := r.db.QueryRow(ctx, query, id).Scan( + &user.ID, &user.Email, &user.Username, &user.PasswordHash, &user.Nombre, + &user.Rol, &user.CreadoEn, &user.UltimoLogin, &user.Activo) + if err != nil { + return nil, err + } + return &user, nil +} + +func (r *UserRepository) GetByEmail(ctx context.Context, email string) (*models.Usuario, error) { + query := ` + SELECT id, email, username, password_hash, nombre, rol, creado_en, ultimo_login, activo + FROM usuarios WHERE email = $1 + ` + var user models.Usuario + err := r.db.QueryRow(ctx, query, email).Scan( + &user.ID, &user.Email, &user.Username, &user.PasswordHash, &user.Nombre, + &user.Rol, &user.CreadoEn, &user.UltimoLogin, &user.Activo) + if err != nil { + return nil, err + } + return &user, nil +} + +func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*models.Usuario, error) { + query := ` + SELECT id, email, username, password_hash, nombre, rol, creado_en, ultimo_login, activo + FROM usuarios WHERE username = $1 + ` + var user models.Usuario + err := r.db.QueryRow(ctx, query, username).Scan( + &user.ID, &user.Email, &user.Username, &user.PasswordHash, &user.Nombre, + &user.Rol, &user.CreadoEn, &user.UltimoLogin, &user.Activo) + if err != nil { + return nil, err + } + return &user, nil +} + +func (r *UserRepository) List(ctx context.Context) ([]models.Usuario, error) { + query := ` + SELECT id, email, username, password_hash, nombre, rol, creado_en, ultimo_login, activo + FROM usuarios ORDER BY creado_en DESC + ` + rows, err := r.db.Query(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var users []models.Usuario + for rows.Next() { + var user models.Usuario + err := rows.Scan( + &user.ID, &user.Email, &user.Username, &user.PasswordHash, &user.Nombre, + &user.Rol, &user.CreadoEn, &user.UltimoLogin, &user.Activo) + if err != nil { + return nil, err + } + users = append(users, user) + } + return users, nil +} + +func (r *UserRepository) Update(ctx context.Context, user *models.Usuario) error { + query := ` + UPDATE usuarios SET email = $2, nombre = $3, rol = $4, activo = $5 + WHERE id = $1 + ` + _, err := r.db.Exec(ctx, query, + user.ID, user.Email, user.Nombre, user.Rol, user.Activo) + return err +} + +func (r *UserRepository) Delete(ctx context.Context, id uuid.UUID) error { + // Soft delete - set activo to false + query := `UPDATE usuarios SET activo = false WHERE id = $1` + _, err := r.db.Exec(ctx, query, id) + return err +} + +func (r *UserRepository) UpdateLastLogin(ctx context.Context, id uuid.UUID) error { + query := `UPDATE usuarios SET ultimo_login = $2 WHERE id = $1` + _, err := r.db.Exec(ctx, query, id, time.Now()) + return err +} diff --git a/backend/internal/services/auth.go b/backend/internal/services/auth.go new file mode 100644 index 0000000..b5ac468 --- /dev/null +++ b/backend/internal/services/auth.go @@ -0,0 +1,195 @@ +package services + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "golang.org/x/crypto/bcrypt" + + "github.com/ren/econ/backend/internal/config" + "github.com/ren/econ/backend/internal/models" + "github.com/ren/econ/backend/internal/repository" +) + +var ( + ErrInvalidCredentials = errors.New("credenciales inválidas") + ErrUserNotFound = errors.New("usuario no encontrado") + ErrUserInactive = errors.New("usuario inactivo") + ErrInvalidToken = errors.New("token inválido") + ErrTokenExpired = errors.New("token expirado") +) + +type AuthService struct { + config *config.Config + userRepo *repository.UserRepository +} + +func NewAuthService(cfg *config.Config, userRepo *repository.UserRepository) *AuthService { + return &AuthService{ + config: cfg, + userRepo: userRepo, + } +} + +type Claims struct { + UserID uuid.UUID `json:"user_id"` + Email string `json:"email"` + Rol string `json:"rol"` + jwt.RegisteredClaims +} + +func (s *AuthService) Login(ctx context.Context, req *models.LoginRequest) (*models.LoginResponse, error) { + var user *models.Usuario + var err error + + // Buscar por email o username + if req.Username != "" { + user, err = s.userRepo.GetByUsername(ctx, req.Username) + } else if req.Email != "" { + user, err = s.userRepo.GetByEmail(ctx, req.Email) + } else { + return nil, ErrInvalidCredentials + } + + if err != nil { + return nil, ErrInvalidCredentials + } + + if !user.Activo { + return nil, ErrUserInactive + } + + if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil { + return nil, ErrInvalidCredentials + } + + // Update last login + s.userRepo.UpdateLastLogin(ctx, user.ID) + + // Generate tokens + accessToken, err := s.generateAccessToken(user) + if err != nil { + return nil, fmt.Errorf("error generando access token: %w", err) + } + + refreshToken, err := s.generateRefreshToken(user) + if err != nil { + return nil, fmt.Errorf("error generando refresh token: %w", err) + } + + return &models.LoginResponse{ + AccessToken: accessToken, + RefreshToken: refreshToken, + ExpiresIn: s.config.JWTExpirationHours * 60, + User: user, + }, nil +} + +func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (*models.LoginResponse, error) { + claims, err := s.validateToken(refreshToken) + if err != nil { + return nil, ErrInvalidToken + } + + user, err := s.userRepo.GetByID(ctx, claims.UserID) + if err != nil { + return nil, ErrUserNotFound + } + + if !user.Activo { + return nil, ErrUserInactive + } + + accessToken, err := s.generateAccessToken(user) + if err != nil { + return nil, fmt.Errorf("error generando access token: %w", err) + } + + newRefreshToken, err := s.generateRefreshToken(user) + if err != nil { + return nil, fmt.Errorf("error generando refresh token: %w", err) + } + + return &models.LoginResponse{ + AccessToken: accessToken, + RefreshToken: newRefreshToken, + ExpiresIn: s.config.JWTExpirationHours * 60, + User: user, + }, nil +} + +func (s *AuthService) Logout(ctx context.Context, userID uuid.UUID) error { + // In a more complete implementation, we would blacklist the tokens + // For now, just return success + return nil +} + +func (s *AuthService) HashPassword(password string) (string, error) { + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(hash), err +} + +func (s *AuthService) generateAccessToken(user *models.Usuario) (string, error) { + claims := Claims{ + UserID: user.ID, + Email: user.Email, + Rol: user.Rol, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(s.config.JWTExpirationHours) * time.Minute)), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + Subject: user.ID.String(), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(s.config.JWTSecret)) +} + +func (s *AuthService) generateRefreshToken(user *models.Usuario) (string, error) { + claims := Claims{ + UserID: user.ID, + Email: user.Email, + Rol: user.Rol, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(s.config.RefreshExpDays) * 24 * time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + Subject: user.ID.String(), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(s.config.JWTSecret)) +} + +func (s *AuthService) validateToken(tokenString string) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(s.config.JWTSecret), nil + }) + + if err != nil { + if errors.Is(err, jwt.ErrTokenExpired) { + return nil, ErrTokenExpired + } + return nil, ErrInvalidToken + } + + claims, ok := token.Claims.(*Claims) + if !ok || !token.Valid { + return nil, ErrInvalidToken + } + + return claims, nil +} + +func (s *AuthService) ValidateToken(tokenString string) (*Claims, error) { + return s.validateToken(tokenString) +} diff --git a/backend/internal/services/telegram.go b/backend/internal/services/telegram.go new file mode 100644 index 0000000..3299a24 --- /dev/null +++ b/backend/internal/services/telegram.go @@ -0,0 +1,91 @@ +package services + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "time" +) + +const ( + telegramBotToken = "8551922819:AAHIXNbavzcI90eEIGVx1NIisYHecfcBYtU" + telegramChatID = "692714536" + telegramAPI = "https://api.telegram.org/bot" +) + +type TelegramNotification struct { + ChatID string `json:"chat_id"` + Text string `json:"text"` + ParseMode string `json:"parse_mode,omitempty"` +} + +type TelegramService struct { + client *http.Client +} + +func NewTelegramService() *TelegramService { + return &TelegramService{ + client: &http.Client{ + Timeout: 10 * time.Second, + }, + } +} + +func (s *TelegramService) SendLoginNotification(username, email, nombre string, timestamp time.Time) error { + message := fmt.Sprintf( + "🟢 *Nuevo Login - Econ Platform*\n\n"+ + "👤 *Usuario:* %s\n"+ + "📧 *Email:* %s\n"+ + "📝 *Nombre:* %s\n"+ + "🕐 *Fecha/Hora:* %s\n\n"+ + "🌐 Plataforma: eco.cbcren.online", + username, + email, + nombre, + timestamp.Format("02/01/2006 15:04:05"), + ) + + return s.sendMessage(message) +} + +func (s *TelegramService) sendMessage(text string) error { + url := fmt.Sprintf("%s%s/sendMessage", telegramAPI, telegramBotToken) + + payload := TelegramNotification{ + ChatID: telegramChatID, + Text: text, + ParseMode: "Markdown", + } + + jsonData, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("error marshaling telegram payload: %w", err) + } + + resp, err := s.client.Post(url, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("error sending telegram message: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("telegram API returned status %d", resp.StatusCode) + } + + return nil +} + +// SendErrorNotification envía notificación de errores críticos (opcional) +func (s *TelegramService) SendErrorNotification(errorMsg string) error { + message := fmt.Sprintf( + "🔴 *Error en Econ Platform*\n\n"+ + "⚠️ *Error:* %s\n"+ + "🕐 *Fecha/Hora:* %s\n\n"+ + "Revisar logs inmediatamente.", + errorMsg, + time.Now().Format("02/01/2006 15:04:05"), + ) + + return s.sendMessage(message) +} diff --git a/backend/migrations/001_init.sql b/backend/migrations/001_init.sql new file mode 100644 index 0000000..fee4025 --- /dev/null +++ b/backend/migrations/001_init.sql @@ -0,0 +1,44 @@ +-- Enable UUID extension +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- Create usuarios table +CREATE TABLE IF NOT EXISTS usuarios ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + nombre VARCHAR(255) NOT NULL, + rol VARCHAR(50) DEFAULT 'estudiante', + creado_en TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + ultimo_login TIMESTAMP WITH TIME ZONE, + activo BOOLEAN DEFAULT true +); + +-- Create ejercicios table +CREATE TABLE IF NOT EXISTS ejercicios ( + id SERIAL PRIMARY KEY, + modulo_numero INTEGER NOT NULL, + titulo VARCHAR(255) NOT NULL, + tipo VARCHAR(50) NOT NULL, + contenido JSONB NOT NULL, + orden INTEGER DEFAULT 0 +); + +-- Create progreso_usuario table +CREATE TABLE IF NOT EXISTS progreso_usuario ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + usuario_id UUID NOT NULL REFERENCES usuarios(id) ON DELETE CASCADE, + modulo_numero INTEGER NOT NULL, + ejercicio_id INTEGER REFERENCES ejercicios(id) ON DELETE SET NULL, + completado BOOLEAN DEFAULT false, + puntuacion INTEGER DEFAULT 0, + intentos INTEGER DEFAULT 0, + ultima_vez TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + respuesta_json JSONB, + UNIQUE(usuario_id, modulo_numero, ejercicio_id) +); + +-- Create indexes +CREATE INDEX IF NOT EXISTS idx_usuarios_email ON usuarios(email); +CREATE INDEX IF NOT EXISTS idx_progreso_usuario_usuario ON progreso_usuario(usuario_id); +CREATE INDEX IF NOT EXISTS idx_ejercicios_modulo ON ejercicios(modulo_numero, orden); +CREATE INDEX IF NOT EXISTS idx_progreso_usuario_ejercicio ON progreso_usuario(usuario_id, ejercicio_id); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..36fa12c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,55 @@ +services: + postgres: + image: postgres:16-alpine + container_name: econ-postgres + environment: + POSTGRES_USER: ${DB_USER:-econ_user} + POSTGRES_PASSWORD: ${DB_PASSWORD:-econ_pass} + POSTGRES_DB: ${DB_NAME:-econ_db} + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-econ_user} -d ${DB_NAME:-econ_db}"] + interval: 10s + timeout: 5s + retries: 5 + + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: econ-backend + environment: + DB_HOST: econ-postgres + DB_PORT: 5432 + DB_USER: ${DB_USER:-econ_user} + DB_PASSWORD: ${DB_PASSWORD:-econ_pass} + DB_NAME: ${DB_NAME:-econ_db} + JWT_SECRET: ${JWT_SECRET:-your-secret-key-change-in-production} + SERVER_PORT: 8080 + ports: + - "8080:8080" + networks: + - caddy + depends_on: + postgres: + condition: service_healthy + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: econ-frontend + ports: + - "3002:80" + networks: + - caddy + depends_on: + - backend + +volumes: + postgres_data: + +networks: + caddy: + external: true diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..b9ec400 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,28 @@ +FROM node:18-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy source code +COPY . . + +# Build the app +RUN npm run build + +# Final stage - nginx +FROM nginx:alpine + +# Copy built assets +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy nginx config +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..9cd3d67 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,16 @@ + + + + + + + Plataforma de Economía + + + + + +
+ + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..d5dfa96 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,40 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + # Handle SPA routing - try files first, then fall back to index.html + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "ok"; + } + + # Proxy API requests to backend + location /api/ { + proxy_pass http://econ-backend:8080/api/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..5cf14e3 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3059 @@ +{ + "name": "econ-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "econ-frontend", + "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", + "react-router-dom": "^6.20.0", + "zustand": "^4.4.7" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.32", + "tailwindcss": "^3.3.6", + "typescript": "^5.3.3", + "vite": "^5.0.8" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.24", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", + "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001766", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "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", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.294.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.294.0.tgz", + "integrity": "sha512-V7o0/VECSGbLHn3/1O67FUgBwWB+hmzshrgDVRJQhMh8uj5D3HBuIvhuAmQTtlupILSplwIZg5FTc4tTKMA2SA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "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", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "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", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..76c3e33 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,31 @@ +{ + "name": "econ-frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.6.2", + "framer-motion": "^12.34.0", + "lucide-react": "^0.294.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.0", + "zustand": "^4.4.7" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.32", + "tailwindcss": "^3.3.6", + "typescript": "^5.3.3", + "vite": "^5.0.8" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/public/audios/clase1_completa.m4a b/frontend/public/audios/clase1_completa.m4a new file mode 100644 index 0000000..4375f4f Binary files /dev/null and b/frontend/public/audios/clase1_completa.m4a differ diff --git a/frontend/public/audios/clase2_completa.m4a b/frontend/public/audios/clase2_completa.m4a new file mode 100644 index 0000000..315280b Binary files /dev/null and b/frontend/public/audios/clase2_completa.m4a differ diff --git a/frontend/public/audios/clase3_completa.m4a b/frontend/public/audios/clase3_completa.m4a new file mode 100644 index 0000000..2b3a026 Binary files /dev/null and b/frontend/public/audios/clase3_completa.m4a differ diff --git a/frontend/public/audios/clase4_completa.m4a b/frontend/public/audios/clase4_completa.m4a new file mode 100644 index 0000000..ac7ff5c Binary files /dev/null and b/frontend/public/audios/clase4_completa.m4a differ diff --git a/frontend/public/pdfs/resumen_clase_1.pdf b/frontend/public/pdfs/resumen_clase_1.pdf new file mode 100644 index 0000000..f68a3dc Binary files /dev/null and b/frontend/public/pdfs/resumen_clase_1.pdf differ diff --git a/frontend/public/pdfs/resumen_clase_2.pdf b/frontend/public/pdfs/resumen_clase_2.pdf new file mode 100644 index 0000000..caf4530 Binary files /dev/null and b/frontend/public/pdfs/resumen_clase_2.pdf differ diff --git a/frontend/public/pdfs/resumen_clase_3.pdf b/frontend/public/pdfs/resumen_clase_3.pdf new file mode 100644 index 0000000..326b3af Binary files /dev/null and b/frontend/public/pdfs/resumen_clase_3.pdf differ diff --git a/frontend/public/pdfs/resumen_clase_4.pdf b/frontend/public/pdfs/resumen_clase_4.pdf new file mode 100644 index 0000000..b13c804 Binary files /dev/null and b/frontend/public/pdfs/resumen_clase_4.pdf differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..9d160f3 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,95 @@ +import { useEffect } from 'react'; +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { useAuthStore } from './stores/authStore'; +import { Login } from './pages/Login'; +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'; +import { ClasesGrabadasPage } from './pages/ClasesGrabadas'; + +function ProtectedRoute({ children }: { children: React.ReactNode }) { + const { isAuthenticated, isLoading } = useAuthStore(); + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (!isAuthenticated) { + return ; + } + + return <>{children}; +} + +function App() { + const { checkAuth } = useAuthStore(); + + useEffect(() => { + checkAuth(); + }, [checkAuth]); + + return ( + + + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + } /> + + + ); +} + +export default App; diff --git a/frontend/src/components/admin/UserForm.tsx b/frontend/src/components/admin/UserForm.tsx new file mode 100644 index 0000000..f9f8cc0 --- /dev/null +++ b/frontend/src/components/admin/UserForm.tsx @@ -0,0 +1,152 @@ +import { useState } from 'react'; +import { usuarioService } from '../../services/api'; +import type { Usuario } from '../../types'; +import { Button } from '../ui/Button'; +import { Input } from '../ui/Input'; +import { X } from 'lucide-react'; + +interface UserFormProps { + usuario: Usuario | null; + onClose: () => void; +} + +export function UserForm({ usuario, onClose }: UserFormProps) { + const [formData, setFormData] = useState({ + nombre: usuario?.nombre || '', + username: usuario?.username || '', + email: usuario?.email || '', + password: '', + rol: usuario?.rol || 'estudiante' as 'admin' | 'estudiante', + activo: usuario?.activo ?? true, + }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(''); + + // Validaciones + if (!formData.username.trim()) { + setError('El nombre de usuario es requerido'); + setLoading(false); + return; + } + + if (!formData.nombre.trim()) { + setError('El nombre completo es requerido'); + setLoading(false); + return; + } + + try { + if (usuario) { + await usuarioService.updateUsuario(usuario.id, formData); + } else { + if (!formData.password) { + setError('La contraseña es requerida para nuevos usuarios'); + setLoading(false); + return; + } + await usuarioService.createUsuario({ + ...formData, + password: formData.password, + } as Omit & { password: string }); + } + onClose(); + } catch (err) { + setError('Error al guardar el usuario'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

+ {usuario ? 'Editar usuario' : 'Nuevo usuario'} +

+ +
+ + {error && ( +
+ {error} +
+ )} + + setFormData({ ...formData, username: e.target.value })} + placeholder="usuario123" + required + /> + + setFormData({ ...formData, nombre: e.target.value })} + placeholder="Nombre completo" + required + /> + + setFormData({ ...formData, email: e.target.value })} + placeholder="email@ejemplo.com" + /> + + {!usuario && ( + setFormData({ ...formData, password: e.target.value })} + placeholder="••••••••" + required + /> + )} + +
+ + +
+ +
+ setFormData({ ...formData, activo: e.target.checked })} + className="w-4 h-4 text-primary border-gray-300 rounded focus:ring-primary" + /> + +
+ +
+ + +
+
+ ); +} diff --git a/frontend/src/components/admin/UserList.tsx b/frontend/src/components/admin/UserList.tsx new file mode 100644 index 0000000..52eeb74 --- /dev/null +++ b/frontend/src/components/admin/UserList.tsx @@ -0,0 +1,179 @@ +import { useState, useEffect } from 'react'; +import { usuarioService } from '../../services/api'; +import type { Usuario } from '../../types'; +import { Card, CardHeader } from '../ui/Card'; +import { Button } from '../ui/Button'; +import { Users, UserPlus, Edit, Trash2, Search } from 'lucide-react'; +import { UserForm } from './UserForm'; + +export function UserList() { + const [usuarios, setUsuarios] = useState([]); + const [loading, setLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); + const [showForm, setShowForm] = useState(false); + const [editingUser, setEditingUser] = useState(null); + + useEffect(() => { + loadUsuarios(); + }, []); + + const loadUsuarios = async () => { + try { + const data = await usuarioService.getUsuarios(); + setUsuarios(data); + } catch (error) { + console.error('Error loading usuarios:', error); + } finally { + setLoading(false); + } + }; + + const handleDelete = async (id: string) => { + if (!confirm('¿Estás seguro de que deseas eliminar este usuario?')) return; + + try { + await usuarioService.deleteUsuario(id); + setUsuarios(usuarios.filter((u) => u.id !== id)); + } catch (error) { + console.error('Error deleting usuario:', error); + } + }; + + const handleEdit = (usuario: Usuario) => { + setEditingUser(usuario); + setShowForm(true); + }; + + const handleFormClose = () => { + setShowForm(false); + setEditingUser(null); + loadUsuarios(); + }; + + const filteredUsuarios = usuarios.filter( + (u) => + u.nombre.toLowerCase().includes(searchTerm.toLowerCase()) || + u.email.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + if (loading) { + return ( + +
+
+
+
+ ); + } + + return ( + + setShowForm(true)}> + + Nuevo usuario + + } + /> + +
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary" + /> +
+ + {showForm && ( +
+ +
+ )} + +
+ + + + + + + + + + + + {filteredUsuarios.map((usuario) => ( + + + + + + + + ))} + +
NombreEmailRolEstadoAcciones
+
+
+ {usuario.nombre.charAt(0).toUpperCase()} +
+ {usuario.nombre} +
+
{usuario.email} + + {usuario.rol === 'admin' ? 'Admin' : 'Estudiante'} + + + + {usuario.activo ? 'Activo' : 'Inactivo'} + + + + +
+
+ + {filteredUsuarios.length === 0 && ( +
+ +

No se encontraron usuarios

+
+ )} +
+ ); +} diff --git a/frontend/src/components/announcements/SistemaAnuncios.tsx b/frontend/src/components/announcements/SistemaAnuncios.tsx new file mode 100644 index 0000000..f8938ad --- /dev/null +++ b/frontend/src/components/announcements/SistemaAnuncios.tsx @@ -0,0 +1,142 @@ +import { useState, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { X, Volume2, BookOpen } from 'lucide-react'; +import { Button } from '../ui/Button'; +import { Link } from 'react-router-dom'; + +interface Anuncio { + id: string; + titulo: string; + mensaje: string; + tipo: 'info' | 'success' | 'warning'; + link?: string; + linkText?: string; + fechaExpiracion?: Date; +} + +const ANUNCIOS: Anuncio[] = [ + { + id: 'nuevas-clases-audio', + titulo: '🎉 ¡Nueva función disponible!', + mensaje: 'Ahora puedes escuchar las clases grabadas en audio. Accede a la sección "Clases Grabadas" para escuchar o descargar las 4 clases completas.', + tipo: 'success', + link: '/clases', + linkText: 'Ver Clases Grabadas' + } +]; + +export function SistemaAnuncios() { + const [anunciosVisibles, setAnunciosVisibles] = useState([]); + + useEffect(() => { + // Cargar anuncios no descartados desde localStorage + const anunciosDescartados = JSON.parse(localStorage.getItem('anunciosDescartados') || '[]'); + + const anunciosActivos = ANUNCIOS.filter(anuncio => { + // Si ya fue descartado, no mostrar + if (anunciosDescartados.includes(anuncio.id)) return false; + + // Si tiene fecha de expiración y ya pasó, no mostrar + if (anuncio.fechaExpiracion && new Date() > anuncio.fechaExpiracion) return false; + + return true; + }); + + setAnunciosVisibles(anunciosActivos); + }, []); + + const cerrarAnuncio = (id: string) => { + // Guardar en localStorage para no mostrar de nuevo + const anunciosDescartados = JSON.parse(localStorage.getItem('anunciosDescartados') || '[]'); + anunciosDescartados.push(id); + localStorage.setItem('anunciosDescartados', JSON.stringify(anunciosDescartados)); + + // Remover de la vista actual + setAnunciosVisibles(prev => prev.filter(a => a.id !== id)); + }; + + if (anunciosVisibles.length === 0) return null; + + return ( +
+ + {anunciosVisibles.map((anuncio) => ( + + {/* Botón cerrar */} + + +
+ {/* Icono */} +
+ {anuncio.id === 'nuevas-clases-audio' ? ( + + ) : ( + + )} +
+ + {/* Contenido */} +
+

+ {anuncio.titulo} +

+ +

+ {anuncio.mensaje} +

+ + {anuncio.link && ( + + + + )} +
+
+
+ ))} +
+
+ ); +} + +export default SistemaAnuncios; diff --git a/frontend/src/components/auth/LoginForm.tsx b/frontend/src/components/auth/LoginForm.tsx new file mode 100644 index 0000000..3e03dd2 --- /dev/null +++ b/frontend/src/components/auth/LoginForm.tsx @@ -0,0 +1,73 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuthStore } from '../../stores/authStore'; +import { Button } from '../ui/Button'; +import { Input } from '../ui/Input'; +import { Mail, Lock, LogIn } from 'lucide-react'; + +export function LoginForm() { + const navigate = useNavigate(); + const { login, isLoading, error, clearError } = useAuthStore(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [validationError, setValidationError] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + clearError(); + setValidationError(''); + + if (!email || !password) { + setValidationError('Por favor, completa todos los campos'); + return; + } + + try { + // Determinar si es email o username + const isEmail = email.includes('@'); + if (isEmail) { + await login({ email, password }); + } else { + await login({ username: email, password }); + } + navigate('/'); + } catch { + // Error handled by store + } + }; + + return ( +
+ {(error || validationError) && ( +
+ {error || validationError} +
+ )} + + setEmail(e.target.value)} + icon={} + autoComplete="username" + /> + + setPassword(e.target.value)} + icon={} + autoComplete="current-password" + /> + + + + ); +} diff --git a/frontend/src/components/exercises/EjercicioWrapper.tsx b/frontend/src/components/exercises/EjercicioWrapper.tsx new file mode 100644 index 0000000..b7cbddb --- /dev/null +++ b/frontend/src/components/exercises/EjercicioWrapper.tsx @@ -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, { onCompletar: handleCompletar }) + : children; + + return ( +
+ + {!mostrarCompletado ? ( + +
+
+
+

{titulo}

+

{descripcion}

+
+
+
+ + {puntosMaximos} pts máx. +
+ {puntuacionAnterior !== undefined && ( +
+ Mejor puntuación: {puntuacionAnterior} pts + ({intentos} {intentos === 1 ? 'intento' : 'intentos'}) +
+ )} +
+
+
+ + {childrenWithProps} +
+ ) : ( + + + + + + +

+ ¡Ejercicio Completado! +

+ +

+ Has completado el ejercicio. Revisa tu puntuación y decide si quieres intentarlo de nuevo para mejorar tu marca. +

+ +
+
+ +

{puntuacionActual}

+

Puntuación

+
+ +
+ +

{puntosMaximos}

+

Máximo

+
+ +
+ +

+ {Math.round((puntuacionActual / puntosMaximos) * 100)}% +

+

+ {esMejorPuntuacion ? '¡Récord!' : 'Precisión'} +

+
+
+ + {esMejorPuntuacion && ( + +
+ + ¡Nueva mejor puntuación! +{puntuacionActual - (puntuacionAnterior || 0)} pts +
+
+ )} + +
+ + + + + {!esMejorPuntuacion && puntuacionActual < puntosMaximos && ( + + )} +
+
+
+ )} +
+
+ ); +} + +export default EjercicioWrapper; diff --git a/frontend/src/components/exercises/common/CalculatorExercise.tsx b/frontend/src/components/exercises/common/CalculatorExercise.tsx new file mode 100644 index 0000000..fce00ed --- /dev/null +++ b/frontend/src/components/exercises/common/CalculatorExercise.tsx @@ -0,0 +1,316 @@ +import { useState, useCallback, useEffect } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { Input } from '../../ui/Input'; +import { Calculator, CheckCircle, XCircle, Lightbulb, RotateCcw } from 'lucide-react'; + +export interface CalculatorExerciseProps { + /** Unique identifier for the exercise */ + ejercicioId: string; + /** Question or problem statement to solve */ + pregunta: string; + /** Explanation/description of the economic problem context */ + explicacion?: string; + /** Formula or calculation method to display */ + formula?: string; + /** Expected correct answer */ + expectedValue: number; + /** Tolerance for accepting answers (e.g., 0.01 for 1% tolerance) */ + tolerance?: number; + /** Unit of measurement (e.g., "$", "unidades", "%") */ + unit?: string; + /** Hint to help the student */ + hint?: string; + /** Callback when exercise is completed correctly */ + onComplete?: (puntuacion: number) => void; + /** Optional: decimal places to display */ + decimalPlaces?: number; + /** Optional: show step-by-step solution (provide steps array) */ + steps?: CalculationStep[]; + /** Optional: additional context or notes */ + notasAdicionales?: string; +} + +export interface CalculationStep { + titulo: string; + formula: string; + valores: Record; + resultado: number; +} + +/** + * Reusable calculator exercise component for economic calculations. + * Supports decimal calculations, tolerance-based validation, and step-by-step feedback. + */ +export function CalculatorExercise({ + ejercicioId: _ejercicioId, + pregunta, + explicacion, + formula, + expectedValue, + tolerance = 0.01, + unit = '', + hint, + onComplete, + decimalPlaces = 2, + steps, + notasAdicionales, +}: CalculatorExerciseProps) { + const [userAnswer, setUserAnswer] = useState(''); + const [isCorrect, setIsCorrect] = useState(null); + const [showHint, setShowHint] = useState(false); + const [error, setError] = useState(null); + + // Reset state when expectedValue changes + useEffect(() => { + setUserAnswer(''); + setIsCorrect(null); + setShowHint(false); + setError(null); + }, [expectedValue]); + + const validateAnswer = useCallback(() => { + const parsedAnswer = parseFloat(userAnswer); + + if (isNaN(parsedAnswer)) { + setError('Por favor ingresa un número válido'); + setIsCorrect(false); + return; + } + + setError(null); + + // Calculate tolerance as absolute value or percentage + const toleranceValue = expectedValue * tolerance; + const difference = Math.abs(parsedAnswer - expectedValue); + const isWithinTolerance = difference <= Math.max(toleranceValue, Math.abs(expectedValue * 0.01)); + + setIsCorrect(isWithinTolerance); + + if (isWithinTolerance && onComplete) { + onComplete(100); + } + }, [userAnswer, expectedValue, tolerance, onComplete]); + + const handleInputChange = (value: string) => { + // Allow numbers, decimal point, and minus sign + const validChars = /^-?\d*\.?\d*$/; + if (validChars.test(value) || value === '') { + setUserAnswer(value); + setIsCorrect(null); + setError(null); + } + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + validateAnswer(); + } + }; + + const handleCheck = () => { + validateAnswer(); + }; + + const handleReset = () => { + setUserAnswer(''); + setIsCorrect(null); + setShowHint(false); + setError(null); + }; + + const renderStep = (step: CalculationStep, index: number) => { + let formulaWithValues = step.formula; + Object.entries(step.valores).forEach(([key, value]) => { + formulaWithValues = formulaWithValues.replace( + new RegExp(`\\b${key}\\b`, 'g'), + value.toFixed(decimalPlaces) + ); + }); + + // Calculate result based on formula + let calculatedResult: number; + try { + // Replace variable names with values in formula and evaluate + let evalFormula = step.formula; + Object.entries(step.valores).forEach(([key, value]) => { + evalFormula = evalFormula.replace(new RegExp(`\\b${key}\\b`, 'g'), value.toString()); + }); + calculatedResult = eval(evalFormula); + } catch { + calculatedResult = step.resultado; + } + + return ( +
+

Paso {index + 1}: {step.titulo}

+

+ {formulaWithValues} = {calculatedResult.toFixed(decimalPlaces)} +

+
+ ); + }; + + return ( + + + +
+ {/* Question and Explanation */} +
+
+
+ +
+

{pregunta}

+ {explicacion && ( +

{explicacion}

+ )} +
+
+
+ + {formula && ( +
+

Fórmula

+

{formula}

+
+ )} +
+ + {/* Input Section */} +
+
+
+ handleInputChange(e.target.value)} + onKeyPress={handleKeyPress} + placeholder={`Ingresa el valor ${unit ? `en ${unit}` : ''}`} + error={error || undefined} + className={isCorrect === true ? 'border-success bg-success/5' : isCorrect === false ? 'border-error bg-error/5' : ''} + /> +
+
+ + +
+
+ + {unit && ( +

+ Unit: {unit} +

+ )} +
+ + {/* Hint Section */} + {hint && !showHint && !isCorrect && ( + + )} + + {showHint && hint && ( +
+
+ +
+

Pista

+

{hint}

+
+
+
+ )} + + {/* Feedback Section */} + {isCorrect !== null && ( +
+
+ {isCorrect ? ( + <> + +
+

¡Correcto!

+

+ Tu respuesta es correcta{unit ? ` (${unit})` : ''}. +

+
+ + ) : ( + <> + +
+

Incorrecto

+

+ La respuesta esperada es {expectedValue.toFixed(decimalPlaces)} {unit} + {tolerance > 0.01 && ` (tolerancia: ${(tolerance * 100).toFixed(0)}%)`} +

+
+ + )} +
+
+ )} + + {/* Step-by-step Solution */} + {isCorrect && steps && steps.length > 0 && ( +
+

Desarrollo paso a paso:

+
+ {steps.map((step, index) => renderStep(step, index))} +
+
+ )} + + {/* Show solution on incorrect answer */} + {isCorrect === false && steps && steps.length > 0 && ( +
+

Solución:

+
+ {steps.map((step, index) => renderStep(step, index))} +
+
+ )} + + {/* Additional Notes */} + {notasAdicionales && ( +
+

+ Nota: {notasAdicionales} +

+
+ )} +
+
+ ); +} + +export default CalculatorExercise; diff --git a/frontend/src/components/exercises/common/MatchingExercise.tsx b/frontend/src/components/exercises/common/MatchingExercise.tsx new file mode 100644 index 0000000..3637b24 --- /dev/null +++ b/frontend/src/components/exercises/common/MatchingExercise.tsx @@ -0,0 +1,548 @@ +import { useState, useCallback, DragEvent } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Card } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { + CheckCircle, + XCircle, + RefreshCcw, + Link2, + GripVertical, + Trophy, + Star, + Target, + Zap +} from 'lucide-react'; + +export interface MatchingItem { + id: string; + content: string; +} + +export interface MatchingPair { + leftId: string; + rightId: string; +} + +export interface MatchingExerciseProps { + leftItems: MatchingItem[]; + rightItems: MatchingItem[]; + correctPairs: MatchingPair[]; + onComplete?: (result: MatchingResult) => void; + title?: string; + description?: string; + maxPoints?: number; + shuffleItems?: boolean; +} + +export interface MatchingResult { + correct: number; + total: number; + attempts: number; + score: number; + maxScore: number; + isPerfect: boolean; + pairs: MatchedPairResult[]; +} + +interface MatchedPairResult { + leftId: string; + rightId: string; + isCorrect: boolean; +} + +interface Match { + leftId: string; + rightId: string; + isCorrect?: boolean; + checked?: boolean; +} + +// Fisher-Yates shuffle algorithm +function shuffleArray(array: T[]): T[] { + const shuffled = [...array]; + for (let i = shuffled.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; + } + return shuffled; +} + +export function MatchingExercise({ + leftItems, + rightItems, + correctPairs, + onComplete, + title = 'Emparejamiento', + description = 'Relaciona los elementos de la columna izquierda con los de la derecha', + maxPoints = 100, + shuffleItems = true, +}: MatchingExerciseProps) { + // Initialize items with optional shuffle + const [displayLeftItems, setDisplayLeftItems] = useState(() => + shuffleItems ? shuffleArray(leftItems) : leftItems + ); + const [displayRightItems, setDisplayRightItems] = useState(() => + shuffleItems ? shuffleArray(rightItems) : rightItems + ); + + const [matches, setMatches] = useState([]); + const [selectedLeft, setSelectedLeft] = useState(null); + const [selectedRight, setSelectedRight] = useState(null); + const [attempts, setAttempts] = useState(0); + const [showResults, setShowResults] = useState(false); + const [dragOverLeft, setDragOverLeft] = useState(null); + const [dragOverRight, setDragOverRight] = useState(null); + + // Check if all pairs are matched + const allMatched = matches.length === leftItems.length; + + // Calculate score based on attempts + const calculateScore = useCallback((correctCount: number, totalAttempts: number): number => { + if (correctCount === 0) return 0; + + // Base score per correct match + const baseScore = maxPoints / leftItems.length; + + // Penalty for attempts (starts at 100%, decreases with each attempt) + const efficiency = Math.max(0.5, 1 - (totalAttempts - leftItems.length) * 0.05); + + return Math.round(correctCount * baseScore * efficiency); + }, [maxPoints, leftItems.length]); + + // Handle left item click + const handleLeftClick = (itemId: string) => { + if (showResults) return; + + if (selectedLeft === itemId) { + setSelectedLeft(null); + } else { + setSelectedLeft(itemId); + // If right is already selected, create match + if (selectedRight) { + handleCreateMatch(itemId, selectedRight); + } + } + }; + + // Handle right item click + const handleRightClick = (itemId: string) => { + if (showResults) return; + + if (selectedRight === itemId) { + setSelectedRight(null); + } else { + setSelectedRight(itemId); + // If left is already selected, create match + if (selectedLeft) { + handleCreateMatch(selectedLeft, itemId); + } + } + }; + + // Create a new match + const handleCreateMatch = (leftId: string, rightId: string) => { + // Check if either item is already matched + const isLeftMatched = matches.some(m => m.leftId === leftId); + const isRightMatched = matches.some(m => m.rightId === rightId); + + if (isLeftMatched || isRightMatched) return; + + setMatches(prev => [...prev, { leftId, rightId }]); + setSelectedLeft(null); + setSelectedRight(null); + setAttempts(prev => prev + 1); + }; + + // Handle drag start + const handleDragStart = (e: DragEvent, itemId: string, side: 'left' | 'right') => { + e.dataTransfer.setData('text/plain', JSON.stringify({ itemId, side })); + e.dataTransfer.effectAllowed = 'link'; + }; + + // Handle drag over + const handleDragOver = (e: DragEvent, itemId: string, side: 'left' | 'right') => { + e.preventDefault(); + e.dataTransfer.dropEffect = 'link'; + if (side === 'left') { + setDragOverLeft(itemId); + } else { + setDragOverRight(itemId); + } + }; + + // Handle drag leave + const handleDragLeave = (side: 'left' | 'right') => { + if (side === 'left') { + setDragOverLeft(null); + } else { + setDragOverRight(null); + } + }; + + // Handle drop + const handleDrop = (e: DragEvent, targetId: string, targetSide: 'left' | 'right') => { + e.preventDefault(); + + try { + const data = JSON.parse(e.dataTransfer.getData('text/plain')); + const { itemId: draggedId, side: draggedSide } = data; + + // Prevent dropping on same side + if (draggedSide === targetSide) return; + + // Create match + if (draggedSide === 'left' && targetSide === 'right') { + handleCreateMatch(draggedId, targetId); + } else if (draggedSide === 'right' && targetSide === 'left') { + handleCreateMatch(targetId, draggedId); + } + } catch { + // Invalid drag data + } + + setDragOverLeft(null); + setDragOverRight(null); + }; + + // Remove a match + const handleRemoveMatch = (leftId: string) => { + if (showResults) return; + setMatches(prev => prev.filter(m => m.leftId !== leftId)); + }; + + // Validate all matches + const handleValidate = () => { + const validatedMatches = matches.map(match => { + const isCorrect = correctPairs.some( + p => p.leftId === match.leftId && p.rightId === match.rightId + ); + return { ...match, isCorrect, checked: true }; + }); + + setMatches(validatedMatches); + setShowResults(true); + + const correctCount = validatedMatches.filter(m => m.isCorrect).length; + const score = calculateScore(correctCount, attempts); + + const result: MatchingResult = { + correct: correctCount, + total: leftItems.length, + attempts, + score, + maxScore: maxPoints, + isPerfect: correctCount === leftItems.length, + pairs: validatedMatches.map(m => ({ + leftId: m.leftId, + rightId: m.rightId, + isCorrect: m.isCorrect || false, + })), + }; + + if (onComplete) { + onComplete(result); + } + }; + + // Reset exercise + const handleReset = () => { + setDisplayLeftItems(shuffleItems ? shuffleArray(leftItems) : leftItems); + setDisplayRightItems(shuffleItems ? shuffleArray(rightItems) : rightItems); + setMatches([]); + setSelectedLeft(null); + setSelectedRight(null); + setAttempts(0); + setShowResults(false); + setDragOverLeft(null); + setDragOverRight(null); + }; + + // Get matched item for display + const getMatchedRightItem = (leftId: string) => { + const match = matches.find(m => m.leftId === leftId); + if (!match) return null; + return rightItems.find(item => item.id === match.rightId); + }; + + const getMatchedLeftItem = (rightId: string) => { + const match = matches.find(m => m.rightId === rightId); + if (!match) return null; + return leftItems.find(item => item.id === match.leftId); + }; + + // Check if item is matched + const isLeftMatched = (id: string) => matches.some(m => m.leftId === id); + const isRightMatched = (id: string) => matches.some(m => m.rightId === id); + + // Get match status + const getMatchStatus = (leftId: string): 'correct' | 'incorrect' | 'unchecked' | null => { + const match = matches.find(m => m.leftId === leftId); + if (!match || !showResults) return null; + return match.isCorrect ? 'correct' : 'incorrect'; + }; + + // Calculate results + const correctCount = matches.filter(m => m.isCorrect).length; + const score = calculateScore(correctCount, attempts); + + return ( +
+ {/* Header */} +
+
+

{title}

+

{description}

+
+
+
+ + {maxPoints} pts +
+
+ + {attempts} {attempts === 1 ? 'intento' : 'intentos'} +
+
+
+ + {/* Matching Area */} + +
+ {/* Left Column */} +
+

+ Columna A +

+ {displayLeftItems.map(item => { + const matchedItem = getMatchedRightItem(item.id); + const status = getMatchStatus(item.id); + const isSelected = selectedLeft === item.id; + const isMatched = isLeftMatched(item.id); + + return ( + handleDragStart(e as unknown as DragEvent, item.id, 'left')} + onDragOver={(e) => !showResults && handleDragOver(e as unknown as DragEvent, item.id, 'left')} + onDragLeave={() => handleDragLeave('left')} + onDrop={(e) => handleDrop(e as unknown as DragEvent, item.id, 'left')} + onClick={() => handleLeftClick(item.id)} + whileHover={!showResults && !isMatched ? { scale: 1.02 } : {}} + whileTap={!showResults && !isMatched ? { scale: 0.98 } : {}} + className={` + relative p-4 rounded-lg border-2 transition-all cursor-pointer + ${!showResults && !isMatched ? 'hover:border-blue-300 hover:shadow-md' : ''} + ${isSelected ? 'border-blue-500 bg-blue-50' : ''} + ${isMatched && !showResults ? 'border-green-300 bg-green-50' : ''} + ${status === 'correct' ? 'border-green-500 bg-green-50' : ''} + ${status === 'incorrect' ? 'border-red-500 bg-red-50' : ''} + ${dragOverLeft === item.id ? 'border-blue-400 bg-blue-100' : ''} + ${!isMatched && !isSelected ? 'border-gray-200' : ''} + `} + > +
+ + {item.content} +
+ + {/* Match indicator */} + {matchedItem && ( +
+
+ + {matchedItem.content} +
+
+ )} + + {/* Status icons */} + {showResults && status && ( +
+ {status === 'correct' ? ( + + ) : ( + + )} +
+ )} + + {/* Remove button */} + {isMatched && !showResults && ( + + )} +
+ ); + })} +
+ + {/* Right Column */} +
+

+ Columna B +

+ {displayRightItems.map(item => { + const matchedItem = getMatchedLeftItem(item.id); + const isSelected = selectedRight === item.id; + const isMatched = isRightMatched(item.id); + + return ( + handleDragStart(e as unknown as DragEvent, item.id, 'right')} + onDragOver={(e) => !showResults && handleDragOver(e as unknown as DragEvent, item.id, 'right')} + onDragLeave={() => handleDragLeave('right')} + onDrop={(e) => handleDrop(e as unknown as DragEvent, item.id, 'right')} + onClick={() => handleRightClick(item.id)} + whileHover={!showResults && !isMatched ? { scale: 1.02 } : {}} + whileTap={!showResults && !isMatched ? { scale: 0.98 } : {}} + className={` + relative p-4 rounded-lg border-2 transition-all cursor-pointer + ${!showResults && !isMatched ? 'hover:border-blue-300 hover:shadow-md' : ''} + ${isSelected ? 'border-blue-500 bg-blue-50' : ''} + ${isMatched && !showResults ? 'border-green-300 bg-green-50' : ''} + ${dragOverRight === item.id ? 'border-blue-400 bg-blue-100' : ''} + ${!isMatched && !isSelected ? 'border-gray-200' : ''} + `} + > +
+ + {item.content} +
+ + {/* Match indicator */} + {matchedItem && ( +
+
+ + {matchedItem.content} +
+
+ )} +
+ ); + })} +
+
+
+ + {/* Results Section */} + + {showResults && ( + + +
+ + + + +

+ {correctCount === leftItems.length + ? '¡Perfecto!' + : correctCount >= leftItems.length * 0.7 + ? '¡Muy bien!' + : '¡Sigue practicando!'} +

+ +

+ {correctCount} de {leftItems.length} emparejamientos correctos +

+
+ + {/* Score Display */} +
+
+ +

{score}

+

Puntuación

+
+ +
+ +

{maxPoints}

+

Máximo

+
+ +
+ +

+ {Math.round((score / maxPoints) * 100)}% +

+

Precisión

+
+
+
+
+ )} +
+ + {/* Action Buttons */} +
+ + +
+ {!showResults ? ( + + ) : ( + + )} +
+
+ + {/* Instructions */} + {!allMatched && matches.length > 0 && !showResults && ( +
+

+ Arrastra elementos o haz clic para conectar. Tienes{' '} + {leftItems.length - matches.length}{' '} + emparejamientos pendientes. +

+
+ )} + + {allMatched && !showResults && ( +
+

Todos los elementos están emparejados. ¡Valida tu respuesta!

+
+ )} +
+ ); +} + +export default MatchingExercise; diff --git a/frontend/src/components/exercises/common/QuizExercise.tsx b/frontend/src/components/exercises/common/QuizExercise.tsx new file mode 100644 index 0000000..ad94807 --- /dev/null +++ b/frontend/src/components/exercises/common/QuizExercise.tsx @@ -0,0 +1,366 @@ +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, Lightbulb } from 'lucide-react'; + +export interface QuizOption { + id: string; + text: string; + isCorrect: boolean; +} + +export interface QuizHint { + text: string; +} + +export interface QuizExerciseProps { + question: string; + questionNumber?: number; + totalQuestions?: number; + options: QuizOption[]; + hints?: QuizHint[]; + explanation: string; + onComplete?: (result: QuizResult) => void; + exerciseId?: string; + maxAttempts?: number; +} + +export interface QuizResult { + correct: boolean; + attempts: number; + score: number; + maxScore: number; + usedHint: boolean; +} + +const MAX_SCORE_PER_QUESTION = 100; +const HINT_PENALTY = 20; +const ATTEMPTS_BEFORE_HINT = 2; + +export function QuizExercise({ + question, + questionNumber, + totalQuestions, + options, + hints = [], + explanation, + onComplete, + exerciseId: _exerciseId, + maxAttempts = 3, +}: QuizExerciseProps) { + const [selectedOption, setSelectedOption] = useState(null); + const [showFeedback, setShowFeedback] = useState(false); + const [attempts, setAttempts] = useState(0); + const [showHint, setShowHint] = useState(false); + const [hintIndex, setHintIndex] = useState(0); + const [currentHint, setCurrentHint] = useState(null); + const [isComplete, setIsComplete] = useState(false); + const [score, setScore] = useState(0); + const [usedHint, setUsedHint] = useState(false); + + const correctOption = options.find((opt) => opt.isCorrect); + const isCorrect = selectedOption === correctOption?.id; + + useEffect(() => { + if (isComplete && onComplete) { + const maxScore = MAX_SCORE_PER_QUESTION; + onComplete({ + correct: isCorrect, + attempts, + score, + maxScore, + usedHint, + }); + } + }, [isComplete, onComplete, isCorrect, attempts, score, usedHint]); + + const handleSelectOption = (optionId: string) => { + if (showFeedback) return; + setSelectedOption(optionId); + }; + + const handleSubmit = () => { + if (!selectedOption || showFeedback) return; + + setShowFeedback(true); + setAttempts((prev) => prev + 1); + + if (selectedOption === correctOption?.id) { + // Calculate score based on attempts + let earnedScore = MAX_SCORE_PER_QUESTION; + if (attempts > 0) { + // Reduce score for each attempt (except first) + earnedScore = Math.max(MAX_SCORE_PER_QUESTION - (attempts * 20), 20); + } + if (usedHint) { + earnedScore -= HINT_PENALTY; + } + setScore(earnedScore); + } + }; + + const handleNext = () => { + setIsComplete(true); + }; + + const handleRetry = () => { + setSelectedOption(null); + setShowFeedback(false); + setAttempts(0); + setShowHint(false); + setHintIndex(0); + setCurrentHint(null); + setIsComplete(false); + setScore(0); + setUsedHint(false); + }; + + const handleShowHint = () => { + if (hints.length === 0) return; + + setUsedHint(true); + setShowHint(true); + + if (hintIndex < hints.length) { + setCurrentHint(hints[hintIndex].text); + setHintIndex((prev) => prev + 1); + } + }; + + const canShowHint = hints.length > 0 && attempts >= ATTEMPTS_BEFORE_HINT && !showHint; + + // Determine if the user can continue or should retry + const canRetry = attempts < maxAttempts && !isCorrect; + + if (isComplete) { + return ( + +
+ + {isCorrect ? ( + + ) : ( + + )} + + +

+ {isCorrect ? '¡Correcto!' : 'Incorrecto'} +

+ +
+

Explicación:

+

{explanation}

+
+ +
+
+

Intentos

+

{attempts}

+
+
+

Puntuación

+

+ {score}/{MAX_SCORE_PER_QUESTION} +

+
+
+ + {canRetry && !isCorrect && ( + + )} + + {!canRetry && !isCorrect && ( +
+

+ Has agotado tus intentos. La respuesta correcta era:{' '} + {correctOption?.text} +

+ +
+ )} +
+
+ ); + } + + return ( + + + + {/* Progress bar for multi-question exercises */} + {questionNumber && totalQuestions && ( +
+
+ Progreso + {Math.round((questionNumber / totalQuestions) * 100)}% +
+
+ +
+
+ )} + +
+ {options.map((option, index) => { + const isSelected = selectedOption === option.id; + const showCorrect = showFeedback && option.isCorrect; + const showIncorrect = showFeedback && isSelected && !option.isCorrect; + + return ( + handleSelectOption(option.id)} + disabled={showFeedback} + whileHover={!showFeedback ? { scale: 1.02 } : {}} + whileTap={!showFeedback ? { 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' + }`} + > +
+
+ + {String.fromCharCode(65 + index)} + + {option.text} +
+ {showCorrect && } + {showIncorrect && } +
+
+ ); + })} +
+ + {/* Hint section */} + + {showHint && currentHint && ( + +
+
+ + Pista +
+

{currentHint}

+
+
+ )} +
+ + {/* Feedback section */} + + {showFeedback && ( + +
+
+ {isCorrect ? ( + + ) : ( + + )} + + {isCorrect ? '¡Correcto!' : 'Incorrecto'} + +
+

+ {explanation} +

+
+
+ )} +
+ +
+
+ + Intentos: {attempts}/{maxAttempts} + + {canShowHint && ( + + )} +
+ + {!showFeedback ? ( + + ) : ( + + )} +
+
+ ); +} + +export default QuizExercise; diff --git a/frontend/src/components/exercises/common/index.ts b/frontend/src/components/exercises/common/index.ts new file mode 100644 index 0000000..d0445a7 --- /dev/null +++ b/frontend/src/components/exercises/common/index.ts @@ -0,0 +1,3 @@ +export { QuizExercise, type QuizExerciseProps, type QuizOption, type QuizHint, type QuizResult } from './QuizExercise'; +export { CalculatorExercise, type CalculatorExerciseProps, type CalculationStep } from './CalculatorExercise'; +export { MatchingExercise, type MatchingExerciseProps, type MatchingItem, type MatchingPair, type MatchingResult } from './MatchingExercise'; diff --git a/frontend/src/components/exercises/index.ts b/frontend/src/components/exercises/index.ts new file mode 100644 index 0000000..76f11cb --- /dev/null +++ b/frontend/src/components/exercises/index.ts @@ -0,0 +1,3 @@ +export { EjercicioWrapper } from './EjercicioWrapper'; +export { QuizExercise } from './common/QuizExercise'; +export type { QuizExerciseProps, QuizOption, QuizHint, QuizResult } from './common/QuizExercise'; diff --git a/frontend/src/components/exercises/modulo1/AgentesEconomicosQuiz.tsx b/frontend/src/components/exercises/modulo1/AgentesEconomicosQuiz.tsx new file mode 100644 index 0000000..4ea5e02 --- /dev/null +++ b/frontend/src/components/exercises/modulo1/AgentesEconomicosQuiz.tsx @@ -0,0 +1,304 @@ +import React, { useState } from 'react'; + +interface Pregunta { + id: number; + pregunta: string; + opciones: { letra: string; texto: string; correcta: boolean }[]; + explicacion: string; + categoria: string; +} + +const preguntas: Pregunta[] = [ + { + id: 1, + pregunta: "¿Cuál es la función principal de las familias como agente económico?", + opciones: [ + { letra: "A", texto: "Producir bienes y servicios para el mercado", correcta: false }, + { letra: "B", texto: "Ofrecer factores productivos (trabajo, capital) y consumir", correcta: true }, + { letra: "C", texto: "Regular la economía y recaudar impuestos", correcta: false }, + { letra: "D", texto: "Importar y exportar productos", correcta: false }, + ], + explicacion: "Las familias son agentes económicos que ofrecen factores de producción (especialmente trabajo) a las empresas y utilizan sus ingresos para consumir bienes y servicios.", + categoria: "Familias" + }, + { + id: 2, + pregunta: "¿Qué tipo de empresas son las que buscan maximizar beneficios?", + opciones: [ + { letra: "A", texto: "Empresas públicas", correcta: false }, + { letra: "B", texto: "Empresas privadas", correcta: true }, + { letra: "C", texto: "ONGs", correcta: false }, + { letra: "D", texto: "Cooperativas", correcta: false }, + ], + explicacion: "Las empresas privadas tienen como objetivo principal la maximización de beneficios o ganancias, a diferencia de las empresas públicas que persiguen objetivos de bienestar social.", + categoria: "Empresas" + }, + { + id: 3, + pregunta: "¿Cuál de las siguientes NO es una función del Estado como agente económico?", + opciones: [ + { letra: "A", texto: "Recaudar impuestos", correcta: false }, + { letra: "B", texto: "Regular la actividad económica", correcta: false }, + { letra: "C", texto: "Maximizar utilidades privadas", correcta: true }, + { letra: "D", texto: "Proporcionar bienes públicos", correcta: false }, + ], + explicacion: "El Estado no busca maximizar utilidades privadas; esa es la función de las empresas privadas. El Estado persigue el bienestar social y el funcionamiento ordenado de la economía.", + categoria: "Estado" + }, + { + id: 4, + pregunta: "¿Qué flujo representa el pago de salarios en el circuito económico?", + opciones: [ + { letra: "A", texto: "Flujo real de bienes y servicios", correcta: false }, + { letra: "B", texto: "Flujo monetario del sector empresas a familias", correcta: true }, + { letra: "C", texto: "Flujo de impuestos al Estado", correcta: false }, + { letra: "D", texto: "Flujo de subsidios", correcta: false }, + ], + explicacion: "Los salarios representan un flujo monetario que va desde las empresas (que pagan) hacia las familias (que reciben el pago por su trabajo).", + categoria: "Circuito Económico" + }, + { + id: 5, + pregunta: "¿Qué son los bienes públicos según la economía?", + opciones: [ + { letra: "A", texto: "Productos que solo pueden usar las familias ricas", correcta: false }, + { letra: "B", texto: "Bienes no rivales y no excluibles proporcionados por el Estado", correcta: true }, + { letra: "C", texto: "Productos importados de otros países", correcta: false }, + { letra: "D", texto: "Bienes de lujo que produce el sector privado", correcta: false }, + ], + explicacion: "Los bienes públicos son aquellos que son no rivales (el uso por una persona no impide el uso por otra) y no excluibles (no se puede impedir que alguien los use), como la defensa nacional o los parques públicos.", + categoria: "Bienes Públicos" + }, + { + id: 6, + pregunta: "¿Cuál es la relación entre empresas y familias en el mercado de factores?", + opciones: [ + { letra: "A", texto: "Las empresas ofrecen trabajo y las familias lo demandan", correcta: false }, + { letra: "B", texto: "Las familias ofrecen factores productivos y las empresas los demandan", correcta: true }, + { letra: "C", texto: "El Estado controla ambos lados del mercado", correcta: false }, + { letra: "D", texto: "No hay relación entre ellos", correcta: false }, + ], + explicacion: "En el mercado de factores, las familias son los oferentes (proveen trabajo, tierra, capital) y las empresas son los demandantes de estos factores productivos.", + categoria: "Mercado de Factores" + }, + { + id: 7, + pregunta: "¿Qué papel juega el Estado en la redistribución del ingreso?", + opciones: [ + { letra: "A", texto: "No interviene en la distribución del ingreso", correcta: false }, + { letra: "B", texto: "Recauda impuestos y proporciona transferencias y servicios sociales", correcta: true }, + { letra: "C", texto: "Solo cobra impuestos a las empresas", correcta: false }, + { letra: "D", texto: "Fija los salarios de todos los trabajadores", correcta: false }, + ], + explicacion: "El Estado redistribuye el ingreso mediante el cobro de impuestos (generalmente progresivos) y el gasto en transferencias, subsidios, educación, salud y otros servicios públicos.", + categoria: "Redistribución" + }, + { + id: 8, + pregunta: "¿Cuál es un ejemplo de empresa estatal?", + opciones: [ + { letra: "A", texto: "Una tienda de ropa privada", correcta: false }, + { letra: "B", texto: "Una empresa petrolera nacional", correcta: true }, + { letra: "C", texto: "Un restaurante familiar", correcta: false }, + { letra: "D", texto: "Una empresa tecnológica multinacional", correcta: false }, + ], + explicacion: "Las empresas petroleras nacionales (como PEMEX, Petrobras, PDVSA) son ejemplos clásicos de empresas estatales, propiedad del gobierno.", + categoria: "Empresas Estatales" + } +]; + +export const AgentesEconomicosQuiz: React.FC = () => { + const [preguntaActual, setPreguntaActual] = useState(0); + const [respuestas, setRespuestas] = useState<{ [key: number]: string }>({}); + const [mostrarResultado, setMostrarResultado] = useState(false); + const [quizTerminado, setQuizTerminado] = useState(false); + + const seleccionarRespuesta = (letra: string) => { + setRespuestas({ ...respuestas, [preguntas[preguntaActual].id]: letra }); + setMostrarResultado(true); + }; + + const siguientePregunta = () => { + if (preguntaActual < preguntas.length - 1) { + setPreguntaActual(preguntaActual + 1); + setMostrarResultado(false); + } else { + setQuizTerminado(true); + } + }; + + const anteriorPregunta = () => { + if (preguntaActual > 0) { + setPreguntaActual(preguntaActual - 1); + setMostrarResultado(true); + } + }; + + const reiniciarQuiz = () => { + setPreguntaActual(0); + setRespuestas({}); + setMostrarResultado(false); + setQuizTerminado(false); + }; + + const calcularPuntuacion = () => { + let correctas = 0; + preguntas.forEach(pregunta => { + const opcionCorrecta = pregunta.opciones.find(o => o.correcta); + if (opcionCorrecta && respuestas[pregunta.id] === opcionCorrecta.letra) { + correctas++; + } + }); + return correctas; + }; + + if (quizTerminado) { + const puntuacion = calcularPuntuacion(); + const porcentaje = (puntuacion / preguntas.length) * 100; + + return ( +
+

Resultados del Quiz

+ +
+

+ {puntuacion} / {preguntas.length} +

+

{porcentaje.toFixed(0)}% de aciertos

+

+ {porcentaje >= 80 + ? '🎉 ¡Excelente! Dominas los agentes económicos' + : porcentaje >= 60 + ? '👍 ¡Bien! Puedes mejorar un poco más' + : '📚 Sigue estudiando los agentes económicos'} +

+
+ +
+ {preguntas.map((pregunta, index) => { + const respuestaUsuario = respuestas[pregunta.id]; + const opcionCorrecta = pregunta.opciones.find(o => o.correcta); + const esCorrecta = respuestaUsuario === opcionCorrecta?.letra; + + return ( +
+

{index + 1}. {pregunta.pregunta}

+

+ Tu respuesta: + {respuestaUsuario || 'Sin respuesta'} + + {!esCorrecta && ( + + Correcta: {opcionCorrecta?.letra} + + )} +

+
+ ); + })} +
+ + +
+ ); + } + + const pregunta = preguntas[preguntaActual]; + const respuestaSeleccionada = respuestas[pregunta.id]; + + return ( +
+

Quiz: Agentes Económicos

+ +
+ + Pregunta {preguntaActual + 1} de {preguntas.length} + + + Categoría: {pregunta.categoria} + +
+ +
+

{pregunta.pregunta}

+ +
+ {pregunta.opciones.map((opcion) => { + const estaSeleccionada = respuestaSeleccionada === opcion.letra; + const mostrarCorrecta = mostrarResultado && opcion.correcta; + const mostrarIncorrecta = mostrarResultado && estaSeleccionada && !opcion.correcta; + + return ( + + ); + })} +
+ + {mostrarResultado && ( +
+

Explicación:

+

{pregunta.explicacion}

+
+ )} +
+ +
+ + + +
+ +
+ {preguntas.map((_, index) => ( +
+ ))} +
+
+ ); +}; + +export default AgentesEconomicosQuiz; diff --git a/frontend/src/components/exercises/modulo1/CasosPaises.tsx b/frontend/src/components/exercises/modulo1/CasosPaises.tsx new file mode 100644 index 0000000..9984101 --- /dev/null +++ b/frontend/src/components/exercises/modulo1/CasosPaises.tsx @@ -0,0 +1,305 @@ +import { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Card } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { CheckCircle, XCircle, Globe, TrendingUp, Building2, Scale } from 'lucide-react'; + +interface EjercicioProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +type SistemaTipo = 'mercado' | 'planificado' | 'mixto'; + +interface Pais { + id: string; + nombre: string; + emoji: string; + descripcion: string; + caracteristicas: string[]; + sistemaCorrecto: SistemaTipo; + explicacion: string; +} + +const SISTEMAS: Record = { + mercado: { + nombre: 'Economía de Mercado', + color: 'bg-blue-500', + icono: + }, + planificado: { + nombre: 'Economía Planificada', + color: 'bg-red-500', + icono: + }, + mixto: { + nombre: 'Economía Mixta', + color: 'bg-green-500', + icono: + } +}; + +const PAISES: Pais[] = [ + { + id: 'singapur', + nombre: 'Singapur', + emoji: '🇸🇬', + descripcion: 'Centro financiero asiático con uno de los índices de libertad económica más altos del mundo.', + caracteristicas: [ + 'Libre comercio y bajos aranceles', + 'Impuestos corporativos bajos', + 'Mínima intervención estatal en negocios', + 'Sector privado altamente competitivo' + ], + sistemaCorrecto: 'mercado', + explicacion: 'Singapur es un ejemplo clásico de economía de mercado, con mínima regulación, bajos impuestos y gran libertad para la empresa privada.' + }, + { + id: 'noruega', + nombre: 'Noruega', + emoji: '🇳🇴', + descripcion: 'País escandinavo con altos estándares de vida y fuerte sector petrolero estatal.', + caracteristicas: [ + 'Servicios públicos universales gratuitos', + 'Fuerte sistema de bienestar social', + 'Empresas privadas con regulación estatal', + 'Fondo soberano de petróleo gestionado por el Estado' + ], + sistemaCorrecto: 'mixto', + explicacion: 'Noruega combina economía de mercado con fuerte intervención estatal en bienestar social y sectores estratégicos.' + }, + { + id: 'cuba', + nombre: 'Cuba', + emoji: '🇨🇺', + descripcion: 'Isla caribeña con sistema económico único en el hemisferio occidental.', + caracteristicas: [ + 'Mayoría de empresas son estatales', + 'Planificación centralizada', + 'Racionamiento de bienes básicos', + 'Recientemente ha permitido pequeñas empresas privadas' + ], + sistemaCorrecto: 'planificado', + explicacion: 'Cuba mantiene principalmente una economía planificada donde el Estado controla la mayoría de los medios de producción.' + }, + { + id: 'suiza', + nombre: 'Suiza', + emoji: '🇨🇭', + descripcion: 'País alpino conocido por su estabilidad económica y sistema bancario.', + caracteristicas: [ + 'Política fiscal conservadora', + 'Fuerte protección de la propiedad privada', + 'Mercado laboral flexible', + 'Alta competitividad internacional' + ], + sistemaCorrecto: 'mercado', + explicacion: 'Suiza opera principalmente como economía de mercado con fuerte protección a la propiedad privada y libre empresa.' + }, + { + id: 'francia', + nombre: 'Francia', + emoji: '🇫🇷', + descripcion: 'Potencia europea con tradición de intervención estatal en la economía.', + caracteristicas: [ + 'Altos impuestos para financiar servicios públicos', + 'Regulación extensa del mercado laboral', + 'Empresas privadas dominantes pero reguladas', + 'Sistema de salud público universal' + ], + sistemaCorrecto: 'mixto', + explicacion: 'Francia representa una economía mixta europea donde coexisten empresas privadas con fuerte regulación y servicios públicos extensos.' + }, + { + id: 'corea-norte', + nombre: 'Corea del Norte', + emoji: '🇰🇵', + descripcion: 'País asiático con uno de los sistemas económicos más cerrados del mundo.', + caracteristicas: [ + 'Planificación económica centralizada (Juche)', + 'Propiedad estatal total de medios de producción', + 'Comercio internacional severamente restringido', + 'Distribución de bienes por el Estado' + ], + sistemaCorrecto: 'planificado', + explicacion: 'Corea del Norte mantiene una economía altamente centralizada y planificada con mínima actividad de mercado permitida.' + }, + { + id: 'hong-kong', + nombre: 'Hong Kong', + emoji: '🇭🇰', + descripcion: 'Región administrativa especial de China con sistema económico único.', + caracteristicas: [ + 'Política de "un país, dos sistemas"', + 'Libertad económica y financiera', + 'Bajos impuestos y aranceles', + 'Mínima intervención gubernamental' + ], + sistemaCorrecto: 'mercado', + explicacion: 'Hong Kong históricamente ha operado como economía de mercado con mínima regulación y máxima libertad comercial.' + }, + { + id: 'alemania', + nombre: 'Alemania', + emoji: '🇩🇪', + descripcion: 'Mayor economía de Europa con modelo social de mercado.', + caracteristicas: [ + 'Economía social de mercado', + 'Codeterminación (trabajadores en consejos)', + 'Fuerte sector industrial privado', + 'Extensas redes de protección social' + ], + sistemaCorrecto: 'mixto', + explicacion: 'Alemania practica el modelo de "economía social de mercado", combinando mercado libre con fuerte estado de bienestar.' + } +]; + +export function CasosPaises({ ejercicioId: _ejercicioId, onComplete }: EjercicioProps) { + const [paisActual, setPaisActual] = useState(0); + const [respuestas, setRespuestas] = useState>({}); + const [mostrarResultado, setMostrarResultado] = useState(false); + const [completado, setCompletado] = useState(false); + + const pais = PAISES[paisActual]; + const esUltima = paisActual === PAISES.length - 1; + + const handleRespuesta = (sistema: SistemaTipo) => { + setRespuestas(prev => ({ ...prev, [pais.id]: sistema })); + setMostrarResultado(true); + }; + + const handleSiguiente = () => { + if (esUltima) { + const correctas = PAISES.filter(p => respuestas[p.id] === p.sistemaCorrecto).length; + const puntuacion = Math.round((correctas / PAISES.length) * 100); + setCompletado(true); + onComplete?.(puntuacion); + } else { + setPaisActual(prev => prev + 1); + setMostrarResultado(false); + } + }; + + const esCorrecta = respuestas[pais.id] === pais.sistemaCorrecto; + + if (completado) { + const correctas = PAISES.filter(p => respuestas[p.id] === p.sistemaCorrecto).length; + const puntuacion = Math.round((correctas / PAISES.length) * 100); + + return ( + + +
+ +
+
+ +

¡Ejercicio Completado!

+

+ Identificaste correctamente {correctas} de {PAISES.length} países +

+ +
{puntuacion}
+

puntos

+
+ ); + } + + return ( + +
+
+
+ País {paisActual + 1} de {PAISES.length} + {Math.round((paisActual / PAISES.length) * 100)}% +
+
+ +
+
+ + + +
+ {pais.emoji} +

{pais.nombre}

+

{pais.descripcion}

+
+ +
+

Características económicas:

+
    + {pais.caracteristicas.map((caracteristica, index) => ( +
  • + + {caracteristica} +
  • + ))} +
+
+ + {!mostrarResultado ? ( +
+

¿Qué sistema económico predomina?

+ {(Object.keys(SISTEMAS) as SistemaTipo[]).map((sistema) => ( + handleRespuesta(sistema)} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + className="w-full p-4 flex items-center gap-4 border-2 border-gray-200 rounded-xl hover:border-blue-400 hover:bg-blue-50 transition-all" + > +
+ {SISTEMAS[sistema].icono} +
+ {SISTEMAS[sistema].nombre} +
+ ))} +
+ ) : ( + +
+ {esCorrecta ? ( + + ) : ( + + )} + + {esCorrecta ? '¡Correcto!' : 'Incorrecto'} + +
+

+ Respuesta correcta: {SISTEMAS[pais.sistemaCorrecto].nombre} +

+

{pais.explicacion}

+ + +
+ )} +
+
+
+
+ ); +} + +export default CasosPaises; diff --git a/frontend/src/components/exercises/modulo1/ComparativaSistemas.tsx b/frontend/src/components/exercises/modulo1/ComparativaSistemas.tsx new file mode 100644 index 0000000..4597c30 --- /dev/null +++ b/frontend/src/components/exercises/modulo1/ComparativaSistemas.tsx @@ -0,0 +1,334 @@ +import { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Card } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { CheckCircle, XCircle, ArrowRight, ArrowLeft } from 'lucide-react'; + +interface EjercicioProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Categoria { + id: string; + nombre: string; + color: string; +} + +interface Casilla { + id: string; + categoria: string; + sistema: 'mercado' | 'planificado' | 'mixto' | null; + opciones: string[]; + correcta: string; +} + +const CATEGORIAS: Categoria[] = [ + { id: 'propiedad', nombre: 'Propiedad de medios de producción', color: 'bg-blue-100' }, + { id: 'precios', nombre: 'Fijación de precios', color: 'bg-green-100' }, + { id: 'competencia', nombre: 'Competencia', color: 'bg-purple-100' }, + { id: 'objetivo', nombre: 'Objetivo principal', color: 'bg-orange-100' }, + { id: 'planificacion', nombre: 'Planificación económica', color: 'bg-pink-100' }, + { id: 'bienestar', nombre: 'Bienestar social', color: 'bg-teal-100' } +]; + +const SISTEMAS = [ + { id: 'mercado', nombre: 'Economía de Mercado', color: 'bg-blue-500' }, + { id: 'planificado', nombre: 'Economía Planificada', color: 'bg-red-500' }, + { id: 'mixto', nombre: 'Economía Mixta', color: 'bg-green-500' } +]; + +const CASILLAS: Casilla[] = [ + { + id: 'propiedad-mercado', + categoria: 'propiedad', + sistema: 'mercado', + opciones: ['Privada', 'Estatal', 'Mixta'], + correcta: 'Privada' + }, + { + id: 'propiedad-planificado', + categoria: 'propiedad', + sistema: 'planificado', + opciones: ['Privada', 'Estatal', 'Mixta'], + correcta: 'Estatal' + }, + { + id: 'propiedad-mixto', + categoria: 'propiedad', + sistema: 'mixto', + opciones: ['Privada', 'Estatal', 'Mixta'], + correcta: 'Mixta' + }, + { + id: 'precios-mercado', + categoria: 'precios', + sistema: 'mercado', + opciones: ['Oferta y demanda', 'Estado', 'Combinación'], + correcta: 'Oferta y demanda' + }, + { + id: 'precios-planificado', + categoria: 'precios', + sistema: 'planificado', + opciones: ['Oferta y demanda', 'Estado', 'Combinación'], + correcta: 'Estado' + }, + { + id: 'precios-mixto', + categoria: 'precios', + sistema: 'mixto', + opciones: ['Oferta y demanda', 'Estado', 'Combinación'], + correcta: 'Combinación' + }, + { + id: 'competencia-mercado', + categoria: 'competencia', + sistema: 'mercado', + opciones: ['Libre', 'No existe', 'Regulada'], + correcta: 'Libre' + }, + { + id: 'competencia-planificado', + categoria: 'competencia', + sistema: 'planificado', + opciones: ['Libre', 'No existe', 'Regulada'], + correcta: 'No existe' + }, + { + id: 'competencia-mixto', + categoria: 'competencia', + sistema: 'mixto', + opciones: ['Libre', 'No existe', 'Regulada'], + correcta: 'Regulada' + }, + { + id: 'objetivo-mercado', + categoria: 'objetivo', + sistema: 'mercado', + opciones: ['Beneficio', 'Igualdad', 'Equilibrio'], + correcta: 'Beneficio' + }, + { + id: 'objetivo-planificado', + categoria: 'objetivo', + sistema: 'planificado', + opciones: ['Beneficio', 'Igualdad', 'Equilibrio'], + correcta: 'Igualdad' + }, + { + id: 'objetivo-mixto', + categoria: 'objetivo', + sistema: 'mixto', + opciones: ['Beneficio', 'Igualdad', 'Equilibrio'], + correcta: 'Equilibrio' + }, + { + id: 'planificacion-mercado', + categoria: 'planificacion', + sistema: 'mercado', + opciones: ['Descentralizada', 'Centralizada', 'Mixta'], + correcta: 'Descentralizada' + }, + { + id: 'planificacion-planificado', + categoria: 'planificacion', + sistema: 'planificado', + opciones: ['Descentralizada', 'Centralizada', 'Mixta'], + correcta: 'Centralizada' + }, + { + id: 'planificacion-mixto', + categoria: 'planificacion', + sistema: 'mixto', + opciones: ['Descentralizada', 'Centralizada', 'Mixta'], + correcta: 'Mixta' + }, + { + id: 'bienestar-mercado', + categoria: 'bienestar', + sistema: 'mercado', + opciones: ['Privado', 'Estatal', 'Combinado'], + correcta: 'Privado' + }, + { + id: 'bienestar-planificado', + categoria: 'bienestar', + sistema: 'planificado', + opciones: ['Privado', 'Estatal', 'Combinado'], + correcta: 'Estatal' + }, + { + id: 'bienestar-mixto', + categoria: 'bienestar', + sistema: 'mixto', + opciones: ['Privado', 'Estatal', 'Combinado'], + correcta: 'Combinado' + } +]; + +export function ComparativaSistemas({ ejercicioId: _ejercicioId, onComplete }: EjercicioProps) { + const [respuestas, setRespuestas] = useState>({}); + const [casillaActual, setCasillaActual] = useState(0); + const [completado, setCompletado] = useState(false); + const [mostrarResultado, setMostrarResultado] = useState(false); + + const casilla = CASILLAS[casillaActual]; + const esUltima = casillaActual === CASILLAS.length - 1; + + const handleRespuesta = (respuesta: string) => { + setRespuestas(prev => ({ ...prev, [casilla.id]: respuesta })); + setMostrarResultado(true); + }; + + const handleSiguiente = () => { + if (esUltima) { + const correctas = CASILLAS.filter(c => respuestas[c.id] === c.correcta).length; + const puntuacion = Math.round((correctas / CASILLAS.length) * 100); + setCompletado(true); + onComplete?.(puntuacion); + } else { + setCasillaActual(prev => prev + 1); + setMostrarResultado(false); + } + }; + + const handleAnterior = () => { + if (casillaActual > 0) { + setCasillaActual(prev => prev - 1); + setMostrarResultado(false); + } + }; + + const esCorrecta = respuestas[casilla.id] === casilla.correcta; + const categoria = CATEGORIAS.find(c => c.id === casilla.categoria); + const sistema = SISTEMAS.find(s => s.id === casilla.sistema); + const yaRespondida = respuestas[casilla.id] !== undefined; + + if (completado) { + const correctas = CASILLAS.filter(c => respuestas[c.id] === c.correcta).length; + const puntuacion = Math.round((correctas / CASILLAS.length) * 100); + + return ( + + +
+ +
+
+ +

¡Ejercicio Completado!

+

+ Completaste {correctas} de {CASILLAS.length} casillas correctamente +

+ +
{puntuacion}
+

puntos

+
+ ); + } + + return ( + +
+
+
+ Casilla {casillaActual + 1} de {CASILLAS.length} + {Math.round((Object.keys(respuestas).length / CASILLAS.length) * 100)}% completado +
+
+ +
+
+ +
+
+

Categoría

+

{categoria?.nombre}

+
+
+

Sistema Económico

+

{sistema?.nombre}

+
+
+

Progreso

+

{Object.keys(respuestas).length}/{CASILLAS.length}

+
+
+ + + {!mostrarResultado ? ( + +

+ ¿Cómo se caracteriza esta dimensión en {sistema?.nombre}? +

+
+ {casilla.opciones.map((opcion) => ( + handleRespuesta(opcion)} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + className="p-4 text-left border-2 border-gray-200 rounded-xl hover:border-blue-400 hover:bg-blue-50 transition-all" + > + {opcion} + + ))} +
+
+ ) : ( + +
+ {esCorrecta ? ( + + ) : ( + + )} + + {esCorrecta ? '¡Correcto!' : 'Incorrecto'} + +
+

+ {esCorrecta + ? `Correcto. En ${sistema?.nombre}, la ${categoria?.nombre.toLowerCase()} es ${casilla.correcta.toLowerCase()}.` + : `La respuesta correcta es: ${casilla.correcta}. En ${sistema?.nombre}, la ${categoria?.nombre.toLowerCase()} se caracteriza por ser ${casilla.correcta.toLowerCase()}.` + } +

+
+ + +
+
+ )} +
+
+
+ ); +} + +export default ComparativaSistemas; diff --git a/frontend/src/components/exercises/modulo1/CostoOportunidadCalculator.tsx b/frontend/src/components/exercises/modulo1/CostoOportunidadCalculator.tsx new file mode 100644 index 0000000..56f25b7 --- /dev/null +++ b/frontend/src/components/exercises/modulo1/CostoOportunidadCalculator.tsx @@ -0,0 +1,157 @@ +import React, { useState } from 'react'; + +interface OpcionProduccion { + bienesA: number; + bienesB: number; +} + +const datosFPP: OpcionProduccion[] = [ + { bienesA: 0, bienesB: 100 }, + { bienesA: 20, bienesB: 90 }, + { bienesA: 40, bienesB: 75 }, + { bienesA: 60, bienesB: 55 }, + { bienesA: 80, bienesB: 30 }, + { bienesA: 100, bienesB: 0 }, +]; + +export const CostoOportunidadCalculator: React.FC = () => { + const [puntoInicial, setPuntoInicial] = useState(0); + const [puntoFinal, setPuntoFinal] = useState(1); + const [respuestaUsuario, setRespuestaUsuario] = useState(''); + const [resultado, setResultado] = useState<{ + correcto: boolean; + mensaje: string; + costoReal: number; + } | null>(null); + + const calcularCostoOportunidad = (inicio: number, fin: number): number => { + const opcionInicio = datosFPP[inicio]; + const opcionFin = datosFPP[fin]; + + const cambioBienB = opcionFin.bienesB - opcionInicio.bienesB; + const cambioBienA = opcionFin.bienesA - opcionInicio.bienesA; + + if (cambioBienA === 0) return 0; + return Math.abs(cambioBienB / cambioBienA); + }; + + const verificarRespuesta = () => { + const costoReal = calcularCostoOportunidad(puntoInicial, puntoFinal); + const respuestaNum = parseFloat(respuestaUsuario); + + if (isNaN(respuestaNum)) { + setResultado({ + correcto: false, + mensaje: 'Por favor ingresa un número válido', + costoReal: costoReal + }); + return; + } + + const margenError = 0.5; + const correcto = Math.abs(respuestaNum - costoReal) <= margenError; + + setResultado({ + correcto, + mensaje: correcto + ? '¡Correcto! Has calculado bien el costo de oportunidad.' + : 'Incorrecto. Revisa tu cálculo.', + costoReal: costoReal + }); + }; + + const generarNuevoEjercicio = () => { + const nuevoInicio = Math.floor(Math.random() * (datosFPP.length - 1)); + const nuevoFin = nuevoInicio + 1 + Math.floor(Math.random() * (datosFPP.length - nuevoInicio - 1)); + + setPuntoInicial(nuevoInicio); + setPuntoFinal(nuevoFin); + setRespuestaUsuario(''); + setResultado(null); + }; + + return ( +
+

Calculadora de Costo de Oportunidad

+ +
+

Tabla de Posibilidades de Producción:

+ + + + + + + + + + {datosFPP.map((opcion, index) => ( + + + + + + ))} + +
OpciónBien ABien B
{index + 1}{opcion.bienesA}{opcion.bienesB}
+
+ +
+

Ejercicio:

+

+ Si la economía se mueve de la Opción {puntoInicial + 1} a la + Opción {puntoFinal + 1}, ¿cuál es el costo de oportunidad + de producir una unidad adicional del Bien A? +

+ +
+ + setRespuestaUsuario(e.target.value)} + className="border p-2 rounded w-32" + placeholder="Ej: 0.75" + /> + unidades del Bien B +
+ +
+ + +
+
+ + {resultado && ( +
+

{resultado.mensaje}

+ {!resultado.correcto && ( +

+ El costo de oportunidad correcto es: {resultado.costoReal.toFixed(2)} unidades del Bien B +

+ )} +
+ )} + +
+

Fórmula:

+

+ Costo de Oportunidad = |Cambio en Bien B| / |Cambio en Bien A| +

+
+
+ ); +}; + +export default CostoOportunidadCalculator; diff --git a/frontend/src/components/exercises/modulo1/CostoOportunidadCotidiano.tsx b/frontend/src/components/exercises/modulo1/CostoOportunidadCotidiano.tsx new file mode 100644 index 0000000..b6c4df1 --- /dev/null +++ b/frontend/src/components/exercises/modulo1/CostoOportunidadCotidiano.tsx @@ -0,0 +1,246 @@ +import { useState } from 'react'; + +interface CostoOportunidadCotidianoProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Situacion { + id: number; + titulo: string; + descripcion: string; + decision: string; + opciones: string[]; + costoOportunidadCorrecto: string; + explicacion: string; +} + +const situaciones: Situacion[] = [ + { + id: 1, + titulo: "Tiempo libre", + descripcion: "Tienes 3 horas libres un sábado por la tarde.", + decision: "Decides estudiar para un examen importante.", + opciones: [ + "El tiempo que podrías haber pasado con amigos", + "Las calificaciones del examen", + "El dinero ahorrado", + "La comida que no comiste" + ], + costoOportunidadCorrecto: "El tiempo que podrías haber pasado con amigos", + explicacion: "El costo de oportunidad es lo que sacrificas: el tiempo con amigos que elegiste no hacer." + }, + { + id: 2, + titulo: "Compra de tecnología", + descripcion: "Tienes $1,000 ahorrados.", + decision: "Compras una laptop nueva para trabajar.", + opciones: [ + "El dinero que gastaste", + "El dinero que podrías haber invertido", + "La laptop misma", + "Las especificaciones técnicas" + ], + costoOportunidadCorrecto: "El dinero que podrías haber invertido", + explicacion: "Al gastar en la laptop, sacrificas la oportunidad de invertir ese dinero y obtener rendimientos." + }, + { + id: 3, + titulo: "Carrera profesional", + descripcion: "Terminas la universidad con dos ofertas de trabajo.", + decision: "Aceptas el trabajo en una startup con menor salario inicial.", + opciones: [ + "El salario más alto de la otra oferta", + "La experiencia en la startup", + "Tu título universitario", + "El tiempo de búsqueda" + ], + costoOportunidadCorrecto: "El salario más alto de la otra oferta", + explicacion: "Al elegir la startup, renuncias al salario más alto que ofrecía la otra empresa." + }, + { + id: 4, + titulo: "Vacaciones", + descripcion: "Tienes dos semanas de vacaciones este verano.", + decision: "Viajas a Europa en lugar de quedarte trabajando.", + opciones: [ + "Las fotos que tomarás", + "El dinero que gastarás en el viaje", + "El dinero que podrías haber ganado trabajando", + "La experiencia cultural" + ], + costoOportunidadCorrecto: "El dinero que podrías haber ganado trabajando", + explicacion: "El costo de oportunidad incluye los ingresos que sacrificas al no trabajar esas semanas." + } +]; + +export function CostoOportunidadCotidiano({ ejercicioId: _ejercicioId, onComplete }: CostoOportunidadCotidianoProps) { + const [respuestas, setRespuestas] = useState<{[key: number]: string}>({}); + const [mostrarExplicacion, setMostrarExplicacion] = useState<{[key: number]: boolean}>({}); + const [completado, setCompletado] = useState(false); + + const handleSeleccion = (situacionId: number, opcion: string) => { + setRespuestas(prev => ({ ...prev, [situacionId]: opcion })); + }; + + const handleValidar = () => { + const nuevasExplicaciones: {[key: number]: boolean} = {}; + let correctas = 0; + + situaciones.forEach(situacion => { + nuevasExplicaciones[situacion.id] = true; + if (respuestas[situacion.id] === situacion.costoOportunidadCorrecto) { + correctas++; + } + }); + + setMostrarExplicacion(nuevasExplicaciones); + + if (correctas === situaciones.length && !completado) { + setCompletado(true); + if (onComplete) { + onComplete(100); + } + } + }; + + const handleReset = () => { + setRespuestas({}); + setMostrarExplicacion({}); + setCompletado(false); + }; + + const correctas = situaciones.filter(s => respuestas[s.id] === s.costoOportunidadCorrecto).length; + + return ( +
+
+

Costo de Oportunidad en Decisiones Cotidianas

+

+ Identifica el costo de oportunidad en cada situación de la vida real. +

+
+ +
+

+ Recuerda: El costo de oportunidad es el valor de la mejor alternativa a la que renuncias + al tomar una decisión. No es lo que gastas, sino lo que sacrificas. +

+
+ +
+ {situaciones.map((situacion, index) => ( +
+
+
+ {index + 1} +
+
+

{situacion.titulo}

+

{situacion.descripcion}

+

Decisión: {situacion.decision}

+ +
+

+ ¿Cuál es el costo de oportunidad? +

+
+ {situacion.opciones.map((opcion) => { + const isSelected = respuestas[situacion.id] === opcion; + const isCorrect = opcion === situacion.costoOportunidadCorrecto; + const showResult = mostrarExplicacion[situacion.id]; + + let buttonClass = 'border border-gray-300 bg-white text-gray-700 hover:bg-gray-50'; + + if (showResult) { + if (isCorrect) { + buttonClass = 'border-green-500 bg-green-50 text-green-800'; + } else if (isSelected && !isCorrect) { + buttonClass = 'border-red-500 bg-red-50 text-red-800'; + } else { + buttonClass = 'border-gray-200 bg-gray-50 text-gray-400'; + } + } else if (isSelected) { + buttonClass = 'border-blue-500 bg-blue-50 text-blue-800'; + } + + return ( + + ); + })} +
+
+ + {mostrarExplicacion[situacion.id] && ( +
+

+ {respuestas[situacion.id] === situacion.costoOportunidadCorrecto + ? '¡Correcto!' + : 'Respuesta correcta:'} +

+

+ {situacion.explicacion} +

+
+ )} +
+
+
+ ))} +
+ +
+ + +
+ + {completado && ( +
+

¡Excelente comprensión!

+

100 puntos

+

+ Has identificado correctamente todos los costos de oportunidad. +

+
+ )} +
+ ); +} + +export default CostoOportunidadCotidiano; diff --git a/frontend/src/components/exercises/modulo1/CrecimientoEconomicoFPP.tsx b/frontend/src/components/exercises/modulo1/CrecimientoEconomicoFPP.tsx new file mode 100644 index 0000000..d17aee5 --- /dev/null +++ b/frontend/src/components/exercises/modulo1/CrecimientoEconomicoFPP.tsx @@ -0,0 +1,205 @@ +import React, { useState } from 'react'; + +interface PuntoFPP { + x: number; + y: number; +} + +export const CrecimientoEconomicoFPP: React.FC = () => { + const [tipoCambio, setTipoCambio] = useState(''); + const [factorSeleccionado, setFactorSeleccionado] = useState(''); + const [respuestasCorrectas, setRespuestasCorrectas] = useState([false, false]); + const [mostrarResultado, setMostrarResultado] = useState(false); + + const puntosFPPOriginal: PuntoFPP[] = [ + { x: 0, y: 100 }, + { x: 50, y: 90 }, + { x: 100, y: 70 }, + { x: 150, y: 40 }, + { x: 200, y: 0 }, + ]; + + const puntosFPPDesplazada: PuntoFPP[] = [ + { x: 0, y: 120 }, + { x: 60, y: 108 }, + { x: 120, y: 84 }, + { x: 180, y: 48 }, + { x: 240, y: 0 }, + ]; + + const opcionesCambio = [ + { valor: 'crecimiento', label: 'Crecimiento económico (desplazamiento hacia afuera)' }, + { valor: 'recesion', label: 'Recesión económica (desplazamiento hacia adentro)' }, + { valor: 'mejoraA', label: 'Mejora tecnológica solo en Bien A' }, + { valor: 'mejoraB', label: 'Mejora tecnológica solo en Bien B' }, + ]; + + const opcionesFactores = [ + { valor: 'tecnologia', label: 'Avance tecnológico', tipo: 'crecimiento' }, + { valor: 'capital', label: 'Aumento del capital físico', tipo: 'crecimiento' }, + { valor: 'trabajo', label: 'Aumento de la fuerza laboral', tipo: 'crecimiento' }, + { valor: 'recursos', label: 'Descubrimiento de nuevos recursos', tipo: 'crecimiento' }, + { valor: 'guerra', label: 'Conflicto bélico', tipo: 'recesion' }, + { valor: 'desastre', label: 'Desastre natural', tipo: 'recesion' }, + { valor: 'emigracion', label: 'Emigración masiva', tipo: 'recesion' }, + { valor: 'destruccion', label: 'Destrucción de capital', tipo: 'recesion' }, + ]; + + const verificarRespuestas = () => { + const esCrecimiento = tipoCambio === 'crecimiento'; + const factorEsCrecimiento = ['tecnologia', 'capital', 'trabajo', 'recursos'].includes(factorSeleccionado); + + setRespuestasCorrectas([esCrecimiento, factorEsCrecimiento]); + setMostrarResultado(true); + }; + + const reiniciarEjercicio = () => { + setTipoCambio(''); + setFactorSeleccionado(''); + setRespuestasCorrectas([false, false]); + setMostrarResultado(false); + }; + + const SVG_HEIGHT = 300; + const SVG_WIDTH = 400; + const PADDING = 40; + + const escalarX = (x: number) => PADDING + (x / 250) * (SVG_WIDTH - 2 * PADDING); + const escalarY = (y: number) => SVG_HEIGHT - PADDING - (y / 130) * (SVG_HEIGHT - 2 * PADDING); + + const crearPath = (puntos: PuntoFPP[]) => { + return puntos.map((p, i) => + `${i === 0 ? 'M' : 'L'} ${escalarX(p.x)} ${escalarY(p.y)}` + ).join(' '); + }; + + return ( +
+

Crecimiento Económico y Curva FPP

+ +
+
+

Gráfico de la Frontera de Posibilidades de Producción

+ + + {/* Ejes */} + + + + {/* Etiquetas de ejes */} + Bien A + Bien B + + {/* FPP Original */} + + + {/* FPP Desplazada */} + + + {/* Leyenda */} + + FPP Original + + + FPP Nueva + +
+ +
+
+

Pregunta 1: ¿Qué tipo de cambio observas en el gráfico?

+
+ {opcionesCambio.map((opcion) => ( + + ))} +
+
+ +
+

Pregunta 2: ¿Qué factor podría causar este cambio?

+ +
+
+
+ +
+ + +
+ + {mostrarResultado && ( +
+
+

+ Pregunta 1: {respuestasCorrectas[0] ? '¡Correcto!' : 'Incorrecto.'} +

+

+ {respuestasCorrectas[0] + ? 'El gráfico muestra un crecimiento económico, representado por el desplazamiento hacia afuera de la curva FPP.' + : 'El gráfico muestra crecimiento económico (desplazamiento hacia afuera de la FPP).'} +

+
+ +
+

+ Pregunta 2: {respuestasCorrectas[1] ? '¡Correcto!' : 'Incorrecto.'} +

+

+ {respuestasCorrectas[1] + ? 'Excelente selección. Este factor contribuye al crecimiento económico.' + : 'Revisa tu selección. Considera qué factores aumentan la capacidad productiva de la economía.'} +

+
+
+ )} + +
+

Conceptos clave:

+
    +
  • Crecimiento económico: Desplazamiento de la FPP hacia afuera, permite producir más de ambos bienes
  • +
  • Recesión: Desplazamiento de la FPP hacia adentro, reduce la capacidad productiva
  • +
  • Factores del crecimiento: Tecnología, capital, trabajo, recursos naturales
  • +
+
+
+ ); +}; + +export default CrecimientoEconomicoFPP; diff --git a/frontend/src/components/exercises/modulo1/DefinicionEconomiaQuiz.tsx b/frontend/src/components/exercises/modulo1/DefinicionEconomiaQuiz.tsx new file mode 100644 index 0000000..b91681b --- /dev/null +++ b/frontend/src/components/exercises/modulo1/DefinicionEconomiaQuiz.tsx @@ -0,0 +1,197 @@ +import { useState } from 'react'; +import { motion } from 'framer-motion'; +import { Card } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { CheckCircle, XCircle } from 'lucide-react'; + +interface EjercicioProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +const PREGUNTAS = [ + { + id: 1, + pregunta: "¿Qué es la economía?", + opciones: [ + "Ciencia social que estudia cómo se asignan recursos escasos para satisfacer necesidades ilimitadas", + "Estudio exclusivo del dinero y los bancos", + "Análisis únicamente de empresas grandes", + "Gestión de presupuestos familiares" + ], + correcta: 0, + explicacion: "La economía es una ciencia social que estudia la asignación de recursos escasos para satisfacer necesidades ilimitadas." + }, + { + id: 2, + pregunta: "¿Cuál es la diferencia entre microeconomía y macroeconomía?", + opciones: [ + "La micro estudia individuos y empresas; la macro estudia la economía como un todo", + "La micro es más difícil que la macro", + "La micro estudia solo bancos; la macro estudia gobiernos", + "No hay diferencia, son lo mismo" + ], + correcta: 0, + explicacion: "La microeconomía estudia el comportamiento de individuos y empresas, mientras que la macroeconomía analiza la economía en su conjunto (PIB, inflación, desempleo)." + }, + { + id: 3, + pregunta: "¿Qué es el problema económico fundamental?", + opciones: [ + "La escasez de recursos frente a necesidades ilimitadas", + "La falta de dinero en los bancos", + "El desempleo elevado", + "La inflación alta" + ], + correcta: 0, + explicacion: "El problema económico fundamental es la escasez: los recursos son limitados pero las necesidades humanas son ilimitadas." + }, + { + id: 4, + pregunta: "¿Qué estudia la economía positiva?", + opciones: [ + "Lo que es (hechos y descripciones)", + "Lo que debería ser (valores y juicios)", + "Solo matemáticas económicas", + "Únicamente historia económica" + ], + correcta: 0, + explicacion: "La economía positiva describe y explica hechos objetivos ('lo que es'), sin hacer juicios de valor." + }, + { + id: 5, + pregunta: "Complete: La economía normativa se refiere a...", + opciones: [ + "Juicios de valor sobre lo que debería ser", + "Datos estadísticos objetivos", + "Teorías matemáticas puras", + "Hechos históricos verificables" + ], + correcta: 0, + explicacion: "La economía normativa hace juicios de valor y prescripciones sobre lo que debería ser ('deberíamos aumentar los impuestos')." + } +]; + +export function DefinicionEconomiaQuiz({ ejercicioId: _ejercicioId, onComplete }: EjercicioProps) { + const [preguntaActual, setPreguntaActual] = useState(0); + const [respuestas, setRespuestas] = useState([]); + const [mostrarResultado, setMostrarResultado] = useState(false); + const [completado, setCompletado] = useState(false); + + const pregunta = PREGUNTAS[preguntaActual]; + const esUltima = preguntaActual === PREGUNTAS.length - 1; + + const handleRespuesta = (index: number) => { + const nuevasRespuestas = [...respuestas, index]; + setRespuestas(nuevasRespuestas); + + if (esUltima) { + // Calcular puntuación + const correctas = nuevasRespuestas.filter((r, i) => r === PREGUNTAS[i].correcta).length; + const puntuacion = Math.round((correctas / PREGUNTAS.length) * 100); + setCompletado(true); + onComplete?.(puntuacion); + } else { + setMostrarResultado(true); + } + }; + + const handleSiguiente = () => { + setPreguntaActual(prev => prev + 1); + setMostrarResultado(false); + }; + + const esCorrecta = respuestas[preguntaActual] === pregunta.correcta; + + if (completado) { + const correctas = respuestas.filter((r, i) => r === PREGUNTAS[i].correcta).length; + const puntuacion = Math.round((correctas / PREGUNTAS.length) * 100); + + return ( + + +
+ +
+
+ +

¡Quiz Completado!

+

+ Respondiste {correctas} de {PREGUNTAS.length} preguntas correctamente +

+ +
{puntuacion}
+

puntos

+
+ ); + } + + return ( + +
+ {/* Progress */} +
+
+ Pregunta {preguntaActual + 1} de {PREGUNTAS.length} + {Math.round(((preguntaActual) / PREGUNTAS.length) * 100)}% +
+
+ +
+
+ + {/* Pregunta */} +

{pregunta.pregunta}

+ + {/* Opciones */} + {!mostrarResultado ? ( +
+ {pregunta.opciones.map((opcion, index) => ( + handleRespuesta(index)} + whileHover={{ scale: 1.01 }} + whileTap={{ scale: 0.99 }} + className="w-full p-4 text-left border-2 border-gray-200 rounded-xl hover:border-blue-400 hover:bg-blue-50 transition-all" + > + {String.fromCharCode(65 + index)}. {opcion} + + ))} +
+ ) : ( + +
+ {esCorrecta ? ( + + ) : ( + + )} + + {esCorrecta ? '¡Correcto!' : 'Incorrecto'} + +
+

{pregunta.explicacion}

+ + +
+ )} +
+
+ ); +} + +export default DefinicionEconomiaQuiz; diff --git a/frontend/src/components/exercises/modulo1/EconomiaPositivaVsNormativa.tsx b/frontend/src/components/exercises/modulo1/EconomiaPositivaVsNormativa.tsx new file mode 100644 index 0000000..9965f04 --- /dev/null +++ b/frontend/src/components/exercises/modulo1/EconomiaPositivaVsNormativa.tsx @@ -0,0 +1,275 @@ +import { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Card } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { CheckCircle, XCircle, RefreshCw, ArrowRight } from 'lucide-react'; + +interface EjercicioProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Enunciado { + id: number; + texto: string; + tipo: 'positiva' | 'normativa'; + explicacion: string; +} + +const ENUNCIADOS: Enunciado[] = [ + { + id: 1, + texto: "La inflación en el país alcanzó el 5% el año pasado.", + tipo: 'positiva', + explicacion: "Este es un enunciado positivo porque describe un hecho objetivo y verificable." + }, + { + id: 2, + texto: "El gobierno debería reducir los impuestos para estimular la economía.", + tipo: 'normativa', + explicacion: "Este es un enunciado normativo porque expresa una opinión sobre lo que debería hacerse." + }, + { + id: 3, + texto: "La tasa de desempleo juvenil es del 15%.", + tipo: 'positiva', + explicacion: "Es positivo porque presenta un dato estadístico verificable." + }, + { + id: 4, + texto: "Es injusto que existan grandes diferencias de ingreso entre ricos y pobres.", + tipo: 'normativa', + explicacion: "Es normativo porque contiene un juicio de valor sobre la justicia." + }, + { + id: 5, + texto: "El PIB del país creció un 3% durante el último trimestre.", + tipo: 'positiva', + explicacion: "Es positivo porque es una afirmación factual basada en datos." + }, + { + id: 6, + texto: "Se debería aumentar el salario mínimo para mejorar la calidad de vida.", + tipo: 'normativa', + explicacion: "Es normativo porque prescribe una acción basada en valores." + }, + { + id: 7, + texto: "El costo de vida en la capital es 20% más alto que en el interior.", + tipo: 'positiva', + explicacion: "Es positivo porque compara datos observables y mensurables." + }, + { + id: 8, + texto: "Las empresas multinacionales tienen la obligación ética de pagar impuestos justos.", + tipo: 'normativa', + explicacion: "Es normativo porque habla de obligaciones éticas y valores." + } +]; + +export function EconomiaPositivaVsNormativa({ ejercicioId: _ejercicioId, onComplete }: EjercicioProps) { + const [enunciadosRestantes, setEnunciadosRestantes] = useState([...ENUNCIADOS]); + const [clasificaciones, setClasificaciones] = useState<{id: number, correcta: boolean}[]>([]); + const [mostrarResultado, setMostrarResultado] = useState(false); + const [completado, setCompletado] = useState(false); + const [ultimaRespuesta, setUltimaRespuesta] = useState<'positiva' | 'normativa' | null>(null); + + const enunciadoActual = enunciadosRestantes[0]; + const progreso = ((ENUNCIADOS.length - enunciadosRestantes.length) / ENUNCIADOS.length) * 100; + + const handleClasificacion = (tipo: 'positiva' | 'normativa') => { + if (!enunciadoActual) return; + + const esCorrecta = tipo === enunciadoActual.tipo; + setUltimaRespuesta(tipo); + setClasificaciones(prev => [...prev, { id: enunciadoActual.id, correcta: esCorrecta }]); + setMostrarResultado(true); + + if (enunciadosRestantes.length === 1) { + const nuevasClasificaciones = [...clasificaciones, { id: enunciadoActual.id, correcta: esCorrecta }]; + const correctas = nuevasClasificaciones.filter(c => c.correcta).length; + const puntuacion = Math.round((correctas / ENUNCIADOS.length) * 100); + setCompletado(true); + onComplete?.(puntuacion); + } + }; + + const handleSiguiente = () => { + setEnunciadosRestantes(prev => prev.slice(1)); + setMostrarResultado(false); + setUltimaRespuesta(null); + }; + + const handleReiniciar = () => { + setEnunciadosRestantes([...ENUNCIADOS]); + setClasificaciones([]); + setMostrarResultado(false); + setCompletado(false); + setUltimaRespuesta(null); + }; + + if (completado) { + const correctas = clasificaciones.filter(c => c.correcta).length; + const puntuacion = Math.round((correctas / ENUNCIADOS.length) * 100); + + return ( + + +
= 70 ? 'bg-green-100' : 'bg-yellow-100'}`}> + {puntuacion >= 70 ? ( + + ) : ( + + )} +
+
+ +

+ {puntuacion >= 70 ? '¡Excelente trabajo!' : '¡Sigue practicando!'} +

+

+ Clasificaste correctamente {correctas} de {ENUNCIADOS.length} enunciados +

+ +
{puntuacion}
+

puntos

+ + {puntuacion < 70 && ( + + )} +
+ ); + } + + if (!enunciadoActual) return null; + + const esCorrecta = ultimaRespuesta === enunciadoActual.tipo; + + return ( + +
+ {/* Header */} +
+
+

Clasifica el enunciado

+

¿Es una afirmación positiva o normativa?

+
+ + {ENUNCIADOS.length - enunciadosRestantes.length + 1} / {ENUNCIADOS.length} + +
+ + {/* Progress */} +
+
+ +
+
+ + {/* Enunciado */} + + {!mostrarResultado ? ( + +
+

"{enunciadoActual.texto}"

+
+ +
+ handleClasificacion('positiva')} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + className="p-4 border-2 border-blue-200 rounded-xl hover:border-blue-400 hover:bg-blue-50 transition-all text-center" + > +
Economía Positiva
+

Describe hechos objetivos

+
+ + handleClasificacion('normativa')} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + className="p-4 border-2 border-purple-200 rounded-xl hover:border-purple-400 hover:bg-purple-50 transition-all text-center" + > +
Economía Normativa
+

Expresa juicios de valor

+
+
+
+ ) : ( + +
+ {esCorrecta ? ( + + ) : ( + + )} + + {esCorrecta ? '¡Correcto!' : 'Incorrecto'} + +
+ +
+

Respuesta correcta:

+

+ Economía {enunciadoActual.tipo === 'positiva' ? 'Positiva' : 'Normativa'} +

+
+ +

{enunciadoActual.explicacion}

+ + +
+ )} +
+ + {/* Legend */} +
+
+
+
+
+

Positiva

+

Lo que es (hechos)

+
+
+
+
+
+

Normativa

+

Lo que debería ser (valores)

+
+
+
+
+
+
+ ); +} + +export default EconomiaPositivaVsNormativa; diff --git a/frontend/src/components/exercises/modulo1/EscasezSimulator.tsx b/frontend/src/components/exercises/modulo1/EscasezSimulator.tsx new file mode 100644 index 0000000..dd0732f --- /dev/null +++ b/frontend/src/components/exercises/modulo1/EscasezSimulator.tsx @@ -0,0 +1,189 @@ +import { useState } from 'react'; +import { motion } from 'framer-motion'; +import { Card } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { CheckCircle, AlertCircle } from 'lucide-react'; + +interface EjercicioProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +const NECESIDADES = [ + { id: 'alimentacion', nombre: 'Alimentación', icono: '🍽️' }, + { id: 'vivienda', nombre: 'Vivienda', icono: '🏠' }, + { id: 'educacion', nombre: 'Educación', icono: '📚' }, + { id: 'salud', nombre: 'Salud', icono: '🏥' } +]; + +export function EscasezSimulator({ ejercicioId: _ejercicioId, onComplete }: EjercicioProps) { + const [asignaciones, setAsignaciones] = useState>({ + alimentacion: 25, + vivienda: 25, + educacion: 25, + salud: 25 + }); + const [validado, setValidado] = useState(false); + const [completado, setCompletado] = useState(false); + + const total = Object.values(asignaciones).reduce((sum, val) => sum + val, 0); + const restante = 100 - total; + const excedido = total > 100; + + const handleSliderChange = (id: string, value: number) => { + setAsignaciones(prev => ({ ...prev, [id]: value })); + setValidado(false); + }; + + const handleValidar = () => { + if (excedido) return; + + setValidado(true); + + // Calcular puntuación basada en equilibrio + // Ideal: todas las necesidades tienen al menos 15 puntos y no se excede + const valores = Object.values(asignaciones); + const todasConMinimo = valores.every(v => v >= 15); + const sumaExacta = total === 100; + + let puntuacion = 0; + if (sumaExacta) { + puntuacion = 60; // Base por usar exactamente 100 + if (todasConMinimo) puntuacion += 40; // Bonus por equilibrio + } else if (total <= 100) { + puntuacion = Math.round((total / 100) * 50); // Proporcional si no usa todo + } + + setTimeout(() => { + setCompletado(true); + onComplete?.(puntuacion); + }, 1500); + }; + + if (completado) { + return ( + + +
+ +
+
+ +

¡Simulación Completada!

+

+ Has distribuido los recursos disponibles. +

+ +
+

Distribución final:

+
+ {NECESIDADES.map(nec => ( +
+ {nec.icono} {nec.nombre}: + {asignaciones[nec.id]} pts +
+ ))} +
+
+
+ ); + } + + return ( + +
+
+

Simulador de Escasez

+

+ Tienes 100 puntos para distribuir entre 4 necesidades básicas. +

+
+ + {/* Indicador de recursos */} +
+
+ + {excedido ? '¡Excedido!' : `Restante: ${restante} pts`} + + + {total} / 100 + +
+
+ +
+ {excedido && ( +

+ + Has excedido los 100 puntos disponibles. Reduce alguna asignación. +

+ )} +
+ + {/* Sliders */} +
+ {NECESIDADES.map(necesidad => ( +
+
+
+ {necesidad.icono} + {necesidad.nombre} +
+ + {asignaciones[necesidad.id]} pts + +
+ handleSliderChange(necesidad.id, parseInt(e.target.value))} + className="w-full h-2 bg-gray-300 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+ 0 + 50 +
+
+ ))} +
+ + {/* Botón validar */} + + + {validado && !excedido && ( + +

+ {total === 100 + ? '¡Excelente! Has utilizado todos los recursos disponibles.' + : `Has utilizado ${total} de 100 puntos. ¿Quieres ajustar o continuar?`} +

+
+ )} +
+
+ ); +} + +export default EscasezSimulator; diff --git a/frontend/src/components/exercises/modulo1/FPPAnalizador.tsx b/frontend/src/components/exercises/modulo1/FPPAnalizador.tsx new file mode 100644 index 0000000..0221034 --- /dev/null +++ b/frontend/src/components/exercises/modulo1/FPPAnalizador.tsx @@ -0,0 +1,478 @@ +import { useState, useCallback, DragEvent } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Card } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { + CheckCircle, + XCircle, + RefreshCcw, + Link2, + GripVertical, + Trophy, + Scale, + Target, + Zap, + BookOpen +} from 'lucide-react'; + +interface MatchingItem { + id: string; + content: string; +} + +interface Match { + leftId: string; + rightId: string; + isCorrect?: boolean; + checked?: boolean; +} + +interface FPPAnalizadorProps { + onComplete?: (score: number, total: number) => void; +} + +const PUNTOS_INICIALES = [ + { x: 15, y: 85, tipo: 'ineficiente', label: 'A' }, + { x: 45, y: 55, tipo: 'eficiente', label: 'B' }, + { x: 75, y: 25, tipo: 'inalcanzable', label: 'C' }, + { x: 30, y: 70, tipo: 'ineficiente', label: 'D' }, + { x: 60, y: 40, tipo: 'eficiente', label: 'E' }, + { x: 90, y: 10, tipo: 'inalcanzable', label: 'F' }, +]; + +const TIPOS_OPCIONES = [ + { id: 'eficiente', label: 'Eficiente', color: 'green', icon: CheckCircle, descripcion: 'En la FPP - máxima producción' }, + { id: 'ineficiente', label: 'Ineficiente', color: 'orange', icon: Zap, descripcion: 'Dentro de la FPP - recursos subutilizados' }, + { id: 'inalcanzable', label: 'Inalcanzable', color: 'red', icon: XCircle, descripcion: 'Fuera de la FPP - no hay recursos suficientes' }, +]; + +export function FPPAnalizador({ onComplete }: FPPAnalizadorProps) { + const [asignaciones, setAsignaciones] = useState>(() => + PUNTOS_INICIALES.reduce((acc, punto) => ({ ...acc, [punto.label]: null }), {}) + ); + const [mostrarResultados, setMostrarResultados] = useState(false); + const [draggedTipo, setDraggedTipo] = useState(null); + + const handleDragStart = (e: DragEvent, tipoId: string) => { + e.dataTransfer.setData('text/plain', tipoId); + setDraggedTipo(tipoId); + }; + + const handleDragEnd = () => { + setDraggedTipo(null); + }; + + const handleDrop = (e: DragEvent, puntoLabel: string) => { + e.preventDefault(); + const tipoId = e.dataTransfer.getData('text/plain'); + if (tipoId) { + setAsignaciones(prev => ({ ...prev, [puntoLabel]: tipoId })); + } + setDraggedTipo(null); + }; + + const handleDragOver = (e: DragEvent) => { + e.preventDefault(); + }; + + const handleAsignar = (puntoLabel: string, tipoId: string) => { + setAsignaciones(prev => ({ ...prev, [puntoLabel]: tipoId })); + }; + + const handleVerificar = () => { + setMostrarResultados(true); + const correctas = PUNTOS_INICIALES.filter( + punto => asignaciones[punto.label] === punto.tipo + ).length; + if (onComplete) { + onComplete(correctas, PUNTOS_INICIALES.length); + } + }; + + const handleReiniciar = () => { + setAsignaciones(PUNTOS_INICIALES.reduce((acc, punto) => ({ ...acc, [punto.label]: null }), {})); + setMostrarResultados(false); + }; + + const todasAsignadas = Object.values(asignaciones).every(a => a !== null); + const correctas = PUNTOS_INICIALES.filter( + punto => asignaciones[punto.label] === punto.tipo + ).length; + + // Generar curva FPP + const generateFPPPath = () => { + const puntos = [ + { x: 10, y: 90 }, + { x: 20, y: 85 }, + { x: 30, y: 78 }, + { x: 40, y: 70 }, + { x: 50, y: 60 }, + { x: 60, y: 50 }, + { x: 70, y: 40 }, + { x: 80, y: 30 }, + { x: 90, y: 20 }, + ]; + + let path = `M ${puntos[0].x} ${puntos[0].y}`; + for (let i = 1; i < puntos.length; i++) { + const cp1x = puntos[i - 1].x + (puntos[i].x - puntos[i - 1].x) * 0.5; + const cp1y = puntos[i - 1].y; + const cp2x = puntos[i - 1].x + (puntos[i].x - puntos[i - 1].x) * 0.5; + const cp2y = puntos[i].y; + path += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${puntos[i].x} ${puntos[i].y}`; + } + return path; + }; + + const getEstiloPunto = (punto: typeof PUNTOS_INICIALES[0]) => { + const asignacion = asignaciones[punto.label]; + if (!mostrarResultados) { + return { + fill: asignacion ? { + eficiente: '#22c55e', + ineficiente: '#f97316', + inalcanzable: '#ef4444', + }[asignacion] : '#3b82f6', + stroke: '#1e40af', + strokeWidth: 2, + }; + } + + const esCorrecto = asignacion === punto.tipo; + return { + fill: esCorrecto ? '#22c55e' : '#ef4444', + stroke: esCorrecto ? '#166534' : '#991b1b', + strokeWidth: 3, + }; + }; + + return ( +
+ {/* Header */} +
+
+

Analizador de la FPP

+

+ Identifica si cada punto es Eficiente, Ineficiente o Inalcanzable +

+
+
+ + 100 pts +
+
+ + {/* Leyenda */} + +
+ +

Arrastra el tipo a cada punto:

+
+
+ {TIPOS_OPCIONES.map((tipo) => { + const Icon = tipo.icon; + return ( + handleDragStart(e as unknown as DragEvent, tipo.id)} + onDragEnd={handleDragEnd} + whileHover={!mostrarResultados ? { scale: 1.02 } : {}} + whileTap={!mostrarResultados ? { scale: 0.98 } : {}} + className={`p-3 rounded-lg border-2 cursor-grab active:cursor-grabbing ${ + tipo.color === 'green' ? 'border-green-200 bg-green-50' : + tipo.color === 'orange' ? 'border-orange-200 bg-orange-50' : + 'border-red-200 bg-red-50' + } ${draggedTipo === tipo.id ? 'opacity-50' : ''}`} + > +
+ +
+ + {tipo.label} + +

{tipo.descripcion}

+
+
+
+ ); + })} +
+
+ + {/* SVG con FPP */} + +
+ + {/* Fondo con gradientes */} + + + + + + + + + + + + {/* Áreas */} + + + + {/* Ejes */} + + + + {/* Flechas de ejes */} + + + + {/* Etiquetas de ejes */} + + Bien de Consumo (Y) + + + Bien de Capital (X) + + + {/* Curva FPP */} + + + {/* Puntos interactivos */} + {PUNTOS_INICIALES.map((punto) => { + const estilo = getEstiloPunto(punto); + const asignacion = asignaciones[punto.label]; + + return ( + handleDrop(e as unknown as DragEvent, punto.label)} + className="cursor-pointer" + > + {/* Círculo del punto */} + + + {/* Label del punto */} + + {punto.label} + + + {/* Indicador de asignación */} + {asignacion && !mostrarResultados && ( + + {TIPOS_OPCIONES.find(t => t.id === asignacion)?.label} + + )} + + {/* Checkmark o X si hay resultado */} + {mostrarResultados && ( + + {asignacion === punto.tipo ? ( + + ) : ( + + )} + + )} + + ); + })} + + {/* Leyenda en el SVG */} + + + Leyenda: + + FPP + + Factible + + Inalcanzable + + +
+ + {/* Botones de asignación alternativos (para móvil) */} +
+ {PUNTOS_INICIALES.map((punto) => ( +
+
+
+ Punto {punto.label} +
+ {!mostrarResultados && ( + + )} + {mostrarResultados && ( +
+ {asignaciones[punto.label] === punto.tipo ? '✓ Correcto' : '✗ Incorrecto'} +
+ )} +
+ ))} +
+ + + {/* Resultados */} + + {mostrarResultados && ( + + +
+ + + + +

+ {correctas === PUNTOS_INICIALES.length + ? '¡Perfecto!' + : correctas >= PUNTOS_INICIALES.length * 0.7 + ? '¡Muy bien!' + : '¡Sigue practicando!'} +

+ +

+ {correctas} de {PUNTOS_INICIALES.length} puntos clasificados correctamente +

+ +
+
+ +

{correctas}

+

Correctos

+
+
+ +

{PUNTOS_INICIALES.length - correctas}

+

Incorrectos

+
+
+ +

+ {Math.round((correctas / PUNTOS_INICIALES.length) * 100)}% +

+

Precisión

+
+
+
+
+
+ )} +
+ + {/* Botones de acción */} +
+ + + {!mostrarResultados ? ( + + ) : ( + + )} +
+ + {/* Instrucciones */} + {!todasAsignadas && !mostrarResultados && ( +
+

+ Arrastra los tipos hacia los puntos en el gráfico o usa los selectores debajo. + Faltan + {PUNTOS_INICIALES.length - Object.values(asignaciones).filter(Boolean).length} + puntos por clasificar. +

+
+ )} +
+ ); +} + +export default FPPAnalizador; diff --git a/frontend/src/components/exercises/modulo1/FPPConstructor.tsx b/frontend/src/components/exercises/modulo1/FPPConstructor.tsx new file mode 100644 index 0000000..8832d07 --- /dev/null +++ b/frontend/src/components/exercises/modulo1/FPPConstructor.tsx @@ -0,0 +1,554 @@ +import { useState, useRef, useCallback } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Card } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { + CheckCircle, + XCircle, + RefreshCcw, + Trophy, + Move, + Info, + Target, + TrendingUp, + Trash2 +} from 'lucide-react'; + +interface PuntoFPP { + id: string; + x: number; + y: number; +} + +interface FPPConstructorProps { + onComplete?: (score: number, total: number) => void; +} + +const ESCENARIOS = [ + { + id: 1, + titulo: 'Economía de Bienes', + descripcion: 'Un país produce solo dos bienes: Alimentos y Tecnología.', + objetivo: 'Construye una FPP convexa que muestre la creciente escasez de recursos.', + puntosRequeridos: 5, + tipo: 'convexa', + maxX: 100, + maxY: 100, + }, + { + id: 2, + titulo: 'Especialización Laboral', + descripcion: 'Dos trabajadores pueden producir manzanas o naranjas con costos de oportunidad constantes.', + objetivo: 'Construye una FPP lineal que refleje costos de oportunidad constantes.', + puntosRequeridos: 4, + tipo: 'lineal', + maxX: 100, + maxY: 100, + }, + { + id: 3, + titulo: 'Economía con Recursos Especializados', + descripcion: 'Algunos recursos son mejores para producir un bien que otro.', + objetivo: 'Construye una FPP cóncava que muestre ventajas de especialización.', + puntosRequeridos: 5, + tipo: 'concava', + maxX: 100, + maxY: 100, + }, +]; + +export function FPPConstructor({ onComplete }: FPPConstructorProps) { + const [escenarioActual, setEscenarioActual] = useState(0); + const [puntos, setPuntos] = useState([]); + const [puntoArrastrado, setPuntoArrastrado] = useState(null); + const [mostrarResultados, setMostrarResultados] = useState(false); + const svgRef = useRef(null); + + const escenario = ESCENARIOS[escenarioActual]; + + const handleSvgClick = (e: React.MouseEvent) => { + if (mostrarResultados || puntoArrastrado) return; + + const svg = svgRef.current; + if (!svg) return; + + const rect = svg.getBoundingClientRect(); + const x = ((e.clientX - rect.left) / rect.width) * 100; + const y = ((e.clientY - rect.top) / rect.height) * 100; + + // Limitar dentro del área del gráfico + const limitedX = Math.max(5, Math.min(95, x)); + const limitedY = Math.max(5, Math.min(95, y)); + + const nuevoPunto: PuntoFPP = { + id: `punto-${Date.now()}`, + x: limitedX, + y: limitedY, + }; + + setPuntos(prev => [...prev, nuevoPunto]); + }; + + const handleMouseDown = (e: React.MouseEvent, puntoId: string) => { + e.stopPropagation(); + if (mostrarResultados) return; + setPuntoArrastrado(puntoId); + }; + + const handleMouseMove = useCallback((e: React.MouseEvent) => { + if (!puntoArrastrado || !svgRef.current) return; + + const svg = svgRef.current; + const rect = svg.getBoundingClientRect(); + const x = ((e.clientX - rect.left) / rect.width) * 100; + const y = ((e.clientY - rect.top) / rect.height) * 100; + + const limitedX = Math.max(5, Math.min(95, x)); + const limitedY = Math.max(5, Math.min(95, y)); + + setPuntos(prev => + prev.map(p => + p.id === puntoArrastrado ? { ...p, x: limitedX, y: limitedY } : p + ) + ); + }, [puntoArrastrado]); + + const handleMouseUp = () => { + setPuntoArrastrado(null); + }; + + const handleEliminarPunto = (puntoId: string) => { + if (mostrarResultados) return; + setPuntos(prev => prev.filter(p => p.id !== puntoId)); + }; + + const handleReiniciar = () => { + setPuntos([]); + setMostrarResultados(false); + setPuntoArrastrado(null); + }; + + const handleVerificar = () => { + setMostrarResultados(true); + + // Calcular puntuación basada en el tipo de FPP + let correctas = 0; + const puntosOrdenados = [...puntos].sort((a, b) => a.x - b.x); + + if (escenario.tipo === 'lineal') { + // Para FPP lineal, verificar que los puntos formen aproximadamente una línea recta + if (puntosOrdenados.length >= 2) { + const pendiente = (puntosOrdenados[0].y - puntosOrdenados[puntosOrdenados.length - 1].y) / + (puntosOrdenados[puntosOrdenados.length - 1].x - puntosOrdenados[0].x); + + correctas = puntosOrdenados.every((p, i) => { + if (i === 0) return true; + const xExpected = puntosOrdenados[0].x + (i / (puntosOrdenados.length - 1)) * + (puntosOrdenados[puntosOrdenados.length - 1].x - puntosOrdenados[0].x); + const yExpected = puntosOrdenados[0].y - pendiente * (xExpected - puntosOrdenados[0].x); + return Math.abs(p.x - xExpected) < 15 && Math.abs(p.y - yExpected) < 15; + }) ? escenario.puntosRequeridos : Math.floor(escenario.puntosRequeridos * 0.6); + } + } else if (escenario.tipo === 'convexa') { + // Para FPP convexa (creciente escasez), verificar curvatura hacia arriba + if (puntosOrdenados.length >= 3) { + let esConvexa = true; + for (let i = 1; i < puntosOrdenados.length - 1; i++) { + const pendiente1 = (puntosOrdenados[i].y - puntosOrdenados[i-1].y) / + (puntosOrdenados[i].x - puntosOrdenados[i-1].x || 0.1); + const pendiente2 = (puntosOrdenados[i+1].y - puntosOrdenados[i].y) / + (puntosOrdenados[i+1].x - puntosOrdenados[i].x || 0.1); + if (pendiente2 < pendiente1 * 0.5) esConvexa = false; + } + correctas = esConvexa && puntosOrdenados.length >= escenario.puntosRequeridos + ? escenario.puntosRequeridos + : Math.max(0, puntosOrdenados.length - 1); + } + } else if (escenario.tipo === 'concava') { + // Para FPP cóncava, verificar curvatura hacia abajo + if (puntosOrdenados.length >= 3) { + let esConcava = true; + for (let i = 1; i < puntosOrdenados.length - 1; i++) { + const pendiente1 = (puntosOrdenados[i].y - puntosOrdenados[i-1].y) / + (puntosOrdenados[i].x - puntosOrdenados[i-1].x || 0.1); + const pendiente2 = (puntosOrdenados[i+1].y - puntosOrdenados[i].y) / + (puntosOrdenados[i+1].x - puntosOrdenados[i].x || 0.1); + if (pendiente2 > pendiente1 * 1.5) esConcava = false; + } + correctas = esConcava && puntosOrdenados.length >= escenario.puntosRequeridos + ? escenario.puntosRequeridos + : Math.max(0, puntosOrdenados.length - 1); + } + } + + const puntosFinales = Math.min(correctas, escenario.puntosRequeridos); + if (onComplete) { + onComplete(puntosFinales, escenario.puntosRequeridos); + } + }; + + const handleSiguienteEscenario = () => { + if (escenarioActual < ESCENARIOS.length - 1) { + setEscenarioActual(prev => prev + 1); + setPuntos([]); + setMostrarResultados(false); + } + }; + + const handleAnteriorEscenario = () => { + if (escenarioActual > 0) { + setEscenarioActual(prev => prev - 1); + setPuntos([]); + setMostrarResultados(false); + } + }; + + // Generar path para la línea FPP + const generateFPPPath = () => { + if (puntos.length < 2) return ''; + + const sorted = [...puntos].sort((a, b) => a.x - b.x); + + // Si es lineal, usar líneas rectas + if (escenario.tipo === 'lineal') { + return sorted.reduce((path, p, i) => + i === 0 ? `M ${p.x} ${p.y}` : `${path} L ${p.x} ${p.y}`, '' + ); + } + + // Para convexa/cóncava, usar curvas suaves + let path = `M ${sorted[0].x} ${sorted[0].y}`; + for (let i = 1; i < sorted.length; i++) { + const prev = sorted[i - 1]; + const curr = sorted[i]; + const cpX1 = prev.x + (curr.x - prev.x) * 0.3; + const cpX2 = prev.x + (curr.x - prev.x) * 0.7; + path += ` C ${cpX1} ${prev.y}, ${cpX2} ${curr.y}, ${curr.x} ${curr.y}`; + } + return path; + }; + + const esCompletado = puntos.length >= escenario.puntosRequeridos; + + return ( +
+ {/* Header */} +
+
+

Constructor de FPP

+

+ Escenario {escenarioActual + 1} de {ESCENARIOS.length}: {escenario.titulo} +

+
+
+ + + {escenario.puntosRequeridos} pts mín. + +
+
+ + {/* Descripción del escenario */} + +
+ +
+

{escenario.descripcion}

+

+ Objetivo: {escenario.objetivo} +

+
+
+
+ + {/* Área de trabajo SVG */} + +
+
+ + + Haz clic para agregar puntos • Arrastra para mover • + {puntos.length} puntos + +
+ {puntos.length > 0 && ( + + )} +
+ +
+ + {/* Grid */} + + + + + + + + {/* Ejes */} + + + + {/* Flechas */} + + + + {/* Etiquetas */} + + Bien X + + + Bien Y + + + {/* Marcas de escala */} + {[0, 25, 50, 75, 100].map(val => ( + + + {val} + + {val} + + ))} + + {/* Línea FPP */} + {puntos.length >= 2 && ( + + )} + + {/* Puntos */} + {puntos.map((punto, index) => ( + + handleMouseDown(e, punto.id)} + /> + + P{index + 1} + + + {/* Botón eliminar */} + {!mostrarResultados && ( + { + e.stopPropagation(); + handleEliminarPunto(punto.id); + }} + > + + × + + )} + + ))} + + {/* Indicador de tipo de FPP */} + + + Tipo FPP: + + {escenario.tipo === 'lineal' ? 'Lineal (CCO constante)' : + escenario.tipo === 'convexa' ? 'Convexa (escasez creciente)' : + 'Cóncava (especialización)'} + + + +
+ + {/* Lista de puntos */} + {puntos.length > 0 && ( +
+

Coordenadas:

+
+ {[...puntos].sort((a, b) => a.x - b.x).map((punto, index) => ( +
+
P{index + 1}
+
X: {punto.x.toFixed(1)}
+
Y: {punto.y.toFixed(1)}
+
+ ))} +
+
+ )} +
+ + {/* Resultados */} + + {mostrarResultados && ( + + = escenario.puntosRequeridos + ? 'bg-gradient-to-br from-green-50 to-emerald-50 border-green-200' + : 'bg-gradient-to-br from-yellow-50 to-orange-50 border-yellow-200' + }`}> +
+ = escenario.puntosRequeridos + ? 'bg-gradient-to-br from-yellow-400 to-orange-500' + : 'bg-gradient-to-br from-yellow-400 to-orange-400' + }`} + > + {puntos.length >= escenario.puntosRequeridos ? ( + + ) : ( + + )} + + +

+ {puntos.length >= escenario.puntosRequeridos + ? '¡Excelente trabajo!' + : '¡Necesitas más puntos!'} +

+ +

+ {puntos.length >= escenario.puntosRequeridos + ? `Has construido una FPP ${escenario.tipo} correctamente con ${puntos.length} puntos.` + : `Agrega al menos ${escenario.puntosRequeridos - puntos.length} punto(s) más para completar el escenario.`} +

+ +
+
+

{puntos.length}

+

Puntos agregados

+
+
+

{escenario.puntosRequeridos}

+

Requeridos

+
+
+
+
+
+ )} +
+ + {/* Navegación y acciones */} +
+
+ + +
+ +
+ + + {!mostrarResultados ? ( + + ) : ( + + )} +
+
+ + {/* Instrucciones adicionales */} + {!esCompletado && !mostrarResultados && ( +
+

+ Faltan + {Math.max(0, escenario.puntosRequeridos - puntos.length)} + puntos para completar este escenario. +

+
+ )} + + {/* Guía de tipos de FPP */} + +

+ + Tipos de Frontera de Posibilidades +

+
+
+
Lineal
+

Costos de oportunidad constantes. Los recursos son perfectamente sustituibles entre bienes.

+
+
+
Convexa (hacia afuera)
+

Costos de oportunidad crecientes. Los recursos no son perfectamente adaptables.

+
+
+
Cóncava (hacia adentro)
+

Costos de oportunidad decrecientes. Especialización en bienes específicos.

+
+
+
+
+ ); +} + +export default FPPConstructor; diff --git a/frontend/src/components/exercises/modulo1/FactoresProduccionQuiz.tsx b/frontend/src/components/exercises/modulo1/FactoresProduccionQuiz.tsx new file mode 100644 index 0000000..ed140df --- /dev/null +++ b/frontend/src/components/exercises/modulo1/FactoresProduccionQuiz.tsx @@ -0,0 +1,414 @@ +import { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Card } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { CheckCircle, XCircle, Trophy, RotateCcw, ArrowRight, Mountain, Users, Factory, Lightbulb } from 'lucide-react'; + +interface FactoresProduccionQuizProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Pregunta { + id: number; + pregunta: string; + tipo: 'tierra' | 'trabajo' | 'capital' | 'emprendimiento'; + opciones: string[]; + respuestaCorrecta: number; + explicacion: string; +} + +const PREGUNTAS: Pregunta[] = [ + { + id: 1, + pregunta: '¿Cuál de los siguientes es un ejemplo de TIERRA como factor de producción?', + tipo: 'tierra', + opciones: [ + 'El trabajo de un obrero', + 'Un terreno agrícola', + 'Una máquina industrial', + 'La habilidad de un gerente' + ], + respuestaCorrecta: 1, + explicacion: 'La tierra incluye todos los recursos naturales: terrenos, minerales, agua, petróleo, etc. Es todo lo que nos proporciona la naturaleza sin transformar.' + }, + { + id: 2, + pregunta: 'El TRABAJO como factor de producción se refiere a:', + tipo: 'trabajo', + opciones: [ + 'Solo el esfuerzo físico', + 'Solo el esfuerzo mental', + 'El esfuerzo físico y mental que aportan las personas', + 'Las máquinas que reemplazan a los humanos' + ], + respuestaCorrecta: 2, + explicacion: 'El trabajo incluye tanto el esfuerzo físico (como el de un albañil) como el mental (como el de un ingeniero). Es el factor humano en la producción.' + }, + { + id: 3, + pregunta: '¿Qué se considera CAPITAL como factor de producción?', + tipo: 'capital', + opciones: [ + 'Dinero en una cuenta bancaria', + 'Acciones de una empresa', + 'Maquinaria, herramientas y equipos utilizados para producir', + 'Terrenos y edificios' + ], + respuestaCorrecta: 2, + explicacion: 'En economía, el capital físico (o capital real) son los bienes manufacturados utilizados para producir otros bienes: máquinas, herramientas, fábricas, etc. No es dinero.' + }, + { + id: 4, + pregunta: '¿Cuál es la recompensa que reciben los propietarios del factor TIERRA?', + tipo: 'tierra', + opciones: [ + 'Salarios', + 'Rentas o alquileres', + 'Intereses', + 'Beneficios' + ], + respuestaCorrecta: 1, + explicacion: 'Los propietarios de tierra reciben RENTAS (o alquileres) como pago por el uso de sus recursos naturales.' + }, + { + id: 5, + pregunta: 'Los trabajadores reciben _____ como recompensa por su factor de producción.', + tipo: 'trabajo', + opciones: [ + 'Intereses', + 'Rentas', + 'Salarios', + 'Dividendos' + ], + respuestaCorrecta: 2, + explicacion: 'El trabajo recibe SALARIOS (o sueldos) como compensación por el esfuerzo físico y mental aportado a la producción.' + }, + { + id: 6, + pregunta: '¿Qué reciben los propietarios de CAPITAL como recompensa?', + tipo: 'capital', + opciones: [ + 'Salarios', + 'Rentas', + 'Intereses', + 'Bonificaciones' + ], + respuestaCorrecta: 2, + explicacion: 'El capital recibe INTERESES como recompensa. Si prestas tu capital (maquinaria o dinero para comprarla), recibes intereses a cambio.' + }, + { + id: 7, + pregunta: 'El EMPRENDIMIENTO (o empresa) es el factor que:', + tipo: 'emprendimiento', + opciones: [ + 'Solo invierte dinero', + 'Combina los otros factores de producción asumiendo riesgos', + 'Trabaja en la fábrica', + 'Solo vende los productos' + ], + respuestaCorrecta: 1, + explicacion: 'El emprendimiento es el factor que organiza y combina tierra, trabajo y capital para producir bienes y servicios, asumiendo el riesgo del negocio.' + }, + { + id: 8, + pregunta: '¿Cuál es la recompensa del EMPRENDIMIENTO?', + tipo: 'emprendimiento', + opciones: [ + 'Salario fijo', + 'Intereses garantizados', + 'Beneficios (o pérdidas)', + 'Renta del terreno' + ], + respuestaCorrecta: 2, + explicacion: 'El emprendimiento recibe BENEFICIOS cuando la empresa tiene éxito, pero también puede sufrir PÉRDIDAS. Es el factor con mayor riesgo y potencial de ganancia.' + } +]; + +export function FactoresProduccionQuiz({ ejercicioId: _ejercicioId, onComplete }: FactoresProduccionQuizProps) { + const [preguntaActual, setPreguntaActual] = useState(0); + const [respuestaSeleccionada, setRespuestaSeleccionada] = useState(null); + const [mostrarResultado, setMostrarResultado] = useState(false); + const [puntuacion, setPuntuacion] = useState(0); + const [completado, setCompletado] = useState(false); + const [respuestasCorrectas, setRespuestasCorrectas] = useState(0); + + const pregunta = PREGUNTAS[preguntaActual]; + + const getTipoIcon = (tipo: string) => { + switch (tipo) { + case 'tierra': + return ; + case 'trabajo': + return ; + case 'capital': + return ; + case 'emprendimiento': + return ; + default: + return null; + } + }; + + const getTipoLabel = (tipo: string) => { + switch (tipo) { + case 'tierra': + return 'Tierra'; + case 'trabajo': + return 'Trabajo'; + case 'capital': + return 'Capital'; + case 'emprendimiento': + return 'Emprendimiento'; + default: + return ''; + } + }; + + const getTipoColor = (tipo: string) => { + switch (tipo) { + case 'tierra': + return 'bg-green-100 text-green-700 border-green-200'; + case 'trabajo': + return 'bg-blue-100 text-blue-700 border-blue-200'; + case 'capital': + return 'bg-amber-100 text-amber-700 border-amber-200'; + case 'emprendimiento': + return 'bg-purple-100 text-purple-700 border-purple-200'; + default: + return 'bg-gray-100 text-gray-700 border-gray-200'; + } + }; + + const handleSeleccionar = (index: number) => { + if (mostrarResultado) return; + setRespuestaSeleccionada(index); + }; + + const handleVerificar = () => { + if (respuestaSeleccionada === null) return; + + const esCorrecta = respuestaSeleccionada === pregunta.respuestaCorrecta; + setMostrarResultado(true); + + if (esCorrecta) { + setPuntuacion(prev => prev + Math.round(100 / PREGUNTAS.length)); + setRespuestasCorrectas(prev => prev + 1); + } + + if (preguntaActual === PREGUNTAS.length - 1) { + setTimeout(() => { + setCompletado(true); + const puntuacionFinal = puntuacion + (esCorrecta ? Math.round(100 / PREGUNTAS.length) : 0); + if (onComplete) { + onComplete(puntuacionFinal); + } + }, 2000); + } + }; + + const handleSiguiente = () => { + setPreguntaActual(prev => prev + 1); + setRespuestaSeleccionada(null); + setMostrarResultado(false); + }; + + const handleReiniciar = () => { + setPreguntaActual(0); + setRespuestaSeleccionada(null); + setMostrarResultado(false); + setPuntuacion(0); + setCompletado(false); + setRespuestasCorrectas(0); + }; + + if (completado) { + return ( + +
+ + + + +

+ ¡Quiz Completado! +

+ +

+ Has respondido {respuestasCorrectas} de {PREGUNTAS.length} preguntas correctamente +

+ +
+
+ +

Tierra

+

Rentas

+
+
+ +

Trabajo

+

Salarios

+
+
+ +

Capital

+

Intereses

+
+
+ +

Emprendimiento

+

Beneficios

+
+
+ +
+

Puntuación Total

+

{puntuacion}

+

puntos

+
+ + +
+
+ ); + } + + return ( + +
+
+
+

Factores de Producción

+

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

+
+
+

Puntos

+

{puntuacion}

+
+
+ +
+ +
+ +
+
+ {getTipoIcon(pregunta.tipo)} + {getTipoLabel(pregunta.tipo)} +
+

+ {pregunta.pregunta} +

+
+ +
+ {pregunta.opciones.map((opcion, index) => { + const estaSeleccionada = respuestaSeleccionada === index; + const esCorrecta = index === pregunta.respuestaCorrecta; + const mostrarCorrecta = mostrarResultado && esCorrecta; + const mostrarIncorrecta = mostrarResultado && estaSeleccionada && !esCorrecta; + + return ( + handleSeleccionar(index)} + disabled={mostrarResultado} + whileHover={!mostrarResultado ? { scale: 1.01 } : {}} + whileTap={!mostrarResultado ? { scale: 0.99 } : {}} + className={`w-full p-4 rounded-xl border-2 text-left transition-all ${ + mostrarCorrecta + ? 'border-green-500 bg-green-50' + : mostrarIncorrecta + ? 'border-red-500 bg-red-50' + : estaSeleccionada + ? 'border-blue-500 bg-blue-50' + : 'border-gray-200 bg-white hover:border-blue-300' + }`} + > +
+
+ {mostrarCorrecta && } + {mostrarIncorrecta && } + {!mostrarResultado && estaSeleccionada && ( +
+ )} +
+ + {opcion} + +
+ + ); + })} +
+ + + {mostrarResultado && ( + +

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

+

{pregunta.explicacion}

+
+ )} +
+ +
+ {!mostrarResultado ? ( + + ) : preguntaActual < PREGUNTAS.length - 1 ? ( + + ) : null} +
+
+ + ); +} + +export default FactoresProduccionQuiz; diff --git a/frontend/src/components/exercises/modulo1/FlujoCircular.tsx b/frontend/src/components/exercises/modulo1/FlujoCircular.tsx new file mode 100644 index 0000000..de19f8b --- /dev/null +++ b/frontend/src/components/exercises/modulo1/FlujoCircular.tsx @@ -0,0 +1,300 @@ +import { useState } from 'react'; +import { motion } from 'framer-motion'; +import { Card } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { CheckCircle, XCircle, Trophy, RotateCcw, ArrowRight } from 'lucide-react'; + +interface FlujoCircularProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Pregunta { + id: number; + pregunta: string; + opciones: string[]; + respuestaCorrecta: number; + explicacion: string; +} + +const PREGUNTAS: Pregunta[] = [ + { + id: 1, + pregunta: "¿Quiénes son los principales agentes económicos en el flujo circular?", + opciones: [ + "Solo el gobierno y las empresas", + "Familias y empresas", + "Bancos y familias", + "Empresas y extranjeros" + ], + respuestaCorrecta: 1, + explicacion: "Las familias (consumidores) y las empresas (productores) son los dos agentes principales en el modelo básico del flujo circular." + }, + { + id: 2, + pregunta: "Las familias ofrecen a las empresas:", + opciones: [ + "Productos terminados", + "Trabajo, tierra y capital (factores de producción)", + "Dinero para invertir", + "Servicios bancarios" + ], + respuestaCorrecta: 1, + explicacion: "Las familias son propietarias de los factores de producción (trabajo, tierra, capital) y los ofrecen a las empresas a cambio de ingresos (salarios, rentas, intereses)." + }, + { + id: 3, + pregunta: "Las empresas le venden a las familias:", + opciones: [ + "Acciones de la empresa", + "Bienes y servicios", + "Materias primas", + "Deudas" + ], + respuestaCorrecta: 1, + explicacion: "Las empresas producen bienes y servicios que venden a las familias en el mercado de bienes." + }, + { + id: 4, + pregunta: "En el flujo MONETARIO (de dinero), el dinero va:", + opciones: [ + "De empresas a familias (salarios) y de familias a empresas (gastos)", + "Solo de familias a empresas", + "Solo de empresas a familias", + "En círculo en una sola dirección" + ], + respuestaCorrecta: 0, + explicacion: "El flujo monetario es bidireccional: empresas pagan salarios/rentas a familias, y familias gastan dinero comprando bienes a las empresas." + }, + { + id: 5, + pregunta: "En el flujo REAL (de bienes/factores), ¿qué ofrecen las familias?", + opciones: [ + "Productos terminados", + "Factores de producción (trabajo, tierra, capital)", + "Dinero", + "Servicios financieros" + ], + respuestaCorrecta: 1, + explicacion: "En el flujo real, las familias ofrecen sus factores de producción (trabajo, tierra, capital) a las empresas." + } +]; + +export function FlujoCircular({ ejercicioId: _ejercicioId, onComplete }: FlujoCircularProps) { + const [preguntaActual, setPreguntaActual] = useState(0); + const [respuestaSeleccionada, setRespuestaSeleccionada] = useState(null); + const [mostrarResultado, setMostrarResultado] = useState(false); + const [puntuacion, setPuntuacion] = useState(0); + const [completado, setCompletado] = useState(false); + const [respuestasCorrectas, setRespuestasCorrectas] = useState(0); + + const pregunta = PREGUNTAS[preguntaActual]; + + const handleSeleccionar = (index: number) => { + if (mostrarResultado) return; + setRespuestaSeleccionada(index); + }; + + const handleVerificar = () => { + if (respuestaSeleccionada === null) return; + + const esCorrecta = respuestaSeleccionada === pregunta.respuestaCorrecta; + setMostrarResultado(true); + + if (esCorrecta) { + setPuntuacion(prev => prev + 20); + setRespuestasCorrectas(prev => prev + 1); + } + + // Si es la última pregunta + if (preguntaActual === PREGUNTAS.length - 1) { + setTimeout(() => { + setCompletado(true); + const puntuacionFinal = puntuacion + (esCorrecta ? 20 : 0); + if (onComplete) { + onComplete(puntuacionFinal); + } + }, 2000); + } + }; + + const handleSiguiente = () => { + setPreguntaActual(prev => prev + 1); + setRespuestaSeleccionada(null); + setMostrarResultado(false); + }; + + const handleReiniciar = () => { + setPreguntaActual(0); + setRespuestaSeleccionada(null); + setMostrarResultado(false); + setPuntuacion(0); + setCompletado(false); + setRespuestasCorrectas(0); + }; + + if (completado) { + return ( + +
+ + + + +

+ ¡Ejercicio Completado! +

+ +

+ Has respondido {respuestasCorrectas} de {PREGUNTAS.length} preguntas correctamente +

+ +
+

Puntuación

+

{puntuacion}

+

puntos

+
+ + +
+
+ ); + } + + return ( + +
+ {/* Header */} +
+
+

Flujo Circular de la Renta

+

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

+
+
+

Puntos

+

{puntuacion}

+
+
+ + {/* Barra de progreso */} +
+ +
+ + {/* Pregunta */} +
+

+ {pregunta.pregunta} +

+ +
+ {pregunta.opciones.map((opcion, index) => { + const estaSeleccionada = respuestaSeleccionada === index; + const esCorrecta = index === pregunta.respuestaCorrecta; + const mostrarCorrecta = mostrarResultado && esCorrecta; + const mostrarIncorrecta = mostrarResultado && estaSeleccionada && !esCorrecta; + + return ( + handleSeleccionar(index)} + disabled={mostrarResultado} + whileHover={!mostrarResultado ? { scale: 1.01 } : {}} + whileTap={!mostrarResultado ? { scale: 0.99 } : {}} + className={`w-full p-4 rounded-xl border-2 text-left transition-all ${ + mostrarCorrecta + ? 'border-green-500 bg-green-50' + : mostrarIncorrecta + ? 'border-red-500 bg-red-50' + : estaSeleccionada + ? 'border-blue-500 bg-blue-50' + : 'border-gray-200 bg-white hover:border-blue-300' + }`} + > +
+
+ {mostrarCorrecta && } + {mostrarIncorrecta && } + {!mostrarResultado && estaSeleccionada && ( +
+ )} +
+ + {opcion} + +
+ + ); + })} +
+
+ + {/* Explicación */} + {mostrarResultado && ( + +

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

+

{pregunta.explicacion}

+
+ )} + + {/* Botones */} +
+ {!mostrarResultado ? ( + + ) : preguntaActual < PREGUNTAS.length - 1 ? ( + + ) : null} +
+
+ + ); +} + +export default FlujoCircular; diff --git a/frontend/src/components/exercises/modulo1/FlujoCircularBasico.tsx b/frontend/src/components/exercises/modulo1/FlujoCircularBasico.tsx new file mode 100644 index 0000000..e4111ed --- /dev/null +++ b/frontend/src/components/exercises/modulo1/FlujoCircularBasico.tsx @@ -0,0 +1,361 @@ +import { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Card } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { CheckCircle, XCircle, Trophy, RotateCcw, ArrowRight, Users, Building2 } from 'lucide-react'; + +interface FlujoCircularBasicoProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Pregunta { + id: number; + pregunta: string; + tipo: 'mercado-bienes' | 'mercado-factores' | 'flujo-real' | 'flujo-monetario'; + opciones: string[]; + respuestaCorrecta: number; + explicacion: string; +} + +const PREGUNTAS: Pregunta[] = [ + { + id: 1, + pregunta: 'En el mercado de bienes y servicios, ¿quiénes son los demandantes?', + tipo: 'mercado-bienes', + opciones: [ + 'Las empresas', + 'Las familias', + 'El gobierno', + 'Los bancos' + ], + respuestaCorrecta: 1, + explicacion: 'En el mercado de bienes y servicios, las familias son los demandantes (compran productos) y las empresas son los oferentes (venden productos).' + }, + { + id: 2, + pregunta: 'En el mercado de factores de producción, ¿quiénes ofrecen el trabajo, la tierra y el capital?', + tipo: 'mercado-factores', + opciones: [ + 'Las familias', + 'Las empresas', + 'El Estado', + 'Los inversores extranjeros' + ], + respuestaCorrecta: 0, + explicacion: 'Las familias son propietarias de los factores de producción (trabajo, tierra, capital) y los ofrecen a las empresas a cambio de ingresos.' + }, + { + id: 3, + pregunta: '¿Qué representa el FLUJO REAL en el modelo de 2 sectores?', + tipo: 'flujo-real', + opciones: [ + 'El movimiento de dinero entre familias y empresas', + 'El movimiento de bienes, servicios y factores de producción', + 'Los impuestos pagados al gobierno', + 'Las transacciones bancarias' + ], + respuestaCorrecta: 1, + explicacion: 'El flujo real representa el movimiento físico de bienes y servicios (de empresas a familias) y factores de producción (de familias a empresas).' + }, + { + id: 4, + pregunta: '¿Qué reciben las familias a cambio de ofrecer sus factores de producción?', + tipo: 'flujo-monetario', + opciones: [ + 'Productos terminados', + 'Ingresos (salarios, rentas, intereses, beneficios)', + 'Servicios públicos', + 'Acciones de empresas' + ], + respuestaCorrecta: 1, + explicacion: 'Las familias reciben ingresos monetarios: salarios (por trabajo), rentas (por tierra), intereses (por capital) y beneficios (por empresa).' + }, + { + id: 5, + pregunta: 'En el FLUJO MONETARIO, el dinero fluye de las empresas a las familias como:', + tipo: 'flujo-monetario', + opciones: [ + 'Pagos por compra de bienes', + 'Pagos por factores de producción (costes)', + 'Impuestos', + 'Subvenciones' + ], + respuestaCorrecta: 1, + explicacion: 'Las empresas pagan a las familias por el uso de sus factores: salarios (trabajo), alquileres (tierra), intereses (capital) y beneficios (emprendimiento).' + }, + { + id: 6, + pregunta: '¿Por qué se llama "flujo circular"?', + tipo: 'flujo-real', + opciones: [ + 'Porque el dinero siempre aumenta', + 'Porque hay un flujo continuo en ambas direcciones entre familias y empresas', + 'Porque las empresas siempre ganan', + 'Porque el gobierno interviene' + ], + respuestaCorrecta: 1, + explicacion: 'Se llama flujo circular porque hay un movimiento continuo en ambas direcciones: factores de producción van de familias a empresas, y bienes/servicios van de empresas a familias.' + } +]; + +export function FlujoCircularBasico({ ejercicioId: _ejercicioId, onComplete }: FlujoCircularBasicoProps) { + const [preguntaActual, setPreguntaActual] = useState(0); + const [respuestaSeleccionada, setRespuestaSeleccionada] = useState(null); + const [mostrarResultado, setMostrarResultado] = useState(false); + const [puntuacion, setPuntuacion] = useState(0); + const [completado, setCompletado] = useState(false); + const [respuestasCorrectas, setRespuestasCorrectas] = useState(0); + + const pregunta = PREGUNTAS[preguntaActual]; + + const getTipoLabel = (tipo: string) => { + switch (tipo) { + case 'mercado-bienes': + return 'Mercado de Bienes'; + case 'mercado-factores': + return 'Mercado de Factores'; + case 'flujo-real': + return 'Flujo Real'; + case 'flujo-monetario': + return 'Flujo Monetario'; + default: + return ''; + } + }; + + const getTipoColor = (tipo: string) => { + switch (tipo) { + case 'mercado-bienes': + return 'bg-blue-100 text-blue-700'; + case 'mercado-factores': + return 'bg-green-100 text-green-700'; + case 'flujo-real': + return 'bg-purple-100 text-purple-700'; + case 'flujo-monetario': + return 'bg-amber-100 text-amber-700'; + default: + return 'bg-gray-100 text-gray-700'; + } + }; + + const handleSeleccionar = (index: number) => { + if (mostrarResultado) return; + setRespuestaSeleccionada(index); + }; + + const handleVerificar = () => { + if (respuestaSeleccionada === null) return; + + const esCorrecta = respuestaSeleccionada === pregunta.respuestaCorrecta; + setMostrarResultado(true); + + if (esCorrecta) { + setPuntuacion(prev => prev + Math.round(100 / PREGUNTAS.length)); + setRespuestasCorrectas(prev => prev + 1); + } + + if (preguntaActual === PREGUNTAS.length - 1) { + setTimeout(() => { + setCompletado(true); + const puntuacionFinal = puntuacion + (esCorrecta ? Math.round(100 / PREGUNTAS.length) : 0); + if (onComplete) { + onComplete(puntuacionFinal); + } + }, 2000); + } + }; + + const handleSiguiente = () => { + setPreguntaActual(prev => prev + 1); + setRespuestaSeleccionada(null); + setMostrarResultado(false); + }; + + const handleReiniciar = () => { + setPreguntaActual(0); + setRespuestaSeleccionada(null); + setMostrarResultado(false); + setPuntuacion(0); + setCompletado(false); + setRespuestasCorrectas(0); + }; + + if (completado) { + return ( + +
+ + + + +

+ ¡Ejercicio Completado! +

+ +

+ Has respondido {respuestasCorrectas} de {PREGUNTAS.length} preguntas correctamente +

+ +
+
+ + Familias +
+
↔️
+
+ + Empresas +
+
+ +
+

Puntuación

+

{puntuacion}

+

puntos

+
+ + +
+
+ ); + } + + return ( + +
+
+
+

Flujo Circular: 2 Sectores

+

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

+
+
+

Puntos

+

{puntuacion}

+
+
+ +
+ +
+ +
+
+ {getTipoLabel(pregunta.tipo)} +
+

+ {pregunta.pregunta} +

+
+ +
+ {pregunta.opciones.map((opcion, index) => { + const estaSeleccionada = respuestaSeleccionada === index; + const esCorrecta = index === pregunta.respuestaCorrecta; + const mostrarCorrecta = mostrarResultado && esCorrecta; + const mostrarIncorrecta = mostrarResultado && estaSeleccionada && !esCorrecta; + + return ( + handleSeleccionar(index)} + disabled={mostrarResultado} + whileHover={!mostrarResultado ? { scale: 1.01 } : {}} + whileTap={!mostrarResultado ? { scale: 0.99 } : {}} + className={`w-full p-4 rounded-xl border-2 text-left transition-all ${ + mostrarCorrecta + ? 'border-green-500 bg-green-50' + : mostrarIncorrecta + ? 'border-red-500 bg-red-50' + : estaSeleccionada + ? 'border-blue-500 bg-blue-50' + : 'border-gray-200 bg-white hover:border-blue-300' + }`} + > +
+
+ {mostrarCorrecta && } + {mostrarIncorrecta && } + {!mostrarResultado && estaSeleccionada && ( +
+ )} +
+ + {opcion} + +
+ + ); + })} +
+ + + {mostrarResultado && ( + +

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

+

{pregunta.explicacion}

+
+ )} +
+ +
+ {!mostrarResultado ? ( + + ) : preguntaActual < PREGUNTAS.length - 1 ? ( + + ) : null} +
+
+ + ); +} + +export default FlujoCircularBasico; diff --git a/frontend/src/components/exercises/modulo1/ProblemaEconomicoFundamental.tsx b/frontend/src/components/exercises/modulo1/ProblemaEconomicoFundamental.tsx new file mode 100644 index 0000000..eac9e61 --- /dev/null +++ b/frontend/src/components/exercises/modulo1/ProblemaEconomicoFundamental.tsx @@ -0,0 +1,258 @@ +import { useState } from 'react'; +import { motion } from 'framer-motion'; +import { Card } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { CheckCircle, XCircle, ArrowRight } from 'lucide-react'; + +interface EjercicioProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Pregunta { + id: number; + pregunta: string; + opciones: string[]; + correcta: number; + explicacion: string; + categoria: 'que' | 'como' | 'para_quien'; +} + +const PREGUNTAS: Pregunta[] = [ + { + id: 1, + pregunta: "¿Qué decisión responde a la pregunta 'QUÉ producir'?", + opciones: [ + "Elegir entre producir computadoras o smartphones", + "Decidir si usar mano de obra o maquinaria", + "Determinar si los productos van a ricos o pobres", + "Establecer el precio de venta de los productos" + ], + correcta: 0, + explicacion: "La pregunta 'QUÉ producir' se refiere a la elección de qué bienes y servicios se van a fabricar con los recursos disponibles.", + categoria: 'que' + }, + { + id: 2, + pregunta: "¿Qué decisión responde a la pregunta 'CÓMO producir'?", + opciones: [ + "Elegir entre producir autos o camiones", + "Decidir entre usar tecnología o mano de obra intensiva", + "Determinar quién consumirá los productos", + "Calcular cuánto invertir en publicidad" + ], + correcta: 1, + explicacion: "La pregunta 'CÓMO producir' se refiere a la elección de la técnica o método de producción a utilizar.", + categoria: 'como' + }, + { + id: 3, + pregunta: "¿Qué decisión responde a la pregunta 'PARA QUIÉN producir'?", + opciones: [ + "Seleccionar los materiales a utilizar", + "Elegir la ubicación de la fábrica", + "Distribuir los productos entre diferentes grupos de la sociedad", + "Determinar la cantidad a producir" + ], + correcta: 2, + explicacion: "La pregunta 'PARA QUIÉN producir' se refiere a la distribución de los bienes y servicios entre los miembros de la sociedad.", + categoria: 'para_quien' + }, + { + id: 4, + pregunta: "Un país debe decidir entre destinar sus recursos a hospitales o a escuelas. ¿Qué pregunta del problema económico resuelve?", + opciones: [ + "¿Qué producir?", + "¿Cómo producir?", + "¿Para quién producir?", + "¿Cuánto producir?" + ], + correcta: 0, + explicacion: "Elegir entre hospitales y escuelas es una decisión sobre QUÉ bienes y servicios públicos producir.", + categoria: 'que' + }, + { + id: 5, + pregunta: "Una empresa textil decide reemplazar trabajadores por máquinas automáticas. ¿Qué pregunta responde?", + opciones: [ + "¿Qué producir?", + "¿Cómo producir?", + "¿Para quién producir?", + "¿Dónde producir?" + ], + correcta: 1, + explicacion: "La decisión de usar máquinas vs. trabajadores es una decisión sobre CÓMO producir.", + categoria: 'como' + }, + { + id: 6, + pregunta: "El gobierno implementa subsidios para que los medicamentos sean accesibles a personas de bajos recursos. ¿Qué pregunta resuelve?", + opciones: [ + "¿Qué producir?", + "¿Cómo producir?", + "¿Para quién producir?", + "¿Cuándo producir?" + ], + correcta: 2, + explicacion: "Los subsidios para acceso equitativo responden a la pregunta PARA QUIÉN producir o distribuir.", + categoria: 'para_quien' + } +]; + +export function ProblemaEconomicoFundamental({ ejercicioId: _ejercicioId, onComplete }: EjercicioProps) { + const [preguntaActual, setPreguntaActual] = useState(0); + const [respuestas, setRespuestas] = useState([]); + const [mostrarResultado, setMostrarResultado] = useState(false); + const [completado, setCompletado] = useState(false); + + const pregunta = PREGUNTAS[preguntaActual]; + const esUltima = preguntaActual === PREGUNTAS.length - 1; + const progreso = ((preguntaActual) / PREGUNTAS.length) * 100; + + const handleRespuesta = (index: number) => { + const nuevasRespuestas = [...respuestas, index]; + setRespuestas(nuevasRespuestas); + + if (esUltima) { + const correctas = nuevasRespuestas.filter((r, i) => r === PREGUNTAS[i].correcta).length; + const puntuacion = Math.round((correctas / PREGUNTAS.length) * 100); + setCompletado(true); + onComplete?.(puntuacion); + } else { + setMostrarResultado(true); + } + }; + + const handleSiguiente = () => { + setPreguntaActual(prev => prev + 1); + setMostrarResultado(false); + }; + + const esCorrecta = respuestas[preguntaActual] === pregunta.correcta; + + const getCategoriaLabel = (cat: string) => { + switch (cat) { + case 'que': return '¿Qué producir?'; + case 'como': return '¿Cómo producir?'; + case 'para_quien': return '¿Para quién producir?'; + default: return ''; + } + }; + + const getCategoriaColor = (cat: string) => { + switch (cat) { + case 'que': return 'bg-blue-100 text-blue-800'; + case 'como': return 'bg-green-100 text-green-800'; + case 'para_quien': return 'bg-purple-100 text-purple-800'; + default: return 'bg-gray-100 text-gray-800'; + } + }; + + if (completado) { + const correctas = respuestas.filter((r, i) => r === PREGUNTAS[i].correcta).length; + const puntuacion = Math.round((correctas / PREGUNTAS.length) * 100); + + return ( + + +
+ +
+
+ +

¡Ejercicio Completado!

+

+ Dominaste las tres preguntas fundamentales de la economía +

+ +
{puntuacion}
+

puntos

+ +
+

Resumen:

+

{correctas} de {PREGUNTAS.length} correctas

+
+
+ ); + } + + return ( + +
+ {/* Header */} +
+ + {getCategoriaLabel(pregunta.categoria)} + + + {preguntaActual + 1} / {PREGUNTAS.length} + +
+ + {/* Progress */} +
+
+ +
+
+ + {/* Pregunta */} +

{pregunta.pregunta}

+ + {/* Opciones */} + {!mostrarResultado ? ( +
+ {pregunta.opciones.map((opcion, index) => ( + handleRespuesta(index)} + whileHover={{ scale: 1.01 }} + whileTap={{ scale: 0.99 }} + className="w-full p-4 text-left border-2 border-gray-200 rounded-xl hover:border-blue-400 hover:bg-blue-50 transition-all flex items-center gap-3" + > + + {String.fromCharCode(65 + index)} + + {opcion} + + ))} +
+ ) : ( + +
+ {esCorrecta ? ( + + ) : ( + + )} + + {esCorrecta ? '¡Correcto!' : 'Incorrecto'} + +
+

{pregunta.explicacion}

+ + +
+ )} +
+
+ ); +} + +export default ProblemaEconomicoFundamental; diff --git a/frontend/src/components/exercises/modulo1/ProductividadCalculator.tsx b/frontend/src/components/exercises/modulo1/ProductividadCalculator.tsx new file mode 100644 index 0000000..17f5ab2 --- /dev/null +++ b/frontend/src/components/exercises/modulo1/ProductividadCalculator.tsx @@ -0,0 +1,220 @@ +import { useState } from 'react'; + +interface ProductividadCalculatorProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Escenario { + id: number; + nombre: string; + trabajadores: number; + output: number; + productividadMarginal?: number; +} + +const escenariosIniciales: Escenario[] = [ + { id: 1, nombre: 'Fábrica A', trabajadores: 10, output: 500 }, + { id: 2, nombre: 'Fábrica B', trabajadores: 20, output: 900 }, + { id: 3, nombre: 'Fábrica C', trabajadores: 30, output: 1200 }, +]; + +export function ProductividadCalculator({ ejercicioId: _ejercicioId, onComplete }: ProductividadCalculatorProps) { + const [escenarios, setEscenarios] = useState(escenariosIniciales); + const [respuestas, setRespuestas] = useState<{[key: number]: {media: string; marginal: string}}>({}); + const [validados, setValidados] = useState<{[key: number]: boolean}>({}); + const [completado, setCompletado] = useState(false); + + const calcularProductividadMedia = (trabajadores: number, output: number): number => { + return Number((output / trabajadores).toFixed(2)); + }; + + const calcularRespuestasCorrectas = (): number => { + let correctas = 0; + escenarios.forEach((escenario, index) => { + if (index === 0) return; + + const prodMediaCorrecta = calcularProductividadMedia(escenario.trabajadores, escenario.output); + const prodMarginalCorrecta = (escenario.output - escenarios[index - 1].output) / + (escenario.trabajadores - escenarios[index - 1].trabajadores); + + const respuesta = respuestas[escenario.id]; + if (respuesta) { + if (Math.abs(Number(respuesta.media) - prodMediaCorrecta) < 0.5) correctas++; + if (Math.abs(Number(respuesta.marginal) - prodMarginalCorrecta) < 0.5) correctas++; + } + }); + return correctas; + }; + + const handleValidar = () => { + const nuevosValidados: {[key: number]: boolean} = {}; + let todasCorrectas = true; + + escenarios.forEach((escenario, index) => { + if (index === 0) { + nuevosValidados[escenario.id] = true; + return; + } + + const respuesta = respuestas[escenario.id]; + if (!respuesta || !respuesta.media || !respuesta.marginal) { + todasCorrectas = false; + nuevosValidados[escenario.id] = false; + return; + } + + const prodMediaCorrecta = calcularProductividadMedia(escenario.trabajadores, escenario.output); + const prodMarginalCorrecta = (escenario.output - escenarios[index - 1].output) / + (escenario.trabajadores - escenarios[index - 1].trabajadores); + + const mediaCorrecta = Math.abs(Number(respuesta.media) - prodMediaCorrecta) < 0.5; + const marginalCorrecta = Math.abs(Number(respuesta.marginal) - prodMarginalCorrecta) < 0.5; + + nuevosValidados[escenario.id] = mediaCorrecta && marginalCorrecta; + if (!mediaCorrecta || !marginalCorrecta) todasCorrectas = false; + }); + + setValidados(nuevosValidados); + + if (todasCorrectas && !completado) { + setCompletado(true); + if (onComplete) { + onComplete(100); + } + } + }; + + const handleReset = () => { + setRespuestas({}); + setValidados({}); + setCompletado(false); + }; + + return ( +
+
+

Calculadora de Productividad

+

+ Calcula la productividad media y marginal para cada escenario. +

+
+ +
+

Fórmulas:

+
    +
  • Productividad Media: Output ÷ Número de trabajadores
  • +
  • Productividad Marginal: ΔOutput ÷ ΔTrabajadores
  • +
+
+ +
+ + + + + + + + + + + + + {escenarios.map((escenario, index) => ( + + + + + + + + + ))} + +
EscenarioTrabajadoresOutput (unidades)Productividad MediaProductividad MarginalEstado
{escenario.nombre}{escenario.trabajadores}{escenario.output} + {index === 0 ? ( + + {calcularProductividadMedia(escenario.trabajadores, escenario.output)} + + ) : ( + setRespuestas(prev => ({ + ...prev, + [escenario.id]: { ...prev[escenario.id], media: e.target.value } + }))} + className={`w-24 px-2 py-1 text-center border rounded ${ + validados[escenario.id] === true + ? 'border-green-500 bg-green-50' + : validados[escenario.id] === false + ? 'border-red-500 bg-red-50' + : 'border-gray-300' + }`} + placeholder="?" + /> + )} + + {index === 0 ? ( + - + ) : ( + setRespuestas(prev => ({ + ...prev, + [escenario.id]: { ...prev[escenario.id], marginal: e.target.value } + }))} + className={`w-24 px-2 py-1 text-center border rounded ${ + validados[escenario.id] === true + ? 'border-green-500 bg-green-50' + : validados[escenario.id] === false + ? 'border-red-500 bg-red-50' + : 'border-gray-300' + }`} + placeholder="?" + /> + )} + + {validados[escenario.id] === true && ( + ✓ Correcto + )} + {validados[escenario.id] === false && ( + ✗ Revisar + )} +
+
+ +
+ + +
+ + {completado && ( +
+

¡Excelente trabajo!

+

100 puntos

+

+ Has calculado correctamente todas las productividades. +

+
+ )} +
+ ); +} + +export default ProductividadCalculator; diff --git a/frontend/src/components/exercises/modulo1/QuizBienes.tsx b/frontend/src/components/exercises/modulo1/QuizBienes.tsx new file mode 100644 index 0000000..1cb3c5c --- /dev/null +++ b/frontend/src/components/exercises/modulo1/QuizBienes.tsx @@ -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(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 ( + +
+ + + + +

¡Quiz Completado!

+

+ Respondiste correctamente {respuestasCorrectas} de {PREGUNTAS.length} preguntas +

+ +
+

Puntuación Total

+

{puntuacion} puntos

+
+ + +
+
+ ); + } + + return ( + + + +
+
+ Progreso + {Math.round(progreso)}% +
+
+ +
+
+ +
+
+ + Clasifica el siguiente bien: +
+ +

{pregunta.bien}

+

{pregunta.descripcion}

+
+ +
+ {pregunta.opciones.map((opcion, index) => { + const isSelected = respuestaSeleccionada === opcion; + const isCorrect = opcion === pregunta.respuestaCorrecta; + const showCorrect = mostrarRetroalimentacion && isCorrect; + const showIncorrect = mostrarRetroalimentacion && isSelected && !isCorrect; + + return ( + 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' + }`} + > +
+
+ + {String.fromCharCode(65 + index)} + + {opcion} +
+ {showCorrect && } + {showIncorrect && } +
+
+ ); + })} +
+ + + {mostrarRetroalimentacion && ( + +
+
+ {esCorrecta ? ( + + ) : ( + + )} + + {esCorrecta ? '¡Correcto!' : 'Incorrecto'} + +
+

+ {pregunta.explicacionDetallada} +

+
+
+ )} +
+ +
+
+ Puntuación: {puntuacion} pts +
+ + {!mostrarRetroalimentacion ? ( + + ) : ( + + )} +
+
+ ); +} + +export default QuizBienes; diff --git a/frontend/src/components/exercises/modulo1/RazonamientoEconomico.tsx b/frontend/src/components/exercises/modulo1/RazonamientoEconomico.tsx new file mode 100644 index 0000000..1b7f97a --- /dev/null +++ b/frontend/src/components/exercises/modulo1/RazonamientoEconomico.tsx @@ -0,0 +1,356 @@ +import { useState } from 'react'; +import { motion } from 'framer-motion'; +import { Card } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { CheckCircle, XCircle, Lightbulb, ArrowRight, TrendingUp, Users, DollarSign, Scale } from 'lucide-react'; + +interface EjercicioProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Escenario { + id: number; + titulo: string; + descripcion: string; + pregunta: string; + opciones: { + texto: string; + correcta: boolean; + explicacion: string; + }[]; + icono: 'trending' | 'users' | 'dollar' | 'scale'; +} + +const ESCENARIOS: Escenario[] = [ + { + id: 1, + titulo: "Costo de Oportunidad", + descripcion: "María tiene $100 y está decidiendo entre comprar un libro de economía o ir al cine con amigos.", + pregunta: "¿Cuál es el costo de oportunidad de elegir el libro?", + opciones: [ + { + texto: "Los $100 que gasta en el libro", + correcta: false, + explicacion: "El dinero gastado es el costo explícito, no el costo de oportunidad." + }, + { + texto: "El disfrute y experiencia de ir al cine con amigos", + correcta: true, + explicacion: "¡Correcto! El costo de oportunidad es lo que sacrificas: la experiencia del cine que dejas de tener." + }, + { + texto: "El tiempo que le toma leer el libro", + correcta: false, + explicacion: "Aunque el tiempo es un recurso, el costo de oportunidad específico es la alternativa forgada (el cine)." + }, + { + texto: "El valor del libro mismo", + correcta: false, + explicacion: "El valor del libro es el beneficio de la elección, no el costo de la alternativa." + } + ], + icono: 'scale' + }, + { + id: 2, + titulo: "Incentivos", + descripcion: "Un gobierno aumenta los impuestos a los cigarrillos para reducir el consumo tabáquico.", + pregunta: "¿Qué principio económico se está aplicando?", + opciones: [ + { + texto: "Las personas enfrentan disyuntivas", + correcta: false, + explicacion: "Aunque las disyuntivas existen, no es el principio principal aquí." + }, + { + texto: "El costo de algo es lo que sacrificas", + correcta: false, + explicacion: "No es el principio más relevante en este caso." + }, + { + texto: "Los incentivos afectan el comportamiento", + correcta: true, + explicacion: "¡Correcto! Al aumentar el costo (precio), se crea un incentivo para fumar menos." + }, + { + texto: "El comercio puede mejorar el bienestar", + correcta: false, + explicacion: "Este principio no aplica directamente a esta situación." + } + ], + icono: 'trending' + }, + { + id: 3, + titulo: "Racionalidad Económica", + descripcion: "Una empresa decide invertir en maquinaria nueva que aumentará la producción en 50%.", + pregunta: "¿Qué supuesto sobre la racionalidad económica se está haciendo?", + opciones: [ + { + texto: "Que la empresa busca maximizar beneficios", + correcta: true, + explicacion: "¡Correcto! Se asume que la empresa actúa racionalmente para maximizar sus ganancias." + }, + { + texto: "Que la empresa quiere ayudar a la comunidad", + correcta: false, + explicacion: "Aunque podría ser cierto, la racionalidad económica supone maximización de beneficios." + }, + { + texto: "Que la empresa no tiene otras opciones", + correcta: false, + explicacion: "La racionalidad económica implica elegir la mejor opción entre alternativas." + }, + { + texto: "Que la empresa actúa por emociones", + correcta: false, + explicacion: "La racionalidad económica asume decisiones basadas en cálculo, no emociones." + } + ], + icono: 'dollar' + }, + { + id: 4, + titulo: "Marginalismo", + descripcion: "Un estudiante está estudiando para un examen. Ya lleva 6 horas estudiando.", + pregunta: "¿Qué análisis debería hacer para decidir si estudia una hora más?", + opciones: [ + { + texto: "Calcular el promedio de todas sus calificaciones", + correcta: false, + explicacion: "El análisis promedio no ayuda en decisiones de una hora adicional." + }, + { + texto: "Comparar el beneficio adicional vs el costo de una hora más", + correcta: true, + explicacion: "¡Correcto! El análisis marginal compara beneficios y costos adicionales." + }, + { + texto: "Preguntarle a sus compañeros cuánto estudiaron", + correcta: false, + explicacion: "Las decisiones de otros no determinan tu análisis marginal óptimo." + }, + { + texto: "Ver cuánto tiempo ha estudiado en total", + correcta: false, + explicacion: "El tiempo acumulado no es relevante para la decisión marginal." + } + ], + icono: 'users' + }, + { + id: 5, + titulo: "Eficiencia vs. Equidad", + descripcion: "Un país puede distribuir la riqueza de forma igualitaria (todos ganan lo mismo) o por productividad (los más productivos ganan más).", + pregunta: "¿Qué principio económico ilustra esta disyuntiva?", + opciones: [ + { + texto: "La especialización mejora la productividad", + correcta: false, + explicacion: "La especialización es otro principio diferente." + }, + { + texto: "Los mercados son generalmente eficientes", + correcta: false, + explicacion: "Aunque relacionado, no captura la tensión entre eficiencia y equidad." + }, + { + texto: "Existe una disyuntiva entre eficiencia y equidad", + correcta: true, + explicacion: "¡Correcto! La distribución igualitaria (equidad) puede reducir incentivos (eficiencia)." + }, + { + texto: "El comercio internacional beneficia a todos", + correcta: false, + explicacion: "Este principio no aplica a la distribución interna de riqueza." + } + ], + icono: 'scale' + } +]; + +export function RazonamientoEconomico({ ejercicioId: _ejercicioId, onComplete }: EjercicioProps) { + const [escenarioActual, setEscenarioActual] = useState(0); + const [respuestas, setRespuestas] = useState<{escenarioId: number, correcta: boolean}[]>([]); + const [mostrarResultado, setMostrarResultado] = useState(false); + const [completado, setCompletado] = useState(false); + + const escenario = ESCENARIOS[escenarioActual]; + const esUltima = escenarioActual === ESCENARIOS.length - 1; + const progreso = (escenarioActual / ESCENARIOS.length) * 100; + + const getIcono = (tipo: string) => { + switch (tipo) { + case 'trending': return ; + case 'users': return ; + case 'dollar': return ; + case 'scale': return ; + default: return ; + } + }; + + const handleRespuesta = (index: number) => { + const esCorrecta = escenario.opciones[index].correcta; + const nuevasRespuestas = [...respuestas, { escenarioId: escenario.id, correcta: esCorrecta }]; + setRespuestas(nuevasRespuestas); + + if (esUltima) { + const correctas = nuevasRespuestas.filter(r => r.correcta).length; + const puntuacion = Math.round((correctas / ESCENARIOS.length) * 100); + setCompletado(true); + onComplete?.(puntuacion); + } else { + setMostrarResultado(true); + } + }; + + const handleSiguiente = () => { + setEscenarioActual(prev => prev + 1); + setMostrarResultado(false); + }; + + const respuestaActual = respuestas[respuestas.length - 1]; + + if (completado) { + const correctas = respuestas.filter(r => r.correcta).length; + const puntuacion = Math.round((correctas / ESCENARIOS.length) * 100); + + return ( + + +
+ +
+
+ +

¡Razonamiento Completado!

+

+ Has aplicado principios clave del pensamiento económico +

+ +
{puntuacion}
+

puntos

+ +
+

Conceptos evaluados:

+
+ {ESCENARIOS.map((e, i) => ( + + {e.titulo} + + ))} +
+
+
+ ); + } + + return ( + +
+ {/* Header */} +
+
+
+ {getIcono(escenario.icono)} +
+
+

{escenario.titulo}

+

Caso {escenarioActual + 1} de {ESCENARIOS.length}

+
+
+
+ + {/* Progress */} +
+
+ +
+
+ + {!mostrarResultado ? ( + + {/* Escenario */} +
+

{escenario.descripcion}

+
+ + {/* Pregunta */} +

{escenario.pregunta}

+ + {/* Opciones */} +
+ {escenario.opciones.map((opcion, index) => ( + handleRespuesta(index)} + whileHover={{ scale: 1.01 }} + whileTap={{ scale: 0.99 }} + className="w-full p-4 text-left border-2 border-gray-200 rounded-xl hover:border-blue-400 hover:bg-blue-50 transition-all" + > +
+ + {String.fromCharCode(65 + index)} + + {opcion.texto} +
+
+ ))} +
+
+ ) : ( + +
+ {respuestaActual?.correcta ? ( + + ) : ( + + )} + + {respuestaActual?.correcta ? '¡Excelente razonamiento!' : 'No es correcto'} + +
+ +
+

Respuesta correcta:

+

+ {escenario.opciones.find(o => o.correcta)?.texto} +

+
+ +

+ {escenario.opciones.find(o => o.correcta)?.explicacion} +

+ + +
+ )} +
+
+ ); +} + +export default RazonamientoEconomico; diff --git a/frontend/src/components/exercises/modulo1/RolesAgentesMatching.tsx b/frontend/src/components/exercises/modulo1/RolesAgentesMatching.tsx new file mode 100644 index 0000000..d790301 --- /dev/null +++ b/frontend/src/components/exercises/modulo1/RolesAgentesMatching.tsx @@ -0,0 +1,63 @@ +import { MatchingExercise } from '../common/MatchingExercise'; + +const ROLES_AGENTES = { + leftItems: [ + { id: 'consumidor', content: 'Consumidor' }, + { id: 'productor', content: 'Productor' }, + { id: 'propietario-factores', content: 'Propietario de factores' }, + { id: 'demandante-bienes', content: 'Demandante de bienes y servicios' }, + { id: 'ofertante-factores', content: 'Ofertante de factores de producción' }, + { id: 'generador-ingresos', content: 'Generador de rentas y salarios' }, + ], + rightItems: [ + { id: 'familia', content: 'Familias' }, + { id: 'empresa', content: 'Empresas' }, + { id: 'ambos', content: 'Ambos agentes' }, + ], + correctPairs: [ + { leftId: 'consumidor', rightId: 'familia' }, + { leftId: 'productor', rightId: 'empresa' }, + { leftId: 'propietario-factores', rightId: 'familia' }, + { leftId: 'demandante-bienes', rightId: 'familia' }, + { leftId: 'ofertante-factores', rightId: 'familia' }, + { leftId: 'generador-ingresos', rightId: 'ambos' }, + ], +}; + +interface RolesAgentesMatchingProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +export function RolesAgentesMatching({ + ejercicioId: _ejercicioId, + onComplete, +}: RolesAgentesMatchingProps) { + const handleComplete = (result: { + correct: number; + total: number; + attempts: number; + score: number; + maxScore: number; + isPerfect: boolean; + }) => { + if (onComplete) { + onComplete(result.score); + } + }; + + return ( + + ); +} + +export default RolesAgentesMatching; diff --git a/frontend/src/components/exercises/modulo1/SimuladorDisyuntivas.tsx b/frontend/src/components/exercises/modulo1/SimuladorDisyuntivas.tsx new file mode 100644 index 0000000..ee29577 --- /dev/null +++ b/frontend/src/components/exercises/modulo1/SimuladorDisyuntivas.tsx @@ -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 ( +
+
+

Simulador de Disyuntivas Económicas

+

Explora la Frontera de Posibilidades de Producción (FPP)

+
+ +
+
+ {/* Slider X */} +
+ + setBienX(Number(e.target.value))} + className="w-full h-2 bg-gray-200 rounded-lg cursor-pointer" + style={{ accentColor: '#2563eb' }} + /> +
+ + {/* Slider Y */} +
+ + setBienY(Number(e.target.value))} + className="w-full h-2 bg-gray-200 rounded-lg cursor-pointer" + style={{ accentColor: '#16a34a' }} + /> +
+ + {/* Mensaje de validación */} +
+ {validacion || 'Selecciona'}: +

{mensajeValidacion}

+
+ + {/* Botones */} +
+ + +
+ + {/* Mensaje de éxito */} + {completado && ( +
+

¡Ejercicio Completado!

+

100 puntos

+
+ )} +
+ + {/* Gráfico SVG */} +
+ + {/* Grid */} + + + + + + + + {/* Ejes */} + + + + {/* Flechas */} + + + + {/* Etiquetas */} + + Alimentos (millones de toneladas) + + + Tecnología (millones de unidades) + + + {/* Marcas X */} + {[0, 25, 50, 75, 100].map((val, i) => ( + + + {val} + + ))} + + {/* Marcas Y */} + {[0, 20, 40, 60, 80].map((val, i) => ( + + + {val} + + ))} + + {/* Curva FPP */} + {pathData && ( + + )} + + {/* Punto actual */} + + + {/* Coordenadas */} + + ({bienX}, {bienY}) + + +
+
+
+ ); +} + +export default SimuladorDisyuntivas; diff --git a/frontend/src/components/exercises/modulo1/SistemasEconomicosQuiz.tsx b/frontend/src/components/exercises/modulo1/SistemasEconomicosQuiz.tsx new file mode 100644 index 0000000..a97f1c2 --- /dev/null +++ b/frontend/src/components/exercises/modulo1/SistemasEconomicosQuiz.tsx @@ -0,0 +1,229 @@ +import { useState } from 'react'; +import { motion } from 'framer-motion'; +import { Card } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { CheckCircle, XCircle } from 'lucide-react'; + +interface EjercicioProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +const PREGUNTAS = [ + { + id: 1, + pregunta: "¿En qué sistema económico el Estado controla los medios de producción y la distribución de bienes?", + opciones: [ + "Economía de mercado", + "Economía planificada o centralizada", + "Economía mixta", + "Economía tradicional" + ], + correcta: 1, + explicacion: "En la economía planificada o centralizada, el Estado o gobierno controla todos los medios de producción y decide qué producir, cómo producirlo y para quién." + }, + { + id: 2, + pregunta: "¿Cuál es la característica principal de una economía de mercado?", + opciones: [ + "El gobierno decide todos los precios", + "Las decisiones económicas se toman por la oferta y la demanda", + "No existe propiedad privada", + "La producción se basa en costumbres ancestrales" + ], + correcta: 1, + explicacion: "En la economía de mercado, las decisiones económicas se determinan por la libre interacción de oferta y demanda, sin intervención estatal directa." + }, + { + id: 3, + pregunta: "¿Qué sistema económico combina elementos del mercado con intervención estatal?", + opciones: [ + "Economía de mercado pura", + "Economía planificada", + "Economía mixta", + "Economía cerrada" + ], + correcta: 2, + explicacion: "La economía mixta combina el funcionamiento del mercado con intervención estatal en sectores clave para corregir fallos de mercado y garantizar el bienestar social." + }, + { + id: 4, + pregunta: "En una economía planificada, ¿quién decide qué bienes se producen?", + opciones: [ + "Los consumidores mediante sus compras", + "Las empresas privadas", + "El gobierno o planificadores centrales", + "Los sindicatos" + ], + correcta: 2, + explicacion: "En la economía planificada, son los planificadores gubernamentales quienes determinan qué producir, en qué cantidad y a qué precio." + }, + { + id: 5, + pregunta: "¿Qué ventaja principal tiene la economía de mercado sobre la planificada?", + opciones: [ + "Mayor equidad en la distribución", + "Mayor eficiencia y respuesta a las preferencias de los consumidores", + "Eliminación de la competencia", + "Control total de la inflación" + ], + correcta: 1, + explicacion: "La economía de mercado tiende a ser más eficiente asignando recursos y responde mejor a las preferencias de los consumidores a través del mecanismo de precios." + }, + { + id: 6, + pregunta: "¿Qué problema suele presentar la economía planificada?", + opciones: [ + "Exceso de bienes de lujo", + "Ineficiencia y escasez por falta de incentivos", + "Alta desigualdad entre ricos y pobres", + "Inestabilidad cambiaria" + ], + correcta: 1, + explicacion: "La economía planificada suele sufrir de ineficiencias porque carece de los incentivos del mercado y la información descentralizada que guía la economía de mercado." + }, + { + id: 7, + pregunta: "¿Cuál es el rol del Estado en una economía mixta?", + opciones: [ + "No interviene en absoluto", + "Controla toda la producción", + "Regula, corrige fallos de mercado y provee bienes públicos", + "Solo recauda impuestos" + ], + correcta: 2, + explicacion: "En la economía mixta, el Estado regula el mercado, corrige fallos de mercado, proporciona bienes públicos y protege a los consumidores." + }, + { + id: 8, + pregunta: "La propiedad privada de los medios de producción es característica de:", + opciones: [ + "Solo economía de mercado", + "Solo economía mixta", + "Economía de mercado y economía mixta", + "Economía planificada" + ], + correcta: 2, + explicacion: "Tanto la economía de mercado como la mixta permiten la propiedad privada, a diferencia de la economía planificada donde los medios de producción son estatales." + } +]; + +export function SistemasEconomicosQuiz({ ejercicioId: _ejercicioId, onComplete }: EjercicioProps) { + const [preguntaActual, setPreguntaActual] = useState(0); + const [respuestas, setRespuestas] = useState([]); + const [mostrarResultado, setMostrarResultado] = useState(false); + const [completado, setCompletado] = useState(false); + + const pregunta = PREGUNTAS[preguntaActual]; + const esUltima = preguntaActual === PREGUNTAS.length - 1; + + const handleRespuesta = (index: number) => { + const nuevasRespuestas = [...respuestas, index]; + setRespuestas(nuevasRespuestas); + + if (esUltima) { + const correctas = nuevasRespuestas.filter((r, i) => r === PREGUNTAS[i].correcta).length; + const puntuacion = Math.round((correctas / PREGUNTAS.length) * 100); + setCompletado(true); + onComplete?.(puntuacion); + } else { + setMostrarResultado(true); + } + }; + + const handleSiguiente = () => { + setPreguntaActual(prev => prev + 1); + setMostrarResultado(false); + }; + + const esCorrecta = respuestas[preguntaActual] === pregunta.correcta; + + if (completado) { + const correctas = respuestas.filter((r, i) => r === PREGUNTAS[i].correcta).length; + const puntuacion = Math.round((correctas / PREGUNTAS.length) * 100); + + return ( + + +
+ +
+
+ +

¡Quiz Completado!

+

+ Respondiste {correctas} de {PREGUNTAS.length} preguntas correctamente +

+ +
{puntuacion}
+

puntos

+
+ ); + } + + return ( + +
+
+
+ Pregunta {preguntaActual + 1} de {PREGUNTAS.length} + {Math.round(((preguntaActual) / PREGUNTAS.length) * 100)}% +
+
+ +
+
+ +

{pregunta.pregunta}

+ + {!mostrarResultado ? ( +
+ {pregunta.opciones.map((opcion, index) => ( + handleRespuesta(index)} + whileHover={{ scale: 1.01 }} + whileTap={{ scale: 0.99 }} + className="w-full p-4 text-left border-2 border-gray-200 rounded-xl hover:border-blue-400 hover:bg-blue-50 transition-all" + > + {String.fromCharCode(65 + index)}. {opcion} + + ))} +
+ ) : ( + +
+ {esCorrecta ? ( + + ) : ( + + )} + + {esCorrecta ? '¡Correcto!' : 'Incorrecto'} + +
+

{pregunta.explicacion}

+ + +
+ )} +
+
+ ); +} + +export default SistemasEconomicosQuiz; diff --git a/frontend/src/components/exercises/modulo1/VentajaComparativaCalculator.tsx b/frontend/src/components/exercises/modulo1/VentajaComparativaCalculator.tsx new file mode 100644 index 0000000..b84ef83 --- /dev/null +++ b/frontend/src/components/exercises/modulo1/VentajaComparativaCalculator.tsx @@ -0,0 +1,359 @@ +import { useState } from 'react'; + +interface VentajaComparativaCalculatorProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Pais { + nombre: string; + vino: number; + queso: number; +} + +interface Respuestas { + ventajaAbsolutaVino: string; + ventajaAbsolutaQueso: string; + costoOportunidadPaisA: string; + costoOportunidadPaisB: string; + ventajaComparativaVino: string; + ventajaComparativaQueso: string; +} + +const paises: Pais[] = [ + { nombre: 'País A', vino: 100, queso: 200 }, + { nombre: 'País B', vino: 80, queso: 120 }, +]; + +const opcionesPais = ['País A', 'País B', 'Ninguno (igual producción)']; + +export function VentajaComparativaCalculator({ ejercicioId: _ejercicioId, onComplete }: VentajaComparativaCalculatorProps) { + const [respuestas, setRespuestas] = useState>({}); + const [validados, setValidados] = useState<{[key: string]: boolean | null}>({}); + const [completado, setCompletado] = useState(false); + + const calcularCostoOportunidad = (pais: Pais, bien: 'vino' | 'queso'): number => { + if (bien === 'vino') { + return pais.queso / pais.vino; + } + return pais.vino / pais.queso; + }; + + const handleRespuestaChange = (campo: keyof Respuestas, valor: string) => { + setRespuestas(prev => ({ ...prev, [campo]: valor })); + setValidados(prev => ({ ...prev, [campo]: null })); + }; + + const handleValidar = () => { + const nuevosValidados: {[key: string]: boolean | null} = {}; + + const correctas: {[key: string]: string} = { + ventajaAbsolutaVino: paises[0].vino > paises[1].vino ? 'País A' : 'País B', + ventajaAbsolutaQueso: paises[0].queso > paises[1].queso ? 'País A' : 'País B', + costoOportunidadPaisA: `${calcularCostoOportunidad(paises[0], 'vino').toFixed(2)}`, + costoOportunidadPaisB: `${calcularCostoOportunidad(paises[1], 'vino').toFixed(2)}`, + }; + + const costoA = calcularCostoOportunidad(paises[0], 'vino'); + const costoB = calcularCostoOportunidad(paises[1], 'vino'); + + correctas.ventajaComparativaVino = costoA < costoB ? 'País A' : 'País B'; + correctas.ventajaComparativaQueso = costoA < costoB ? 'País B' : 'País A'; + + let todasCorrectas = true; + Object.keys(correctas).forEach(key => { + const esCorrecta = respuestas[key as keyof Respuestas] === correctas[key]; + nuevosValidados[key] = esCorrecta; + if (!esCorrecta) todasCorrectas = false; + }); + + setValidados(nuevosValidados); + + if (todasCorrectas && !completado) { + setCompletado(true); + if (onComplete) { + onComplete(100); + } + } + }; + + const handleReset = () => { + setRespuestas({}); + setValidados({}); + setCompletado(false); + }; + + return ( +
+
+

Calculadora de Ventaja Comparativa

+

+ Analiza la producción de dos países para determinar ventajas absolutas y comparativas. +

+
+ +
+
+

Tabla de Producción

+ + + + + + + + + + {paises.map((pais, idx) => ( + + + + + + ))} + +
PaísVino (barriles)Queso (kg)
{pais.nombre}{pais.vino}{pais.queso}
+
+ +
+

Guía:

+
    +
  • + Ventaja Absoluta: Quien produce más de un bien con los mismos recursos. +
  • +
  • + Costo de Oportunidad: +
    • Vino: Queso sacrificado ÷ Vino producido +
    • Queso: Vino sacrificado ÷ Queso producido +
  • +
  • + Ventaja Comparativa: Quien tiene el menor costo de oportunidad. +
  • +
+
+
+ +
+
+

1. Ventaja Absoluta

+ +
+
+ +
+ {opcionesPais.map(opcion => { + const isSelected = respuestas.ventajaAbsolutaVino === opcion; + const estado = validados.ventajaAbsolutaVino; + + let className = 'px-4 py-2 rounded-lg text-sm border transition-all '; + if (estado === true) { + className += isSelected ? 'bg-green-100 border-green-500 text-green-800' : 'bg-gray-100 border-gray-200 text-gray-400'; + } else if (estado === false && isSelected) { + className += 'bg-red-100 border-red-500 text-red-800'; + } else { + className += isSelected ? 'bg-blue-100 border-blue-500 text-blue-800' : 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50'; + } + + return ( + + ); + })} +
+
+ +
+ +
+ {opcionesPais.map(opcion => { + const isSelected = respuestas.ventajaAbsolutaQueso === opcion; + const estado = validados.ventajaAbsolutaQueso; + + let className = 'px-4 py-2 rounded-lg text-sm border transition-all '; + if (estado === true) { + className += isSelected ? 'bg-green-100 border-green-500 text-green-800' : 'bg-gray-100 border-gray-200 text-gray-400'; + } else if (estado === false && isSelected) { + className += 'bg-red-100 border-red-500 text-red-800'; + } else { + className += isSelected ? 'bg-blue-100 border-blue-500 text-blue-800' : 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50'; + } + + return ( + + ); + })} +
+
+
+
+ +
+

2. Costo de Oportunidad del Vino

+ +
+
+ +
+ handleRespuestaChange('costoOportunidadPaisA', e.target.value)} + className={`w-24 px-3 py-2 border rounded-lg text-center ${ + validados.costoOportunidadPaisA === true + ? 'border-green-500 bg-green-50' + : validados.costoOportunidadPaisA === false + ? 'border-red-500 bg-red-50' + : 'border-gray-300' + }`} + placeholder="?" + /> + kg de queso +
+
+ +
+ +
+ handleRespuestaChange('costoOportunidadPaisB', e.target.value)} + className={`w-24 px-3 py-2 border rounded-lg text-center ${ + validados.costoOportunidadPaisB === true + ? 'border-green-500 bg-green-50' + : validados.costoOportunidadPaisB === false + ? 'border-red-500 bg-red-50' + : 'border-gray-300' + }`} + placeholder="?" + /> + kg de queso +
+
+
+
+ +
+

3. Ventaja Comparativa

+ +
+
+ +
+ {opcionesPais.map(opcion => { + const isSelected = respuestas.ventajaComparativaVino === opcion; + const estado = validados.ventajaComparativaVino; + + let className = 'px-4 py-2 rounded-lg text-sm border transition-all '; + if (estado === true) { + className += isSelected ? 'bg-green-100 border-green-500 text-green-800' : 'bg-gray-100 border-gray-200 text-gray-400'; + } else if (estado === false && isSelected) { + className += 'bg-red-100 border-red-500 text-red-800'; + } else { + className += isSelected ? 'bg-blue-100 border-blue-500 text-blue-800' : 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50'; + } + + return ( + + ); + })} +
+
+ +
+ +
+ {opcionesPais.map(opcion => { + const isSelected = respuestas.ventajaComparativaQueso === opcion; + const estado = validados.ventajaComparativaQueso; + + let className = 'px-4 py-2 rounded-lg text-sm border transition-all '; + if (estado === true) { + className += isSelected ? 'bg-green-100 border-green-500 text-green-800' : 'bg-gray-100 border-gray-200 text-gray-400'; + } else if (estado === false && isSelected) { + className += 'bg-red-100 border-red-500 text-red-800'; + } else { + className += isSelected ? 'bg-blue-100 border-blue-500 text-blue-800' : 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50'; + } + + return ( + + ); + })} +
+
+
+
+
+ +
+ + +
+ + {completado && ( +
+

¡Excelente análisis económico!

+

100 puntos

+

+ Has identificado correctamente las ventajas absolutas y comparativas. +

+
+ )} +
+ ); +} + +export default VentajaComparativaCalculator; diff --git a/frontend/src/components/exercises/modulo1/VentajasDesventajasSistemas.tsx b/frontend/src/components/exercises/modulo1/VentajasDesventajasSistemas.tsx new file mode 100644 index 0000000..48b549a --- /dev/null +++ b/frontend/src/components/exercises/modulo1/VentajasDesventajasSistemas.tsx @@ -0,0 +1,546 @@ +import { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Card } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { + CheckCircle, + XCircle, + RefreshCcw, + Link2, + Trophy, + Scale, + Target, + Zap, + ArrowRight, + Building2, + Users, + Globe, + Landmark, + Coins +} from 'lucide-react'; + +interface MatchingItem { + id: string; + content: string; + icon?: React.ReactNode; +} + +interface MatchingPair { + leftId: string; + rightId: string; +} + +interface VentajasDesventajasSistemasProps { + onComplete?: (result: { + correct: number; + total: number; + score: number; + isPerfect: boolean; + }) => void; +} + +const SISTEMAS_ECONOMICOS: MatchingItem[] = [ + { + id: 'mercado', + content: 'Economía de Mercado', + icon: , + }, + { + id: 'planificada', + content: 'Economía Planificada', + icon: , + }, + { + id: 'mixta', + content: 'Economía Mixta', + icon: , + }, +]; + +const CARACTERISTICAS: MatchingItem[] = [ + { + id: 'eficiencia', + content: 'Alta eficiencia en la asignación de recursos', + icon: , + }, + { + id: 'desigualdad', + content: 'Puede generar grandes desigualdades de ingreso', + icon: , + }, + { + id: 'planificacion', + content: 'El gobierno controla la producción y distribución', + icon: , + }, + { + id: 'flexibilidad', + content: 'Respuesta rápida a cambios en la demanda', + icon: , + }, + { + id: 'equidad', + content: 'Mayor equidad en la distribución de bienes', + icon: , + }, + { + id: 'burocracia', + content: 'Alta burocracia y lentitud en decisiones', + icon: , + }, + { + id: 'equilibrio', + content: 'Combina eficiencia con justicia social', + icon: , + }, + { + id: 'intervencion', + content: 'El Estado regula y corrige fallas del mercado', + icon: , + }, +]; + +const PAREJAS_CORRECTAS: MatchingPair[] = [ + { leftId: 'mercado', rightId: 'eficiencia' }, + { leftId: 'mercado', rightId: 'desigualdad' }, + { leftId: 'mercado', rightId: 'flexibilidad' }, + { leftId: 'planificada', rightId: 'planificacion' }, + { leftId: 'planificada', rightId: 'equidad' }, + { leftId: 'planificada', rightId: 'burocracia' }, + { leftId: 'mixta', rightId: 'equilibrio' }, + { leftId: 'mixta', rightId: 'intervencion' }, +]; + +interface Match { + leftId: string; + rightId: string; + isCorrect?: boolean; +} + +export function VentajasDesventajasSistemas({ onComplete }: VentajasDesventajasSistemasProps) { + const [matches, setMatches] = useState([]); + const [selectedLeft, setSelectedLeft] = useState(null); + const [selectedRight, setSelectedRight] = useState(null); + const [showResults, setShowResults] = useState(false); + const [attempts, setAttempts] = useState(0); + + const handleLeftClick = (itemId: string) => { + if (showResults) return; + + if (selectedLeft === itemId) { + setSelectedLeft(null); + } else { + setSelectedLeft(itemId); + if (selectedRight) { + handleCreateMatch(itemId, selectedRight); + } + } + }; + + const handleRightClick = (itemId: string) => { + if (showResults) return; + + if (selectedRight === itemId) { + setSelectedRight(null); + } else { + setSelectedRight(itemId); + if (selectedLeft) { + handleCreateMatch(selectedLeft, itemId); + } + } + }; + + const handleCreateMatch = (leftId: string, rightId: string) => { + const isLeftMatched = matches.some(m => m.leftId === leftId); + const isRightMatched = matches.some(m => m.rightId === rightId); + + if (isLeftMatched || isRightMatched) return; + + setMatches(prev => [...prev, { leftId, rightId }]); + setSelectedLeft(null); + setSelectedRight(null); + setAttempts(prev => prev + 1); + }; + + const handleRemoveMatch = (leftId: string) => { + if (showResults) return; + setMatches(prev => prev.filter(m => m.leftId !== leftId)); + }; + + const handleValidate = () => { + const validatedMatches = matches.map(match => { + const isCorrect = PAREJAS_CORRECTAS.some( + p => p.leftId === match.leftId && p.rightId === match.rightId + ); + return { ...match, isCorrect }; + }); + + setMatches(validatedMatches); + setShowResults(true); + + const correctCount = validatedMatches.filter(m => m.isCorrect).length; + const score = Math.round((correctCount / PAREJAS_CORRECTAS.length) * 100); + + if (onComplete) { + onComplete({ + correct: correctCount, + total: PAREJAS_CORRECTAS.length, + score, + isPerfect: correctCount === PAREJAS_CORRECTAS.length, + }); + } + }; + + const handleReset = () => { + setMatches([]); + setSelectedLeft(null); + setSelectedRight(null); + setShowResults(false); + setAttempts(0); + }; + + const isLeftMatched = (id: string) => matches.some(m => m.leftId === id); + const isRightMatched = (id: string) => matches.some(m => m.rightId === id); + + const getMatchStatus = (leftId: string): 'correct' | 'incorrect' | null => { + const match = matches.find(m => m.leftId === leftId); + if (!match || !showResults) return null; + return match.isCorrect ? 'correct' : 'incorrect'; + }; + + const getMatchedRightItem = (leftId: string) => { + const match = matches.find(m => m.leftId === leftId); + if (!match) return null; + return CARACTERISTICAS.find(item => item.id === match.rightId); + }; + + const getMatchedLeftItem = (rightId: string) => { + const match = matches.find(m => m.rightId === rightId); + if (!match) return null; + return SISTEMAS_ECONOMICOS.find(item => item.id === match.leftId); + }; + + const correctCount = matches.filter(m => m.isCorrect).length; + const allMatched = matches.length === PAREJAS_CORRECTAS.length; + + return ( +
+ {/* Header */} +
+
+

Sistemas Económicos

+

+ Relaciona cada sistema económico con sus características correspondientes +

+
+
+
+ + 100 pts +
+
+ + {attempts} intentos +
+
+
+ + {/* Instrucciones */} + +
+
+ +
+
+

¿Cómo jugar?

+

+ Haz clic en un sistema económico y luego en sus características para emparejarlos. + Cada sistema debe emparejarse con sus ventajas y desventajas específicas. +

+
+
+
+ + {/* Matching Area */} + +
+ {/* Sistemas Económicos */} +
+

+ Sistemas Económicos +

+
+ {SISTEMAS_ECONOMICOS.map(item => { + const matchedItem = getMatchedRightItem(item.id); + const status = getMatchStatus(item.id); + const isSelected = selectedLeft === item.id; + const isMatched = isLeftMatched(item.id); + + return ( + handleLeftClick(item.id)} + whileHover={!showResults && !isMatched ? { scale: 1.02 } : {}} + whileTap={!showResults && !isMatched ? { scale: 0.98 } : {}} + className={` + relative p-4 rounded-xl border-2 transition-all cursor-pointer + ${!showResults && !isMatched ? 'hover:border-blue-300 hover:shadow-md' : ''} + ${isSelected ? 'border-blue-500 bg-blue-50 ring-2 ring-blue-200' : ''} + ${isMatched && !showResults ? 'border-indigo-300 bg-indigo-50' : ''} + ${status === 'correct' ? 'border-green-500 bg-green-50' : ''} + ${status === 'incorrect' ? 'border-red-500 bg-red-50' : ''} + ${!isMatched && !isSelected ? 'border-gray-200 bg-white' : ''} + `} + > +
+
+ {item.icon} +
+ {item.content} +
+ + {/* Match indicator */} + {matchedItem && ( +
+
+ + {matchedItem.content} +
+
+ )} + + {/* Status icons */} + {showResults && status && ( +
+ {status === 'correct' ? ( + + ) : ( + + )} +
+ )} + + {/* Remove button */} + {isMatched && !showResults && ( + + )} + + {/* Counter badge */} + {!showResults && ( +
+ + {matches.filter(m => m.leftId === item.id).length} + +
+ )} +
+ ); + })} +
+
+ + {/* Características */} +
+

+ Características +

+
+ {CARACTERISTICAS.map(item => { + const matchedItem = getMatchedLeftItem(item.id); + const isSelected = selectedRight === item.id; + const isMatched = isRightMatched(item.id); + + return ( + handleRightClick(item.id)} + whileHover={!showResults && !isMatched ? { scale: 1.02 } : {}} + whileTap={!showResults && !isMatched ? { scale: 0.98 } : {}} + className={` + relative p-3 rounded-xl border-2 transition-all cursor-pointer + ${!showResults && !isMatched ? 'hover:border-blue-300 hover:shadow-md' : ''} + ${isSelected ? 'border-blue-500 bg-blue-50 ring-2 ring-blue-200' : ''} + ${isMatched ? 'border-gray-300 bg-gray-100 opacity-60' : ''} + ${!isMatched && !isSelected ? 'border-gray-200 bg-white' : ''} + `} + > +
+
+ {item.icon} +
+ + {item.content} + +
+ + {/* Match indicator */} + {matchedItem && ( +
+
+ + Emparejado con: {matchedItem.content} +
+
+ )} +
+ ); + })} +
+
+
+
+ + {/* Results Section */} + + {showResults && ( + + +
+ + + + +

+ {correctCount === PAREJAS_CORRECTAS.length + ? '¡Excelente!' + : correctCount >= PAREJAS_CORRECTAS.length * 0.7 + ? '¡Muy bien!' + : '¡Sigue practicando!'} +

+ +

+ {correctCount} de {PAREJAS_CORRECTAS.length} emparejamientos correctos +

+ + {/* Score Display */} +
+
+ +

+ {Math.round((correctCount / PAREJAS_CORRECTAS.length) * 100)} +

+

Puntuación

+
+ +
+ +

{correctCount}

+

Correctos

+
+ +
+ +

{attempts}

+

Intentos

+
+
+ + {/* Explicación de respuestas */} + {correctCount < PAREJAS_CORRECTAS.length && ( +
+

Respuestas correctas:

+
+ {SISTEMAS_ECONOMICOS.map(sistema => { + const caracteristicas = PAREJAS_CORRECTAS + .filter(p => p.leftId === sistema.id) + .map(p => CARACTERISTICAS.find(c => c.id === p.rightId)?.content); + + return ( +
+ + {sistema.content}: + + + {caracteristicas.join(', ')} + +
+ ); + })} +
+
+ )} +
+
+
+ )} +
+ + {/* Action Buttons */} +
+ + + {!showResults ? ( + + ) : ( + + )} +
+ + {/* Progress indicator */} +
+

+ Progreso: {matches.length} de{' '} + {PAREJAS_CORRECTAS.length} emparejamientos +

+
+ +
+
+
+ ); +} + +export default VentajasDesventajasSistemas; diff --git a/frontend/src/components/exercises/modulo1/index.ts b/frontend/src/components/exercises/modulo1/index.ts new file mode 100644 index 0000000..9418098 --- /dev/null +++ b/frontend/src/components/exercises/modulo1/index.ts @@ -0,0 +1,23 @@ +export { SimuladorDisyuntivas } from './SimuladorDisyuntivas'; +export { QuizBienes } from './QuizBienes'; +export { FlujoCircular } from './FlujoCircular'; +export { DefinicionEconomiaQuiz } from './DefinicionEconomiaQuiz'; +export { EscasezSimulator } from './EscasezSimulator'; +export { ProblemaEconomicoFundamental } from './ProblemaEconomicoFundamental'; +export { EconomiaPositivaVsNormativa } from './EconomiaPositivaVsNormativa'; +export { RazonamientoEconomico } from './RazonamientoEconomico'; +export { SistemasEconomicosQuiz } from './SistemasEconomicosQuiz'; +export { ComparativaSistemas } from './ComparativaSistemas'; +export { CasosPaises } from './CasosPaises'; +export { VentajasDesventajasSistemas } from './VentajasDesventajasSistemas'; +export { FPPConstructor } from './FPPConstructor'; +export { FPPAnalizador } from './FPPAnalizador'; +export { CostoOportunidadCalculator } from './CostoOportunidadCalculator'; +export { CrecimientoEconomicoFPP } from './CrecimientoEconomicoFPP'; +export { AgentesEconomicosQuiz } from './AgentesEconomicosQuiz'; +export { RolesAgentesMatching } from './RolesAgentesMatching'; +export { FlujoCircularBasico } from './FlujoCircularBasico'; +export { FactoresProduccionQuiz } from './FactoresProduccionQuiz'; +export { ProductividadCalculator } from './ProductividadCalculator'; +export { CostoOportunidadCotidiano } from './CostoOportunidadCotidiano'; +export { VentajaComparativaCalculator } from './VentajaComparativaCalculator'; \ No newline at end of file diff --git a/frontend/src/components/exercises/modulo2/AjusteEquilibrio.tsx b/frontend/src/components/exercises/modulo2/AjusteEquilibrio.tsx new file mode 100644 index 0000000..c48bc35 --- /dev/null +++ b/frontend/src/components/exercises/modulo2/AjusteEquilibrio.tsx @@ -0,0 +1,473 @@ +import React, { useState, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { RefreshCw, Play, Pause, ArrowRight, CheckCircle2, Trophy, RotateCcw, Info } from 'lucide-react'; + +interface AjusteEquilibrioProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +type EscenarioTipo = 'exceso_demanda' | 'exceso_oferta'; + +interface Escenario { + id: number; + tipo: EscenarioTipo; + titulo: string; + descripcion: string; + precioInicial: number; + precioEquilibrio: number; + cantidadEquilibrio: number; + mensajeAjuste: string; +} + +const escenarios: Escenario[] = [ + { + id: 1, + tipo: 'exceso_demanda', + titulo: 'Escasez de Vivienda', + descripcion: 'El precio actual de $600 está por debajo del equilibrio. Hay más personas buscando vivienda que apartamentos disponibles.', + precioInicial: 600, + precioEquilibrio: 900, + cantidadEquilibrio: 300, + mensajeAjuste: 'La escasez presiona al alza: los compradores compiten ofreciendo más, los vendedores suben precios.' + }, + { + id: 2, + tipo: 'exceso_oferta', + titulo: 'Superávit de Manzanas', + descripcion: 'La cosecha fue abundante y el precio actual de $80 está por encima del equilibrio. Hay más manzanas de las que la gente quiere comprar.', + precioInicial: 80, + precioEquilibrio: 50, + cantidadEquilibrio: 100, + mensajeAjuste: 'El superávit presiona a la baja: los vendedores compiten bajando precios para liquidar inventario.' + } +]; + +export const AjusteEquilibrio: React.FC = ({ onComplete, ejercicioId: _ejercicioId }) => { + const [escenarioActual, setEscenarioActual] = useState(0); + const [precioActual, setPrecioActual] = useState(escenarios[0].precioInicial); + const [estaAnimando, setEstaAnimando] = useState(false); + const [mostrarResultado, setMostrarResultado] = useState(false); + const [faseAjuste, setFaseAjuste] = useState<'inicio' | 'ajustando' | 'completado'>('inicio'); + const [score, setScore] = useState(0); + const [completado, setCompletado] = useState(false); + const [_startTime] = useState(Date.now()); + + const escenario = escenarios[escenarioActual]; + + useEffect(() => { + let interval: ReturnType; + + if (estaAnimando) { + setFaseAjuste('ajustando'); + interval = setInterval(() => { + setPrecioActual(prev => { + const diferencia = escenario.precioEquilibrio - prev; + const cambio = diferencia * 0.05; + + if (Math.abs(diferencia) < 2) { + setEstaAnimando(false); + setFaseAjuste('completado'); + setMostrarResultado(true); + return escenario.precioEquilibrio; + } + + return prev + cambio; + }); + }, 100); + } + + return () => clearInterval(interval); + }, [estaAnimando, escenario]); + + const handleIniciar = () => { + setEstaAnimando(true); + }; + + const handlePausar = () => { + setEstaAnimando(false); + }; + + const handleSiguiente = () => { + if (escenarioActual < escenarios.length - 1) { + const nextEscenario = escenarios[escenarioActual + 1]; + setEscenarioActual(prev => prev + 1); + setPrecioActual(nextEscenario.precioInicial); + setEstaAnimando(false); + setMostrarResultado(false); + setFaseAjuste('inicio'); + setScore(prev => prev + 50); + } else { + setCompletado(true); + if (onComplete) { + onComplete(100); + } + } + }; + + const handleReiniciar = () => { + setEscenarioActual(0); + setPrecioActual(escenarios[0].precioInicial); + setEstaAnimando(false); + setMostrarResultado(false); + setFaseAjuste('inicio'); + setScore(0); + setCompletado(false); + }; + + const generarCurvas = () => { + const demanda = []; + const oferta = []; + + for (let Q = 0; Q <= 400; Q += 20) { + const Pd = 1200 - 1 * Q; + const Po = 0 + 3 * Q; + if (Pd >= 0) demanda.push({ Q, P: Pd }); + if (Po >= 0) oferta.push({ Q, P: Po }); + } + + return { demanda, oferta }; + }; + + const { demanda, oferta } = generarCurvas(); + + const scaleX = (Q: number) => 60 + (Q / 400) * 320; + const scaleY = (P: number) => 280 - (P / 1200) * 240; + + const demandaPath = demanda.map((p, i) => + `${i === 0 ? 'M' : 'L'} ${scaleX(p.Q)} ${scaleY(p.P)}` + ).join(' '); + + const ofertaPath = oferta.map((p, i) => + `${i === 0 ? 'M' : 'L'} ${scaleX(p.Q)} ${scaleY(p.P)}` + ).join(' '); + + const calcularCantidades = (precio: number) => { + const Qd = Math.max(0, 1200 - precio); + const Qo = Math.max(0, precio / 3); + return { Qd, Qo }; + }; + + const cantidades = calcularCantidades(precioActual); + const esExcesoDemanda = cantidades.Qd > cantidades.Qo; + const diferencia = Math.abs(cantidades.Qd - cantidades.Qo); + + if (completado) { + return ( + + +

¡Ejercicio Completado!

+

Has observado el ajuste hacia el equilibrio

+ +
+
100%
+

+ Has completado todos los escenarios +

+
+ + +
+ ); + } + + return ( +
+
+
+
+ +

Ajuste al Equilibrio

+
+
+ + {escenarioActual + 1} de {escenarios.length} + + +
+
+

+ Observa cómo el mercado se autocorrige hacia el equilibrio. +

+
+ +
+
+

{escenario.titulo}

+ + + {/* Grid */} + {Array.from({ length: 6 }).map((_, i) => ( + + + + + ))} + + {/* Ejes */} + + + + {/* Labels */} + Cantidad (Q) + Precio (P) + + {/* Curva de Demanda */} + + D + + {/* Curva de Oferta */} + + S + + {/* Punto de equilibrio (E) */} + + + E + + + {/* Línea de precio actual */} + + + P=${Math.round(precioActual)} + + + {/* Cantidad demandada */} + + + Qd + + + {/* Cantidad ofrecida */} + + + Qo + + + {/* Flecha de dirección del ajuste */} + {faseAjuste === 'ajustando' && ( + + + + + + + + + + + + )} + + +
+
+ Precio + ${Math.round(precioActual)} +
+
+ Q Demanda + {Math.round(cantidades.Qd)} +
+
+ Q Oferta + {Math.round(cantidades.Qo)} +
+
+
+ +
+
+
+ +
+

Situación Actual

+

{escenario.descripcion}

+
+
+
+ + + {faseAjuste === 'inicio' && ( + +

¿Qué está pasando?

+
+
+

+ {esExcesoDemanda ? '🔥 Exceso de Demanda (Escasez)' : '📦 Exceso de Oferta (Superávit)'} +

+

+ Diferencia: {Math.round(diferencia)} unidades +

+
+
+ + +
+ )} + + {faseAjuste === 'ajustando' && ( + +

+ + Ajustando... +

+

{escenario.mensajeAjuste}

+ +
+
+ +
+ +
+
+ )} + + {faseAjuste === 'completado' && ( + +
+ +

¡Equilibrio Alcanzado!

+
+ +
+

Precio de equilibrio: ${escenario.precioEquilibrio}

+

Cantidad de equilibrio: {escenario.cantidadEquilibrio} unidades

+

+ En equilibrio, la cantidad demandada es igual a la cantidad ofrecida. + No hay presión para que el precio cambie. +

+
+ + +
+ )} +
+ +
+

Principio del Ajuste:

+
    +
  • + + Escasez (P < Pe): Los compradores ofrecen más → sube el precio +
  • +
  • + + Superávit (P > Pe): Los vendedores bajan precios → baja el precio +
  • +
+
+
+
+
+ ); +}; + +export default AjusteEquilibrio; diff --git a/frontend/src/components/exercises/modulo2/CalculoElasticidadPrecio.tsx b/frontend/src/components/exercises/modulo2/CalculoElasticidadPrecio.tsx new file mode 100644 index 0000000..9995d3c --- /dev/null +++ b/frontend/src/components/exercises/modulo2/CalculoElasticidadPrecio.tsx @@ -0,0 +1,310 @@ +import React, { useState, useEffect } from 'react'; + +interface DatosPrecioCantidad { + precioInicial: number; + cantidadInicial: number; + precioFinal: number; + cantidadFinal: number; +} + +const generarDatosAleatorios = (): DatosPrecioCantidad => { + const precioInicial = Math.round((Math.random() * 50 + 10) * 100) / 100; + const cantidadInicial = Math.round(Math.random() * 800 + 200); + + const cambioPrecio = (Math.random() > 0.5 ? 1 : -1) * (Math.random() * 20 + 5); + const elasticidad = Math.random() * 2 + 0.3; + + const precioFinal = Math.round((precioInicial + cambioPrecio) * 100) / 100; + const cambioCantidad = -elasticidad * (cambioPrecio / ((precioInicial + precioFinal) / 2)) * cantidadInicial; + const cantidadFinal = Math.round(cantidadInicial + cambioCantidad); + + return { + precioInicial: Math.max(1, precioInicial), + cantidadInicial: Math.max(10, cantidadInicial), + precioFinal: Math.max(1, precioFinal), + cantidadFinal: Math.max(10, cantidadFinal) + }; +}; + +export const CalculoElasticidadPrecio: React.FC = () => { + const [datos, setDatos] = useState(generarDatosAleatorios()); + const [respuestaUsuario, setRespuestaUsuario] = useState(''); + const [resultado, setResultado] = useState<{ + correcto: boolean; + mensaje: string; + valorReal: number; + } | null>(null); + const [mostrarFormula, setMostrarFormula] = useState(true); + + const calcularElasticidadPuntoMedio = (d: DatosPrecioCantidad): number => { + const cambioCantidad = d.cantidadFinal - d.cantidadInicial; + const cambioPrecio = d.precioFinal - d.precioInicial; + const cantidadPromedio = (d.cantidadInicial + d.cantidadFinal) / 2; + const precioPromedio = (d.precioInicial + d.precioFinal) / 2; + + if (cantidadPromedio === 0 || precioPromedio === 0) return 0; + + const elasticidad = (cambioCantidad / cantidadPromedio) / (cambioPrecio / precioPromedio); + return Math.abs(elasticidad); + }; + + const verificarRespuesta = () => { + const elasticidadReal = calcularElasticidadPuntoMedio(datos); + const respuestaNum = parseFloat(respuestaUsuario); + + if (isNaN(respuestaNum) || respuestaNum < 0) { + setResultado({ + correcto: false, + mensaje: 'Por favor ingresa un número válido mayor o igual a 0', + valorReal: elasticidadReal + }); + return; + } + + const margenError = 0.15; + const correcto = Math.abs(respuestaNum - elasticidadReal) <= margenError; + + setResultado({ + correcto, + mensaje: correcto + ? '¡Correcto! Has calculado la elasticidad correctamente.' + : `Incorrecto. El valor correcto es ${elasticidadReal.toFixed(2)}`, + valorReal: elasticidadReal + }); + }; + + const generarNuevoEjercicio = () => { + setDatos(generarDatosAleatorios()); + setRespuestaUsuario(''); + setResultado(null); + }; + + const precioPromedio = (datos.precioInicial + datos.precioFinal) / 2; + const cantidadPromedio = (datos.cantidadInicial + datos.cantidadFinal) / 2; + const cambioPrecio = datos.precioFinal - datos.precioInicial; + const cambioCantidad = datos.cantidadFinal - datos.cantidadInicial; + + return ( +
+

Cálculo de Elasticidad Precio de la Demanda

+

Utiliza la fórmula del punto medio para calcular la elasticidad.

+ +
+
+

+ 1 + Datos Iniciales +

+
+
+ Precio inicial (P₁): + ${datos.precioInicial.toFixed(2)} +
+
+ Cantidad inicial (Q₁): + {datos.cantidadInicial.toLocaleString()} unidades +
+
+
+ +
+

+ 2 + Datos Finales +

+
+
+ Precio final (P₂): + ${datos.precioFinal.toFixed(2)} +
+
+ Cantidad final (Q₂): + {datos.cantidadFinal.toLocaleString()} unidades +
+
+
+
+ + {mostrarFormula && ( +
+

+ + + + Fórmula del Punto Medio (Arco) +

+
+

+ Ed = |(Q₂ - Q₁) / ((Q₂ + Q₁) / 2)| + ÷ + (P₂ - P₁) / ((P₂ + P₁) / 2) +

+
+

+ Donde: Q = Cantidad, P = Precio, y usamos valores absolutos para obtener la elasticidad como número positivo. +

+
+ )} + +
+

Paso a paso (valores calculados):

+
+
+

Cambio en cantidad:

+

({datos.cantidadFinal} - {datos.cantidadInicial}) = {cambioCantidad}

+
+
+

Cantidad promedio:

+

({datos.cantidadFinal} + {datos.cantidadInicial}) / 2 = {cantidadPromedio}

+
+
+

Cambio en precio:

+

(${datos.precioFinal} - ${datos.precioInicial}) = ${cambioPrecio.toFixed(2)}

+
+
+

Precio promedio:

+

(${datos.precioFinal} + ${datos.precioInicial}) / 2 = ${precioPromedio.toFixed(2)}

+
+
+
+ +
+

Tu Respuesta

+
+
+ + setRespuestaUsuario(e.target.value)} + className="border-2 border-indigo-200 p-3 rounded-lg w-full sm:w-40 text-center text-lg font-mono focus:border-indigo-500 focus:outline-none" + placeholder="Ej: 1.25" + /> +
+ +
+ + +
+
+ + +
+ + {resultado && ( +
+
+
+ {resultado.correcto ? ( + + + + ) : ( + + + + )} +
+
+

+ {resultado.correcto ? '¡Respuesta Correcta!' : 'Respuesta Incorrecta'} +

+

+ {resultado.mensaje} +

+ + {!resultado.correcto && ( +
+

Desglose del cálculo:

+
+

% Cambio en Q = {cambioCantidad} / {cantidadPromedio} = {((cambioCantidad / cantidadPromedio) * 100).toFixed(2)}%

+

% Cambio en P = {cambioPrecio.toFixed(2)} / {precioPromedio.toFixed(2)} = {((cambioPrecio / precioPromedio) * 100).toFixed(2)}%

+

+ Ed = |{((cambioCantidad / cantidadPromedio) / (cambioPrecio / precioPromedio)).toFixed(3)}| = {resultado.valorReal.toFixed(2)} +

+
+
+ )} + + {resultado.correcto && resultado.valorReal > 0 && ( +
+

+ Clasificación: {' '} + {resultado.valorReal > 1 ? ( + Elástica (Ed > 1) + ) : resultado.valorReal < 1 ? ( + Inelástica (Ed < 1) + ) : ( + Unitaria (Ed = 1) + )} +

+

+ {resultado.valorReal > 1 + ? 'La demanda responde proporcionalmente más que el cambio en precio.' + : resultado.valorReal < 1 + ? 'La demanda responde proporcionalmente menos que el cambio en precio.' + : 'La demanda responde exactamente en la misma proporción que el precio.'} +

+
+ )} +
+
+
+ )} + +
+

+ + + + Interpretación de Resultados +

+
+
+

Ed > 1

+

Elástica

+

%ΔQ > %ΔP

+
+
+

Ed = 1

+

Unitaria

+

%ΔQ = %ΔP

+
+
+

Ed < 1

+

Inelástica

+

%ΔQ < %ΔP

+
+
+
+
+ ); +}; + +export default CalculoElasticidadPrecio; diff --git a/frontend/src/components/exercises/modulo2/CambiosEquilibrio.tsx b/frontend/src/components/exercises/modulo2/CambiosEquilibrio.tsx new file mode 100644 index 0000000..0a22ac5 --- /dev/null +++ b/frontend/src/components/exercises/modulo2/CambiosEquilibrio.tsx @@ -0,0 +1,577 @@ +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { GitBranch, ArrowRight, ArrowLeft, CheckCircle2, XCircle, Trophy, RotateCcw, BookOpen, TrendingUp, TrendingDown } from 'lucide-react'; + +interface CambiosEquilibrioProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +type DireccionShock = 'oferta-aumenta' | 'oferta-disminuye' | 'demanda-aumenta' | 'demanda-disminuye'; + +interface Escenario { + id: number; + descripcion: string; + shock: DireccionShock; + curva: 'oferta' | 'demanda'; + direccion: 'aumenta' | 'disminuye'; + cambioPrecio: 'sube' | 'baja'; + cambioCantidad: 'sube' | 'baja'; + explicacion: string; + dificultad: 'facil' | 'medio' | 'dificil'; +} + +const escenarios: Escenario[] = [ + { + id: 1, + descripcion: 'Una nueva tecnología reduce los costos de producción de teléfonos inteligentes.', + shock: 'oferta-aumenta', + curva: 'oferta', + direccion: 'aumenta', + cambioPrecio: 'baja', + cambioCantidad: 'sube', + explicacion: 'La tecnología mejora la productividad, aumentando la oferta. La curva se desplaza a la derecha: el precio baja y la cantidad sube.', + dificultad: 'facil' + }, + { + id: 2, + descripcion: 'Un informe de salud afirma que el café aumenta la longevidad.', + shock: 'demanda-aumenta', + curva: 'demanda', + direccion: 'aumenta', + cambioPrecio: 'sube', + cambioCantidad: 'sube', + explicacion: 'Las preferencias positivas aumentan la demanda. La curva se desplaza a la derecha: el precio y la cantidad suben.', + dificultad: 'facil' + }, + { + id: 3, + descripcion: 'Una plaga de langostas destruye el 30% de la cosecha de granos.', + shock: 'oferta-disminuye', + curva: 'oferta', + direccion: 'disminuye', + cambioPrecio: 'sube', + cambioCantidad: 'baja', + explicacion: 'La plaga reduce la cantidad disponible, disminuyendo la oferta. La curva se desplaza a la izquierda: el precio sube y la cantidad baja.', + dificultad: 'facil' + }, + { + id: 4, + descripcion: 'La economía entra en recesión y el ingreso promedio cae 20% (bien normal).', + shock: 'demanda-disminuye', + curva: 'demanda', + direccion: 'disminuye', + cambioPrecio: 'baja', + cambioCantidad: 'baja', + explicacion: 'Para bienes normales, al bajar el ingreso, disminuye la demanda. La curva se desplaza a la izquierda: el precio y la cantidad bajan.', + dificultad: 'medio' + }, + { + id: 5, + descripcion: 'El gobierno subsidia la compra de autos eléctricos con $10,000.', + shock: 'demanda-aumenta', + curva: 'demanda', + direccion: 'aumenta', + cambioPrecio: 'sube', + cambioCantidad: 'sube', + explicacion: 'El subsidio reduce el precio efectivo para consumidores, aumentando la demanda. El equilibrio se mueve hacia mayor precio y cantidad.', + dificultad: 'medio' + }, + { + id: 6, + descripcion: 'El precio del petróleo (insumo importante) sube un 50%.', + shock: 'oferta-disminuye', + curva: 'oferta', + direccion: 'disminuye', + cambioPrecio: 'sube', + cambioCantidad: 'baja', + explicacion: 'Al subir los costos de insumos, producir es más caro, disminuyendo la oferta. El equilibrio resulta en mayor precio y menor cantidad.', + dificultad: 'dificil' + } +]; + +interface OpcionShock { + value: DireccionShock; + label: string; + descripcion: string; + icon: React.ReactNode; +} + +const opcionesShock: OpcionShock[] = [ + { value: 'oferta-aumenta', label: 'Oferta ↑', descripcion: 'Aumenta', icon: }, + { value: 'oferta-disminuye', label: 'Oferta ↓', descripcion: 'Disminuye', icon: }, + { value: 'demanda-aumenta', label: 'Demanda ↑', descripcion: 'Aumenta', icon: }, + { value: 'demanda-disminuye', label: 'Demanda ↓', descripcion: 'Disminuye', icon: }, +]; + +interface OpcionCambio { + value: 'sube' | 'baja'; + label: string; + icon: React.ReactNode; +} + +const opcionesCambio: OpcionCambio[] = [ + { value: 'sube', label: 'Sube', icon: }, + { value: 'baja', label: 'Baja', icon: }, +]; + +export const CambiosEquilibrio: React.FC = ({ onComplete, ejercicioId: _ejercicioId }) => { + const [escenarioActual, setEscenarioActual] = useState(0); + const [shockSeleccionado, setShockSeleccionado] = useState(null); + const [cambioPrecio, setCambioPrecio] = useState<'sube' | 'baja' | null>(null); + const [cambioCantidad, setCambioCantidad] = useState<'sube' | 'baja' | null>(null); + const [mostrarResultado, setMostrarResultado] = useState(false); + const [esCorrecto, setEsCorrecto] = useState(false); + const [score, setScore] = useState(0); + const [respuestasCorrectas, setRespuestasCorrectas] = useState(0); + const [completado, setCompletado] = useState(false); + const [_startTime] = useState(Date.now()); + + const escenario = escenarios[escenarioActual]; + + const handleVerificar = () => { + if (!shockSeleccionado || !cambioPrecio || !cambioCantidad) return; + + const shockCorrecto = shockSeleccionado === escenario.shock; + const precioCorrecto = cambioPrecio === escenario.cambioPrecio; + const cantidadCorrecta = cambioCantidad === escenario.cambioCantidad; + + const todoCorrecto = shockCorrecto && precioCorrecto && cantidadCorrecta; + + setEsCorrecto(todoCorrecto); + setMostrarResultado(true); + + if (todoCorrecto) { + setScore(prev => prev + Math.round(100 / escenarios.length)); + setRespuestasCorrectas(prev => prev + 1); + } + }; + + const handleSiguiente = () => { + if (escenarioActual < escenarios.length - 1) { + setEscenarioActual(prev => prev + 1); + setShockSeleccionado(null); + setCambioPrecio(null); + setCambioCantidad(null); + setMostrarResultado(false); + } else { + setCompletado(true); + if (onComplete) { + onComplete(score); + } + } + }; + + const handleReiniciar = () => { + setEscenarioActual(0); + setShockSeleccionado(null); + setCambioPrecio(null); + setCambioCantidad(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 getShockColor = (value: DireccionShock) => { + if (value.includes('oferta')) return value.includes('aumenta') ? 'green' : 'red'; + return value.includes('aumenta') ? 'blue' : 'orange'; + }; + + const renderGrafico = () => { + const isOferta = escenario.curva === 'oferta'; + const isAumenta = escenario.direccion === 'aumenta'; + + return ( + + {/* Grid */} + {Array.from({ length: 6 }).map((_, i) => ( + + + + + ))} + + {/* Ejes */} + + + + {/* Labels */} + Q + P + + {/* Curva original */} + {isOferta ? ( + + ) : ( + + )} + + {isOferta ? 'S₁' : 'D₁'} + + + {/* Curva desplazada */} + {mostrarResultado && ( + + {isOferta ? ( + + ) : ( + + )} + + {isOferta ? 'S₂' : 'D₂'} + + + )} + + {/* Punto de equilibrio original */} + + E₁ + + {/* Nuevo equilibrio (si se muestra resultado) */} + {mostrarResultado && ( + + + + E₂ + + + )} + + ); + }; + + if (completado) { + const porcentaje = Math.round((respuestasCorrectas / escenarios.length) * 100); + + return ( + + +

¡Ejercicio Completado!

+

Has analizado cambios en el equilibrio

+ +
+
{porcentaje}%
+

+ {respuestasCorrectas} de {escenarios.length} respuestas correctas +

+
+ + +
+ ); + } + + return ( +
+
+
+
+ +

Cambios en el Equilibrio

+
+
+ + {escenario.dificultad.toUpperCase()} + + + {escenarioActual + 1} de {escenarios.length} + +
+ +
+
+
+

+ Analiza cómo los shocks del mercado afectan el precio y cantidad de equilibrio. +

+
+ +
+
+
+
+ +
+

Escenario {escenario.id}

+

{escenario.descripcion}

+
+
+
+ +
+

1. ¿Qué curva se desplaza y en qué dirección?

+ +
+ {opcionesShock.map((opcion) => { + const isSelected = shockSeleccionado === opcion.value; + const isCorrect = mostrarResultado && opcion.value === escenario.shock; + const color = getShockColor(opcion.value); + + return ( + !mostrarResultado && setShockSeleccionado(opcion.value)} + disabled={mostrarResultado} + whileHover={!mostrarResultado ? { scale: 1.02 } : {}} + whileTap={!mostrarResultado ? { scale: 0.98 } : {}} + className={`p-4 rounded-lg border-2 transition-all flex flex-col items-center gap-2 ${ + isCorrect + ? 'border-green-500 bg-green-50' + : isSelected && mostrarResultado && opcion.value !== escenario.shock + ? 'border-red-500 bg-red-50' + : isSelected + ? `border-${color}-500 bg-${color}-50` + : 'border-gray-200 hover:border-gray-300 hover:bg-gray-50' + }`} + > + {opcion.icon} + + {opcion.label} + + + ); + })} +
+
+ +
+

2. ¿Cómo cambian el precio y la cantidad de equilibrio?

+ +
+
+ +
+ {opcionesCambio.map((opcion) => { + const isSelected = cambioPrecio === opcion.value; + const isCorrect = mostrarResultado && opcion.value === escenario.cambioPrecio; + + return ( + + ); + })} +
+
+ +
+ +
+ {opcionesCambio.map((opcion) => { + const isSelected = cambioCantidad === opcion.value; + const isCorrect = mostrarResultado && opcion.value === escenario.cambioCantidad; + + return ( + + ); + })} +
+
+
+
+ + + {mostrarResultado && ( + +
+ {esCorrecto ? ( + + ) : ( + + )} +
+

+ {esCorrecto ? '¡Correcto!' : 'Algunas respuestas son incorrectas'} +

+

{escenario.explicacion}

+
+
+
+ )} +
+ +
+ {!mostrarResultado ? ( + + ) : ( + + )} +
+
+ +
+

Visualización del Cambio

+ {renderGrafico()} + +
+

Resumen de efectos:

+
+
+ Curva de {escenario.curva}: + + Se {escenario.direccion === 'aumenta' ? 'desplaza a la derecha' : 'desplaza a la izquierda'} + +
+
+ Precio de equilibrio: + + {escenario.cambioPrecio === 'sube' ? '↑ Sube' : '↓ Baja'} + +
+
+ Cantidad de equilibrio: + + {escenario.cambioCantidad === 'sube' ? '↑ Sube' : '↓ Baja'} + +
+
+
+ +
+

+ Recordatorio: +

+
    +
  • • Oferta ↑ → P↓, Q↑
  • +
  • • Oferta ↓ → P↑, Q↓
  • +
  • • Demanda ↑ → P↑, Q↑
  • +
  • • Demanda ↓ → P↓, Q↓
  • +
+
+
+
+ +
+ + +
+ {escenarios.map((_, index) => ( +
+ ))} +
+ + +
+
+ ); +}; + +export default CambiosEquilibrio; diff --git a/frontend/src/components/exercises/modulo2/ConstructorCurvas.tsx b/frontend/src/components/exercises/modulo2/ConstructorCurvas.tsx new file mode 100644 index 0000000..a6b7d5b --- /dev/null +++ b/frontend/src/components/exercises/modulo2/ConstructorCurvas.tsx @@ -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 = ({ onComplete, ejercicioId: _ejercicioId }) => { + const [nivelActual, setNivelActual] = useState(0); + const [puntosDemanda, setPuntosDemanda] = useState([]); + const [puntosOferta, setPuntosOferta] = useState([]); + const [modoActivo, setModoActivo] = useState('demanda'); + const [mensaje, setMensaje] = useState(''); + 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(null); + const [draggedPoint, setDraggedPoint] = useState(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) => { + 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) => { + 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 ( + + ); + }; + + return ( +
+
+
+
+ +

{nivel.titulo}

+
+
+ Nivel {nivelActual + 1} de 3 +
+ +
+ +
+
+

{nivel.descripcion}

+
+ + {nivelActual === 2 && ( +
+ + + Nivel Avanzado: Dibuja ambas curvas. La demanda (azul) con pendiente negativa, + y la oferta (verde) con pendiente positiva. + +
+ )} + +
+
+ + {/* Grid */} + {Array.from({ length: 11 }).map((_, i) => ( + + + + + ))} + + {/* Ejes */} + + + + {/* Labels ejes */} + + Cantidad + + + Precio + + + {/* 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 ( + + handlePointDrag(punto.id, 'demanda')} + onClick={(e) => { + e.stopPropagation(); + eliminarPunto(punto.id, 'demanda'); + }} + /> + + ({punto.x}, {punto.y}) + + + ); + })} + + {/* Puntos Oferta */} + {(nivelActual === 1 || nivelActual === 2) && puntosOferta.map(punto => { + const svg = cartesianToSvg(punto.x, punto.y); + return ( + + handlePointDrag(punto.id, 'oferta')} + onClick={(e) => { + e.stopPropagation(); + eliminarPunto(punto.id, 'oferta'); + }} + /> + + ({punto.x}, {punto.y}) + + + ); + })} + +
+ +
+ {nivelActual === 2 && ( +
+ + +
+ )} + +
+

Puntos colocados:

+ {modoActivo === 'demanda' || nivelActual === 2 ? ( +
+ Demanda: + {puntosDemanda.length} puntos +
+ ) : null} + {(modoActivo === 'oferta' || nivelActual === 2) && ( +
+ Oferta: + {puntosOferta.length} puntos +
+ )} +
+ + {mensaje && ( + + +

{mensaje}

+
+ )} + + {nivelActual < 2 ? ( + + ) : ( + + )} + + + {showSuccess && ( + + +

{nivel.mensajeExito}

+ {nivelActual === 2 && ( +
+ Completado + +
+ )} +
+ )} +
+
+
+
+ ); +}; + +export default ConstructorCurvas; diff --git a/frontend/src/components/exercises/modulo2/ControlesVidaReal.tsx b/frontend/src/components/exercises/modulo2/ControlesVidaReal.tsx new file mode 100644 index 0000000..6a07f75 --- /dev/null +++ b/frontend/src/components/exercises/modulo2/ControlesVidaReal.tsx @@ -0,0 +1,509 @@ +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Building2, Wallet, AlertCircle, CheckCircle2, BookOpen, TrendingUp, Users, MapPin } from 'lucide-react'; + +interface ControlesVidaRealProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface CasoEstudio { + id: string; + titulo: string; + categoria: 'vivienda' | 'laboral' | 'agricola'; + ubicacion: string; + anio: string; + contexto: string; + intervencion: string; + resultados: string[]; + lecciones: string[]; + datos: { + antes: { precio: number; cantidad: number }; + despues: { precio: number; cantidad: number }; + }; + icono: React.ReactNode; + color: string; +} + +const casosEstudio: CasoEstudio[] = [ + { + id: 'nyc-rent', + titulo: "Rent Control en Nueva York", + categoria: 'vivienda', + ubicacion: "Nueva York, USA", + anio: "1947-presente", + contexto: "Nueva York implementó controles de alquiler después de la Segunda Guerra Mundial para proteger a los inquilinos. Actualmente afecta a aproximadamente 1 millón de apartamentos.", + intervencion: "Los alquileres de apartamentos antiguos están regulados y no pueden aumentar más allá de ciertos límites establecidos por la Junta de Alquileres.", + resultados: [ + "Reducción de la oferta de vivienda a largo plazo", + "Deterioro de la calidad de edificios regulados", + "Mercado paralelo de 'pagos clave'", + "Beneficios para inquilinos antiguos, no para nuevos" + ], + lecciones: [ + "Los controles benefician a quienes ya tienen vivienda", + "Desincentivan la construcción de nueva vivienda", + "Crean ineficiencias en la asignación de recursos", + "Difícil de eliminar una vez implementado" + ], + datos: { + antes: { precio: 1000, cantidad: 100 }, + despues: { precio: 800, cantidad: 85 } + }, + icono: , + color: "blue" + }, + { + id: 'venezuela-gasolina', + titulo: "Gasolina Subsidiada en Venezuela", + categoria: 'agricola', + ubicacion: "Venezuela", + anio: "1976-2019", + contexto: "Venezuela mantuvo durante décadas el precio de la gasolina casi gratis (menos de $0.01 por litro) debido a subsidios gubernamentales masivos.", + intervencion: "Precio máximo artificial mantenido por subsidios estatales, sin relación con costos reales de producción.", + resultados: [ + "Contrabando masivo a países vecinos", + "Colapso de la infraestructura de refinación", + "Desabastecimiento crónico en 2019", + "Pérdida fiscal insostenible para el Estado" + ], + lecciones: [ + "Los precios deben reflejar costos reales", + "Subsidios masivos son fiscalmente insostenibles", + "Crearán mercados negros inevitablemente", + "La transición es extremadamente difícil" + ], + datos: { + antes: { precio: 0.50, cantidad: 100 }, + despues: { precio: 0.01, cantidad: 60 } + }, + icono: , + color: "red" + }, + { + id: 'seattle-wage', + titulo: "Salario Mínimo en Seattle", + categoria: 'laboral', + ubicacion: "Seattle, USA", + anio: "2014-2019", + contexto: "Seattle aumentó gradualmente el salario mínimo de $9.47 a $15/hora entre 2014 y 2017, siendo pionera en Estados Unidos.", + intervencion: "Incremento progresivo del salario mínimo municipal hasta $15/hora, con ritmo diferenciado por tamaño de empresa.", + resultados: [ + "Reducción en horas trabajadas por empleados de bajos ingresos", + "Pérdida neta de ingresos para algunos trabajadores", + "Beneficio para trabajadores que mantuvieron empleo", + "Aumento de precios en restaurantes" + ], + lecciones: [ + "Efectos no son uniformes en todos los trabajadores", + "El ajuste puede ocurrir por horas, no solo empleos", + "La elasticidad de la demanda laboral importa", + "Estudios rigurosos muestran efectos mixtos" + ], + datos: { + antes: { precio: 9.47, cantidad: 100 }, + despues: { precio: 15.00, cantidad: 92 } + }, + icono: , + color: "amber" + }, + { + id: 'eu-agricultura', + titulo: "Política Agrícola de la UE", + categoria: 'agricola', + ubicacion: "Unión Europea", + anio: "1962-presente", + contexto: "La Política Agrícola Común (PAC) estableció precios de intervención para garantizar ingresos a los agricultores europeos.", + intervencion: "Precios mínimos garantizados para productos agrícolas clave, con compras gubernamentales del excedente.", + resultados: [ + "Superávits masivos de productos lácteos y cereales", + "Montañas de mantequilla y lagos de vino", + "Gasto fiscal considerable", + "Reformas parciales desde los 90" + ], + lecciones: [ + "Los precios de soporte generan excedentes", + "El gobierno termina comprando producción no deseada", + "Crean distorsiones en el comercio internacional", + "Las reformas son políticamente difíciles" + ], + datos: { + antes: { precio: 100, cantidad: 80 }, + despues: { precio: 130, cantidad: 110 } + }, + icono: , + color: "green" + } +]; + +export const ControlesVidaReal: React.FC = ({ onComplete, ejercicioId: _ejercicioId }) => { + const [casoActivo, setCasoActivo] = useState(null); + const [respuestas, setRespuestas] = useState>({}); + const [mostrarResultado, setMostrarResultado] = useState(false); + const [casosCompletados, setCasosCompletados] = useState>(new Set()); + const [puntuacion, setPuntuacion] = useState(0); + + const seleccionarCaso = (caso: CasoEstudio) => { + setCasoActivo(caso); + setMostrarResultado(false); + }; + + const responderPregunta = (respuesta: string) => { + if (!casoActivo) return; + + setRespuestas(prev => ({ ...prev, [casoActivo.id]: respuesta })); + setMostrarResultado(true); + + if (!casosCompletados.has(casoActivo.id)) { + setCasosCompletados(prev => new Set([...prev, casoActivo.id])); + setPuntuacion(prev => prev + 25); + + if (casosCompletados.size + 1 >= 4) { + setTimeout(() => { + onComplete?.(100); + }, 2000); + } + } + }; + + const getColorClass = (color: string) => { + const colors: Record = { + blue: { bg: 'bg-blue-600', border: 'border-blue-400', text: 'text-blue-800', light: 'bg-blue-50' }, + red: { bg: 'bg-red-600', border: 'border-red-400', text: 'text-red-800', light: 'bg-red-50' }, + amber: { bg: 'bg-amber-600', border: 'border-amber-400', text: 'text-amber-800', light: 'bg-amber-50' }, + green: { bg: 'bg-green-600', border: 'border-green-400', text: 'text-green-800', light: 'bg-green-50' } + }; + return colors[color] || colors.blue; + }; + + return ( +
+ {/* Header */} +
+
+
+
+ +
+
+

Controles de Precio en la Vida Real

+

Estudia casos históricos y sus consecuencias reales

+
+
+
+ Progreso: {casosCompletados.size}/4 +
+ +
+
+
+
+ + {!casoActivo ? ( + /* Grid de casos de estudio */ +
+ {casosEstudio.map((caso) => { + const colors = getColorClass(caso.color); + const completado = casosCompletados.has(caso.id); + + return ( + seleccionarCaso(caso)} + className={`p-5 text-left rounded-xl border-2 transition-all ${ + completado + ? `${colors.light} ${colors.border}` + : 'bg-white border-gray-200 hover:border-indigo-300' + }`} + > +
+
+ {caso.icono} +
+ {completado && ( + + )} +
+ +

{caso.titulo}

+ +
+ + + {caso.ubicacion} + + + + {caso.anio} + +
+ +

{caso.contexto}

+ +
+ + {caso.categoria === 'vivienda' && '🏠 Vivienda'} + {caso.categoria === 'laboral' && '💼 Laboral'} + {caso.categoria === 'agricola' && '🌾 Agrícola'} + +
+
+ ); + })} +
+ ) : ( + /* Vista detallada del caso */ +
+ {/* Navegación */} + + + {(() => { + const colors = getColorClass(casoActivo.color); + + return ( + <> + {/* Header del caso */} +
+
+
+ {casoActivo.icono} +
+
+

{casoActivo.titulo}

+
+ + + {casoActivo.ubicacion} + + + + {casoActivo.anio} + +
+
+
+ +

{casoActivo.contexto}

+
+ +
+ {/* Columna izquierda: Información */} +
+ {/* Intervención */} +
+

+ + Intervención +

+

{casoActivo.intervencion}

+
+ + {/* Resultados */} +
+

+ + Resultados Observados +

+
    + {casoActivo.resultados.map((resultado, idx) => ( + + + {resultado} + + ))} +
+
+ + {/* Lecciones */} +
+

+ + Lecciones Aprendidas +

+
    + {casoActivo.lecciones.map((leccion, idx) => ( + + 💡 + {leccion} + + ))} +
+
+
+ + {/* Columna derecha: Visualización y pregunta */} +
+ {/* Visualización simple */} +
+

Evolución del Mercado

+ +
+
+ Antes +
+
${casoActivo.datos.antes.precio}
+
{casoActivo.datos.antes.cantidad} unidades
+
+
+ +
+
+
+ +
+ Después +
+
${casoActivo.datos.despues.precio}
+
{casoActivo.datos.despues.cantidad} unidades
+
+
+
+
+ + {/* Pregunta de comprensión */} + + {!mostrarResultado ? ( + +

+ ¿Cuál es la principal consecuencia económica observada? +

+ +
+ + + + + +
+
+ ) : ( + +
+ {respuestas[casoActivo.id] === 'desajuste' ? ( + + ) : ( + + )} +

+ {respuestas[casoActivo.id] === 'desajuste' ? '¡Correcto!' : 'Revisa la respuesta'} +

+
+ +

+ {respuestas[casoActivo.id] === 'desajuste' + ? 'Los controles de precio siempre crean desajustes: precios máximos generan escasez, precios mínimos generan superávits.' + : 'Recuerda: los controles de precio fijados fuera del equilibrio siempre crean desajustes entre oferta y demanda, generando ineficiencias.' + } +

+ + +
+ )} +
+
+
+ + ); + })()} +
+ )} + + {/* Barra de progreso */} +
+
+ Tu progreso + {casosCompletados.size} de 4 casos completados +
+
+ +
+ + {casosCompletados.size >= 4 && ( + +
+ + ¡Felicidades! Has completado todos los casos +
+

+ Ahora comprendes mejor las consecuencias reales de los controles de precio en diferentes contextos. +

+
+ )} +
+
+ ); +}; + +export default ControlesVidaReal; diff --git a/frontend/src/components/exercises/modulo2/CurvaDemandaConstructor.tsx b/frontend/src/components/exercises/modulo2/CurvaDemandaConstructor.tsx new file mode 100644 index 0000000..c8056ec --- /dev/null +++ b/frontend/src/components/exercises/modulo2/CurvaDemandaConstructor.tsx @@ -0,0 +1,368 @@ +import React, { useState, useRef, useCallback } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { LineChart, Check, X, RotateCcw, Trophy, HelpCircle } from 'lucide-react'; + +interface Punto { + x: number; + y: number; + id: string; +} + +interface CurvaDemandaConstructorProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +const GRID_SIZE = 350; +const PADDING = 50; +const MAX_PRECIO = 100; +const MAX_CANTIDAD = 100; + +export const CurvaDemandaConstructor: React.FC = ({ + ejercicioId: _ejercicioId, + onComplete +}) => { + const [puntos, setPuntos] = useState([]); + const [mensaje, setMensaje] = useState(''); + const [showSuccess, setShowSuccess] = useState(false); + const [score, setScore] = useState(0); + const [intentos, setIntentos] = useState(0); + const svgRef = useRef(null); + + 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) => { + if (showSuccess) 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 (puntos.length >= 5) { + setMensaje('Máximo 5 puntos permitidos'); + return; + } + + const newPoint: Punto = { + x: cartesian.x, + y: cartesian.y, + id: `point-${Date.now()}-${Math.random()}` + }; + + setPuntos(prev => [...prev, newPoint]); + setMensaje(''); + }; + + 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 = () => { + setIntentos(prev => prev + 1); + + if (puntos.length < 2) { + setMensaje('Necesitas al menos 2 puntos para trazar una curva de demanda'); + return; + } + + const pendiente = calcularPendiente(puntos); + if (pendiente === null) return; + + if (pendiente >= 0) { + setMensaje('¡Incorrecto! La curva de demanda debe tener pendiente NEGATIVA (bajar de izquierda a derecha)'); + return; + } + + // Calcular puntuación basada en intentos + let puntuacion = 100; + if (intentos >= 1) puntuacion -= 20; + if (intentos >= 2) puntuacion -= 20; + puntuacion = Math.max(puntuacion, 40); + + setScore(puntuacion); + setMensaje(''); + setShowSuccess(true); + + setTimeout(() => { + if (onComplete) { + onComplete(puntuacion); + } + }, 2000); + }; + + const reiniciar = () => { + setPuntos([]); + setMensaje(''); + setShowSuccess(false); + setScore(0); + setIntentos(0); + }; + + const eliminarPunto = (id: string) => { + setPuntos(prev => prev.filter(p => p.id !== id)); + }; + + const renderLineaCurva = () => { + 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 ( + + ); + }; + + return ( +
+
+
+
+ +

Constructor de Curva de Demanda

+
+ +
+

+ Haz clic en el gráfico para colocar puntos que formen una curva de demanda con pendiente negativa. + La demanda debe descender de izquierda a derecha. +

+
+ +
+ + + Instrucción: Coloca al menos 2 puntos formando una línea descendente. + Haz clic en un punto para eliminarlo. + +
+ +
+
+ + {/* Grid */} + {Array.from({ length: 11 }).map((_, i) => ( + + + + + ))} + + {/* Ejes */} + + + + {/* Labels ejes */} + + Cantidad (Q) + + + Precio (P) + + + {/* Marcas de ejes */} + {Array.from({ length: 6 }).map((_, i) => ( + + + {i * 20} + + + {i * 20} + + + ))} + + {/* Curva */} + {renderLineaCurva()} + + {/* Puntos */} + {puntos.map(punto => { + const svg = cartesianToSvg(punto.x, punto.y); + return ( + + { + e.stopPropagation(); + eliminarPunto(punto.id); + }} + /> + + ({punto.x}, {punto.y}) + + + ); + })} + + {/* Flecha indicando pendiente descendente */} + {puntos.length >= 2 && ( + + + + + + + + Pendiente - + + + )} + +
+ +
+
+

Progreso

+
+
+ Puntos colocados: + {puntos.length} +
+
+ Intentos: + {intentos} +
+ {showSuccess && ( +
+ Puntuación: + {score}/100 +
+ )} +
+
+ + {mensaje && ( + + {mensaje.includes('Correcto') || mensaje.includes('Excelente') ? ( + + ) : ( + + )} +

{mensaje}

+
+ )} + + {!showSuccess && ( + + )} + + + {showSuccess && ( + + +

¡Excelente!

+

+ Has trazado correctamente una curva de demanda con pendiente negativa. +

+
+ {score}/100 +
+
+ )} +
+
+
+
+ ); +}; + +export default CurvaDemandaConstructor; diff --git a/frontend/src/components/exercises/modulo2/CurvaOfertaConstructor.tsx b/frontend/src/components/exercises/modulo2/CurvaOfertaConstructor.tsx new file mode 100644 index 0000000..8658266 --- /dev/null +++ b/frontend/src/components/exercises/modulo2/CurvaOfertaConstructor.tsx @@ -0,0 +1,451 @@ +import React, { useState, useRef, useCallback } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { TrendingUp, Check, X, RotateCcw, Trophy, HelpCircle } from 'lucide-react'; + +interface Punto { + x: number; + y: number; + id: string; +} + +interface CurvaOfertaConstructorProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +const GRID_SIZE = 350; +const PADDING = 50; +const MAX_PRECIO = 100; +const MAX_CANTIDAD = 100; + +export const CurvaOfertaConstructor: React.FC = ({ + onComplete, + ejercicioId: _ejercicioId +}) => { + const [puntos, setPuntos] = useState([]); + const [mensaje, setMensaje] = useState(''); + const [showSuccess, setShowSuccess] = useState(false); + const [score, setScore] = useState(0); + const [intentos, setIntentos] = useState(0); + const [completado, setCompletado] = useState(false); + + const svgRef = useRef(null); + const [draggedPoint, setDraggedPoint] = useState(null); + + 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) => { + if (draggedPoint || completado) 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 (puntos.length >= 5) { + setMensaje('Máximo 5 puntos permitidos'); + return; + } + + const newPoint: Punto = { + x: cartesian.x, + y: cartesian.y, + id: `point-${Date.now()}-${Math.random()}` + }; + + setPuntos(prev => [...prev, newPoint]); + setMensaje(''); + }; + + const handlePointDrag = (pointId: string) => { + setDraggedPoint(pointId); + }; + + const handlePointMove = (e: React.MouseEvent) => { + 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); + + setPuntos(prev => + prev.map(p => p.id === draggedPoint ? { ...p, x: cartesian.x, y: cartesian.y } : p) + ); + }; + + const handlePointUp = () => { + setDraggedPoint(null); + }; + + const calcularPendiente = (): 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 = () => { + if (puntos.length < 2) { + setMensaje('Necesitas al menos 2 puntos para trazar la curva de oferta'); + return; + } + + const pendiente = calcularPendiente(); + if (pendiente === null) return; + + setIntentos(prev => prev + 1); + + if (pendiente <= 0) { + setMensaje('¡Incorrecto! La curva de oferta debe tener pendiente POSITIVA (subir de izquierda a derecha)'); + + // Penalización por intentos + if (intentos >= 2) { + setScore(Math.max(0, 60 - (intentos - 2) * 10)); + } + return; + } + + // Curva correcta + const puntosBonus = puntos.length >= 3 ? 10 : 0; + const intentosBonus = intentos === 0 ? 30 : intentos === 1 ? 20 : 10; + const puntajeFinal = Math.min(100, 60 + puntosBonus + intentosBonus); + + setScore(puntajeFinal); + setMensaje(''); + setShowSuccess(true); + setCompletado(true); + + setTimeout(() => { + if (onComplete) { + onComplete(puntajeFinal); + } + }, 2000); + }; + + const reiniciar = () => { + setPuntos([]); + setMensaje(''); + setShowSuccess(false); + setScore(0); + setIntentos(0); + setCompletado(false); + }; + + const eliminarPunto = (id: string) => { + setPuntos(prev => prev.filter(p => p.id !== id)); + }; + + const renderLineaCurva = () => { + 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 ( + + ); + }; + + // Puntos guía esperados para la curva de oferta + const puntosGuia = [ + { x: 20, y: 20 }, + { x: 50, y: 40 }, + { x: 80, y: 70 } + ]; + + return ( +
+
+
+
+ +

Constructor de Curva de Oferta

+
+
+ {completado && ( + {score} pts + )} + +
+
+

+ Coloca puntos en el gráfico para trazar una curva de oferta con pendiente POSITIVA. + Recuerda: a mayor precio, mayor cantidad ofrecida. +

+
+ +
+ + + Instrucciones: Haz clic en el gráfico para colocar puntos. + La curva debe subir de izquierda a derecha (pendiente positiva). + Arrastra los puntos para ajustar su posición. Haz clic en un punto para eliminarlo. + +
+ +
+
+ + {/* Grid */} + {Array.from({ length: 11 }).map((_, i) => ( + + + + + ))} + + {/* Ejes */} + + + + {/* Labels ejes */} + + Cantidad (Q) + + + Precio (P) + + + {/* Valores en ejes */} + {[0, 25, 50, 75, 100].map((val) => ( + + + {val} + + + {val} + + + ))} + + {/* Línea de tendencia esperada (dotted, muy sutil) */} + {!completado && ( + + )} + + {/* Curva */} + {renderLineaCurva()} + + {/* Puntos */} + {puntos.map((punto, index) => { + const svg = cartesianToSvg(punto.x, punto.y); + return ( + + handlePointDrag(punto.id)} + onClick={(e) => { + e.stopPropagation(); + eliminarPunto(punto.id); + }} + /> + + P{index + 1}({punto.x}, {punto.y}) + + + ); + })} + + {/* Etiqueta S */} + {puntos.length >= 2 && ( + + S + + )} + +
+ +
+
+

Progreso

+
+
+ Puntos colocados: + {puntos.length}/5 +
+
+ +
+ {intentos > 0 && ( +
+ Intentos: {intentos} +
+ )} +
+
+ +
+

Recuerda:

+
    +
  • + + La oferta tiene pendiente POSITIVA +
  • +
  • + + Subir de izquierda a derecha ↗️ +
  • +
  • + + Precio ↑ → Cantidad ↑ +
  • +
+
+ + {mensaje && ( + + +

{mensaje}

+
+ )} + + + + + {showSuccess && ( + + +

¡Excelente!

+

+ Has trazado correctamente la curva de oferta +

+

{score} puntos

+
+ )} +
+ + {completado && ( +
+ Puntuación: +
    +
  • • Base: 60 puntos
  • +
  • • +3 puntos: 10 pts
  • +
  • • Primer intento: 30 pts
  • +
+
+ )} +
+
+
+ ); +}; + +export default CurvaOfertaConstructor; diff --git a/frontend/src/components/exercises/modulo2/DemandaIndividualVsMercado.tsx b/frontend/src/components/exercises/modulo2/DemandaIndividualVsMercado.tsx new file mode 100644 index 0000000..fbc5e83 --- /dev/null +++ b/frontend/src/components/exercises/modulo2/DemandaIndividualVsMercado.tsx @@ -0,0 +1,260 @@ +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Users, Plus, Check, X, Trophy, RotateCcw, ArrowRight } from 'lucide-react'; + +interface DemandaIndividualVsMercadoProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Consumidor { + id: string; + nombre: string; + cantidad: number; +} + +const consumidores: Consumidor[] = [ + { id: 'ana', nombre: 'Ana', cantidad: 5 }, + { id: 'beto', nombre: 'Beto', cantidad: 3 }, + { id: 'carlos', nombre: 'Carlos', cantidad: 7 }, + { id: 'diana', nombre: 'Diana', cantidad: 4 }, +]; + +export const DemandaIndividualVsMercado: React.FC = ({ + ejercicioId: _ejercicioId, + onComplete +}) => { + const [respuestaUsuario, setRespuestaUsuario] = useState(''); + const [mostrarResultado, setMostrarResultado] = useState(false); + const [score, setScore] = useState(0); + const [intentos, setIntentos] = useState(0); + const [completado, setCompletado] = useState(false); + const [mostrarExplicacion, setMostrarExplicacion] = useState(false); + + const demandaTotal = consumidores.reduce((sum, c) => sum + c.cantidad, 0); + + const validarRespuesta = () => { + if (respuestaUsuario === '') { + alert('Por favor ingresa tu respuesta'); + return; + } + + setIntentos(prev => prev + 1); + setMostrarResultado(true); + + const respuestaNum = parseInt(respuestaUsuario); + const esCorrecta = respuestaNum === demandaTotal; + + let puntuacion = esCorrecta ? 100 : 0; + // Si está cerca (±2), dar puntuación parcial + if (!esCorrecta && Math.abs(respuestaNum - demandaTotal) <= 2) { + puntuacion = 50; + } + // Penalización por intentos + if (intentos >= 1) puntuacion -= 10; + if (intentos >= 2) puntuacion -= 10; + puntuacion = Math.max(puntuacion, 10); + + setScore(puntuacion); + + if (esCorrecta) { + setCompletado(true); + } + }; + + const handleFinalizar = () => { + if (onComplete) { + onComplete(score); + } + }; + + const reiniciar = () => { + setRespuestaUsuario(''); + setMostrarResultado(false); + setMostrarExplicacion(false); + setScore(0); + setCompletado(false); + }; + + const handleSiguienteIntento = () => { + setRespuestaUsuario(''); + setMostrarResultado(false); + }; + + return ( +
+
+
+
+ +

Demanda Individual vs. Mercado

+
+ +
+

+ La demanda de mercado es la suma horizontal de todas las demandas individuales. + Calcula la cantidad total demandada sumando las cantidades de todos los consumidores. +

+
+ +
+ {/* Tarjetas de consumidores */} +
+

Demandas Individuales

+ {consumidores.map((consumidor, index) => ( + +
+
+ {consumidor.nombre[0]} +
+ {consumidor.nombre} +
+
+ Demanda: + {consumidor.cantidad} unidades +
+
+ ))} +
+ + {/* Visualización de la suma */} +
+

Suma de Demandas

+
+ {consumidores.map((consumidor, index) => ( + + + {consumidor.cantidad} + + {index < consumidores.length - 1 && ( + + )} + + ))} +
+ +
+ +
+ setRespuestaUsuario(e.target.value)} + disabled={mostrarResultado} + placeholder="¿Cuánto suma?" + className={`flex-1 px-4 py-3 border-2 rounded-lg text-center text-lg font-bold focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all ${ + mostrarResultado + ? parseInt(respuestaUsuario) === demandaTotal + ? 'border-green-500 bg-green-50 text-green-700' + : 'border-red-500 bg-red-50 text-red-700' + : 'border-gray-300 focus:border-blue-500' + }`} + /> + unidades +
+
+
+
+ + + {mostrarResultado && ( + +
+
+ {parseInt(respuestaUsuario) === demandaTotal ? ( + + ) : ( + + )} +
+

+ {parseInt(respuestaUsuario) === demandaTotal + ? '¡Correcto! Has calculado correctamente la demanda de mercado' + : 'Respuesta incorrecta'} +

+

+ {parseInt(respuestaUsuario) === demandaTotal + ? `La demanda total es ${demandaTotal} unidades (${consumidores.map(c => c.cantidad).join(' + ')}).` + : `La respuesta correcta es ${demandaTotal} unidades. Sumaste ${respuestaUsuario}.`} +

+
+
+
+ + Puntuación: {score}/100 +
+
+
+ )} +
+ +
+ {!mostrarResultado ? ( + + ) : ( + <> + {!completado && ( + + )} + + + )} +
+ + {intentos > 0 && ( +
+ Intentos realizados: {intentos} +
+ )} +
+ ); +}; + +export default DemandaIndividualVsMercado; diff --git a/frontend/src/components/exercises/modulo2/DesplazamientoVsMovimiento.tsx b/frontend/src/components/exercises/modulo2/DesplazamientoVsMovimiento.tsx new file mode 100644 index 0000000..6d162c8 --- /dev/null +++ b/frontend/src/components/exercises/modulo2/DesplazamientoVsMovimiento.tsx @@ -0,0 +1,336 @@ +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { ArrowRightLeft, MoveHorizontal, DollarSign, Check, X, Trophy, RotateCcw } from 'lucide-react'; + +interface DesplazamientoVsMovimientoProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Escenario { + id: number; + situacion: string; + tipo: 'movimiento' | 'desplazamiento'; + explicacion: string; + pista: string; +} + +const escenarios: Escenario[] = [ + { + id: 1, + situacion: "El precio de las manzanas sube de $2 a $4 por kilo. ¿Qué ocurre con la cantidad demandada de manzanas?", + tipo: 'movimiento', + explicacion: "Cambio en el precio del propio bien = MOVIMIENTO a lo largo de la curva. La cantidad demandada disminuye.", + pista: "¿Cambió el precio del propio bien o un factor externo?" + }, + { + id: 2, + situacion: "Los ingresos de los consumidores aumentan. ¿Qué ocurre con la demanda de restaurantes?", + tipo: 'desplazamiento', + explicacion: "Cambio en ingresos = DESPLAZAMIENTO de la curva. La demanda aumenta (la curva se mueve a la derecha).", + pista: "El ingreso es un factor externo que desplaza toda la curva." + }, + { + id: 3, + situacion: "Una heladería baja sus precios en verano. ¿Qué ocurre con la cantidad demandada de helados?", + tipo: 'movimiento', + explicacion: "Cambio en el precio del bien = MOVIMIENTO a lo largo de la curva. La cantidad demandada aumenta.", + pista: "La heladería cambió sus precios, no un factor externo." + }, + { + id: 4, + situacion: "Se espera que el precio de los autos suba el próximo mes. ¿Qué ocurre con la demanda de autos hoy?", + tipo: 'desplazamiento', + explicacion: "Expectativas futuras = DESPLAZAMIENTO de la curva. La gente compra antes, aumentando la demanda actual.", + pista: "Las expectativas son un factor que desplaza la curva, no el precio actual." + }, + { + id: 5, + situacion: "Una campaña publicitaria exitosa promueve el consumo de aguacates. ¿Qué ocurre con la demanda?", + tipo: 'desplazamiento', + explicacion: "Cambio en gustos/preferencias = DESPLAZAMIENTO de la curva. La demanda aumenta.", + pista: "La publicidad afecta los gustos, desplazando la curva." + }, + { + id: 6, + situacion: "El precio de las entradas al cine baja un 50%. ¿Qué ocurre con la cantidad demandada de entradas?", + tipo: 'movimiento', + explicacion: "Cambio en el precio del propio bien = MOVIMIENTO a lo largo de la curva. Más personas van al cine.", + pista: "¿El precio del bien mismo cambió? Entonces es un movimiento." + } +]; + +export const DesplazamientoVsMovimiento: React.FC = ({ + ejercicioId: _ejercicioId, + onComplete +}) => { + const [escenarioActual, setEscenarioActual] = useState(0); + const [respuestaSeleccionada, setRespuestaSeleccionada] = useState<'movimiento' | 'desplazamiento' | null>(null); + const [mostrarFeedback, setMostrarFeedback] = useState(false); + const [respuestasCorrectas, setRespuestasCorrectas] = useState(0); + const [completado, setCompletado] = useState(false); + const [mostrarPista, setMostrarPista] = useState(false); + const [usoPistas, setUsoPistas] = useState(0); + + const escenario = escenarios[escenarioActual]; + const esCorrecta = respuestaSeleccionada === escenario.tipo; + + const handleSeleccionar = (tipo: 'movimiento' | 'desplazamiento') => { + if (mostrarFeedback) return; + setRespuestaSeleccionada(tipo); + }; + + const handleValidar = () => { + if (respuestaSeleccionada === null) return; + + setMostrarFeedback(true); + if (esCorrecta) { + setRespuestasCorrectas(prev => prev + 1); + } + }; + + const handleSiguiente = () => { + if (escenarioActual < escenarios.length - 1) { + setEscenarioActual(prev => prev + 1); + setRespuestaSeleccionada(null); + setMostrarFeedback(false); + setMostrarPista(false); + } else { + setCompletado(true); + // Calcular puntuación + let puntuacion = Math.round((respuestasCorrectas + (esCorrecta ? 1 : 0)) / escenarios.length * 100); + // Penalización por uso de pistas + puntuacion -= usoPistas * 10; + puntuacion = Math.max(puntuacion, 0); + + setTimeout(() => { + if (onComplete) { + onComplete(puntuacion); + } + }, 2000); + } + }; + + const handleMostrarPista = () => { + setMostrarPista(true); + setUsoPistas(prev => prev + 1); + }; + + const reiniciar = () => { + setEscenarioActual(0); + setRespuestaSeleccionada(null); + setMostrarFeedback(false); + setRespuestasCorrectas(0); + setCompletado(false); + setMostrarPista(false); + setUsoPistas(0); + }; + + if (completado) { + const puntuacionFinal = Math.max(0, Math.round((respuestasCorrectas / escenarios.length) * 100) - usoPistas * 10); + return ( +
+ + + +

¡Ejercicio Completado!

+

+ Has identificado correctamente {respuestasCorrectas} de {escenarios.length} situaciones +

+ {usoPistas > 0 && ( +

Pistas utilizadas: {usoPistas} (-{usoPistas * 10} pts)

+ )} +
{puntuacionFinal}/100
+

Puntuación final

+ +
+ ); + } + + return ( +
+
+
+

Desplazamiento vs. Movimiento

+ Situación {escenarioActual + 1} de {escenarios.length} +
+
+ +
+
+ +
+ {/* Panel izquierdo: Situación */} +
+

+ + Situación +

+

{escenario.situacion}

+ + {!mostrarPista && !mostrarFeedback && ( + + )} + + {mostrarPista && ( + +

+ Pista: {escenario.pista} +

+
+ )} +
+ + {/* Panel derecho: Opciones */} +
+

¿Qué tipo de cambio ocurre?

+ + handleSeleccionar('movimiento')} + disabled={mostrarFeedback} + whileHover={!mostrarFeedback ? { scale: 1.02 } : {}} + whileTap={!mostrarFeedback ? { scale: 0.98 } : {}} + className={`w-full p-4 rounded-xl border-2 text-left transition-all ${ + respuestaSeleccionada === 'movimiento' + ? mostrarFeedback + ? escenario.tipo === 'movimiento' + ? 'border-green-500 bg-green-50' + : 'border-red-500 bg-red-50' + : 'border-blue-500 bg-blue-50' + : mostrarFeedback && escenario.tipo === 'movimiento' + ? 'border-green-500 bg-green-50' + : 'border-gray-200 hover:border-blue-300 bg-white' + }`} + > +
+
+ +
+
+

Movimiento a lo largo de la curva

+

+ Cambio en la cantidad demandada debido a un cambio en el precio del propio bien +

+
+
+
+ + handleSeleccionar('desplazamiento')} + disabled={mostrarFeedback} + whileHover={!mostrarFeedback ? { scale: 1.02 } : {}} + whileTap={!mostrarFeedback ? { scale: 0.98 } : {}} + className={`w-full p-4 rounded-xl border-2 text-left transition-all ${ + respuestaSeleccionada === 'desplazamiento' + ? mostrarFeedback + ? escenario.tipo === 'desplazamiento' + ? 'border-green-500 bg-green-50' + : 'border-red-500 bg-red-50' + : 'border-blue-500 bg-blue-50' + : mostrarFeedback && escenario.tipo === 'desplazamiento' + ? 'border-green-500 bg-green-50' + : 'border-gray-200 hover:border-blue-300 bg-white' + }`} + > +
+
+ +
+
+

Desplazamiento de la curva

+

+ Cambio en la demanda debido a factores externos (ingresos, gustos, expectativas, etc.) +

+
+
+
+
+
+ + + {mostrarFeedback && ( + +
+
+ {esCorrecta ? ( + + ) : ( + + )} +
+

+ {esCorrecta ? '¡Correcto!' : 'Incorrecto'} +

+

{escenario.explicacion}

+
+
+
+
+ )} +
+ +
+ {!mostrarFeedback ? ( + + ) : ( + + )} +
+
+ ); +}; + +export default DesplazamientoVsMovimiento; diff --git a/frontend/src/components/exercises/modulo2/ElasticidadElasticaInelastica.tsx b/frontend/src/components/exercises/modulo2/ElasticidadElasticaInelastica.tsx new file mode 100644 index 0000000..f640883 --- /dev/null +++ b/frontend/src/components/exercises/modulo2/ElasticidadElasticaInelastica.tsx @@ -0,0 +1,385 @@ +import React, { useState } from 'react'; + +interface EscenarioElasticidad { + id: number; + producto: string; + descripcion: string; + precioInicial: number; + precioFinal: number; + cantidadInicial: number; + cantidadFinal: number; + categoriaCorrecta: 'elastica' | 'inelastica' | 'unitaria'; + explicacion: string; +} + +const escenarios: EscenarioElasticidad[] = [ + { + id: 1, + producto: 'Medicamentos esenciales', + descripcion: 'El precio de antibióticos aumenta un 20% debido a escasez.', + precioInicial: 100, + precioFinal: 120, + cantidadInicial: 10000, + cantidadFinal: 9500, + categoriaCorrecta: 'inelastica', + explicacion: 'Los medicamentos esenciales tienen demanda inelástica porque son necesarios para la salud y no tienen sustitutos cercanos. La cantidad demandada disminuye muy poco (5%) a pesar del gran aumento de precio (20%).' + }, + { + id: 2, + producto: 'Boletos de cine de lujo', + descripcion: 'Los cines VIP aumentan sus precios un 15%.', + precioInicial: 200, + precioFinal: 230, + cantidadInicial: 5000, + cantidadFinal: 3000, + categoriaCorrecta: 'elastica', + explicacion: 'El entretenimiento de lujo es elástico porque es un bien discrecional con muchos sustitutos (streaming, cines regulares, otras actividades). La cantidad demandada cae drásticamente (40%) ante un aumento moderado de precio.' + }, + { + id: 3, + producto: 'Gasolina', + descripcion: 'El precio de la gasolina sube un 10% por impuestos.', + precioInicial: 50, + precioFinal: 55, + cantidadInicial: 100000, + cantidadFinal: 95000, + categoriaCorrecta: 'inelastica', + explicacion: 'La gasolina tiene demanda inelástica a corto plazo porque es necesaria para el transporte y muchos no pueden cambiar sus hábitos inmediatamente. La cantidad solo baja 5% pese al aumento de 10%.' + }, + { + id: 4, + producto: 'Marca específica de cereal', + descripcion: 'Una marca de cereal aumenta su precio un 8% mientras las competidoras mantienen precios.', + precioInicial: 50, + precioFinal: 54, + cantidadInicial: 8000, + cantidadFinal: 4000, + categoriaCorrecta: 'elastica', + explicacion: 'Una marca específica de cereal tiene demanda muy elástica porque hay muchos sustitutos perfectos (otras marcas). Los consumidores cambian fácilmente de marca cuando sube el precio.' + }, + { + id: 5, + producto: 'Sal marina gourmet', + descripcion: 'El precio de sal marina artesanal baja un 25% en promoción.', + precioInicial: 40, + precioFinal: 30, + cantidadInicial: 2000, + cantidadFinal: 2100, + categoriaCorrecta: 'inelastica', + explicacion: 'La sal es un bien básico con demanda muy inelástica. Aunque baje el precio, la cantidad demandada no aumenta mucho porque la gente solo consume la cantidad que necesita.' + } +]; + +export const ElasticidadElasticaInelastica: React.FC = () => { + const [escenarioActual, setEscenarioActual] = useState(0); + const [respuestaSeleccionada, setRespuestaSeleccionada] = useState(null); + const [resultado, setResultado] = useState<{ + correcto: boolean; + mensaje: string; + mostrarExplicacion: boolean; + } | null>(null); + const [puntuacion, setPuntuacion] = useState(0); + const [ejerciciosCompletados, setEjerciciosCompletados] = useState(0); + + const escenario = escenarios[escenarioActual]; + + const calcularElasticidad = (e: EscenarioElasticidad): number => { + const cambioCantidad = e.cantidadFinal - e.cantidadInicial; + const cambioPrecio = e.precioFinal - e.precioInicial; + const cantidadPromedio = (e.cantidadInicial + e.cantidadFinal) / 2; + const precioPromedio = (e.precioInicial + e.precioFinal) / 2; + + if (cantidadPromedio === 0 || precioPromedio === 0) return 0; + + return Math.abs((cambioCantidad / cantidadPromedio) / (cambioPrecio / precioPromedio)); + }; + + const verificarRespuesta = (categoria: string) => { + if (resultado) return; + + setRespuestaSeleccionada(categoria); + const correcto = categoria === escenario.categoriaCorrecta; + + if (correcto) { + setPuntuacion(prev => prev + 1); + } + + setResultado({ + correcto, + mensaje: correcto + ? '¡Correcto! Has identificado la elasticidad correctamente.' + : 'Incorrecto. Revisa el valor calculado de la elasticidad.', + mostrarExplicacion: true + }); + + setEjerciciosCompletados(prev => prev + 1); + }; + + const siguienteEjercicio = () => { + const siguiente = (escenarioActual + 1) % escenarios.length; + setEscenarioActual(siguiente); + setRespuestaSeleccionada(null); + setResultado(null); + }; + + const reiniciarEjercicios = () => { + setEscenarioActual(0); + setRespuestaSeleccionada(null); + setResultado(null); + setPuntuacion(0); + setEjerciciosCompletados(0); + }; + + const elasticidadCalculada = calcularElasticidad(escenario); + const cambioPrecioPorcentaje = ((escenario.precioFinal - escenario.precioInicial) / ((escenario.precioInicial + escenario.precioFinal) / 2) * 100); + const cambioCantidadPorcentaje = ((escenario.cantidadFinal - escenario.cantidadInicial) / ((escenario.cantidadInicial + escenario.cantidadFinal) / 2) * 100); + + const getCategoriaColor = (cat: string) => { + switch (cat) { + case 'elastica': return 'bg-green-100 border-green-300 text-green-800'; + case 'inelastica': return 'bg-amber-100 border-amber-300 text-amber-800'; + case 'unitaria': return 'bg-blue-100 border-blue-300 text-blue-800'; + default: return 'bg-gray-100 border-gray-300'; + } + }; + + return ( +
+
+
+

Clasificación de Elasticidad

+

Analiza cada escenario y clasifica la elasticidad de la demanda.

+
+
+
+

Puntuación

+

{puntuacion}/{ejerciciosCompletados}

+
+
+
+ +
+
+
+ {escenario.id} +
+
+

{escenario.producto}

+

{escenario.descripcion}

+ +
+
+

Precio Inicial

+

${escenario.precioInicial}

+
+
+

Precio Final

+

${escenario.precioFinal}

+

+ Cambio: {cambioPrecioPorcentaje > 0 ? '+' : ''}{cambioPrecioPorcentaje.toFixed(1)}% +

+
+
+

Cantidad Inicial

+

{escenario.cantidadInicial.toLocaleString()}

+
+
+

Cantidad Final

+

{escenario.cantidadFinal.toLocaleString()}

+

+ Cambio: {cambioCantidadPorcentaje > 0 ? '+' : ''}{cambioCantidadPorcentaje.toFixed(1)}% +

+
+
+ +
+

Datos calculados para ti:

+
+
+ Elasticidad calculada: + {elasticidadCalculada.toFixed(2)} +
+
+ Ratio %Q / %P: + + {Math.abs(cambioCantidadPorcentaje / cambioPrecioPorcentaje).toFixed(2)} + +
+
+
+
+
+
+ +
+

¿Cómo clasificarías la elasticidad de la demanda?

+ +
+ + + + + +
+
+ + {resultado && ( +
+
+
+ {resultado.correcto ? ( + + + + ) : ( + + + + )} +
+
+

+ {resultado.correcto ? '¡Respuesta Correcta!' : 'Respuesta Incorrecta'} +

+

+ {resultado.mensaje} +

+ +
+

+ Respuesta correcta: {escenario.categoriaCorrecta === 'elastica' ? 'Elástica' : escenario.categoriaCorrecta === 'inelastica' ? 'Inelástica' : 'Unitaria'} (Ed = {elasticidadCalculada.toFixed(2)}) +

+

{escenario.explicacion}

+
+
+
+
+ )} + + {resultado && ( +
+ + +
+ )} + +
+

+ + + + Guía de Clasificación +

+
+
+

Elástica (Ed > 1)

+
    +
  • • Lujos y bienes discrecionales
  • +
  • • Muchos sustitutos disponibles
  • +
  • • Consumo puede posponerse
  • +
  • • Representa % grande del ingreso
  • +
+
+
+

Unitaria (Ed = 1)

+
    +
  • • Cambio proporcional exacto
  • +
  • • Caso teórico ideal
  • +
  • • Ingreso total constante
  • +
+
+
+

Inelástica (Ed < 1)

+
    +
  • • Necesidades básicas
  • +
  • • Pocos o ningún sustituto
  • +
  • • Consumo indispensable
  • +
  • • Representa % pequeño del ingreso
  • +
+
+
+
+
+ ); +}; + +export default ElasticidadElasticaInelastica; diff --git a/frontend/src/components/exercises/modulo2/ElasticidadIngresoTotal.tsx b/frontend/src/components/exercises/modulo2/ElasticidadIngresoTotal.tsx new file mode 100644 index 0000000..9f5fb2e --- /dev/null +++ b/frontend/src/components/exercises/modulo2/ElasticidadIngresoTotal.tsx @@ -0,0 +1,437 @@ +import React, { useState } from 'react'; + +interface ProductoEscenario { + id: number; + nombre: string; + elasticidad: number; + precioInicial: number; + cantidadInicial: number; + precioActual: number; + cantidadActual: number; + descripcion: string; +} + +const generarEscenario = (): ProductoEscenario => { + const elasticidades = [0.3, 0.5, 0.8, 1.0, 1.2, 1.5, 2.0, 3.0]; + const elasticidad = elasticidades[Math.floor(Math.random() * elasticidades.length)]; + + const nombresProductos = [ + 'Medicamentos esenciales', + 'Gasolina', + 'Pan de caja', + 'Leche', + 'Cereal de marca', + 'Entradas de cine', + 'Restaurantes de lujo', + 'Viajes internacionales', + 'Yates', + 'Diamantes' + ]; + + const nombre = nombresProductos[Math.floor(Math.random() * nombresProductos.length)]; + const precioInicial = Math.round((Math.random() * 200 + 20) * 100) / 100; + const cantidadInicial = Math.round(Math.random() * 5000 + 500); + + const cambioPrecioPorcentaje = (Math.random() > 0.5 ? 1 : -1) * (Math.random() * 30 + 10); + const cambioCantidadPorcentaje = -elasticidad * cambioPrecioPorcentaje; + + const precioActual = Math.round(precioInicial * (1 + cambioPrecioPorcentaje / 100) * 100) / 100; + const cantidadActual = Math.round(cantidadInicial * (1 + cambioCantidadPorcentaje / 100)); + + const descripciones: Record = { + 'Medicamentos esenciales': 'Bien de necesidad sin sustitutos. La demanda es extremadamente inelástica.', + 'Gasolina': 'Necesidad a corto plazo con pocos sustitutos inmediatos.', + 'Pan de caja': 'Bien básico con algunos sustitutos (pan artesanal, tortillas).', + 'Leche': 'Necesidad básica aunque existen sustitutos (leche de almendra, soya).', + 'Cereal de marca': 'Bien con muchos sustitutos de otras marcas.', + 'Entradas de cine': 'Entretenimiento discrecional con alternativas (streaming).', + 'Restaurantes de lujo': 'Bien de lujo altamente discrecional.', + 'Viajes internacionales': 'Lujo con muchas alternativas de entretenimiento.', + 'Yates': 'Bien de super lujo, demanda muy elástica.', + 'Diamantes': 'Bien de lujo con demanda altamente sensible al precio.' + }; + + return { + id: Date.now(), + nombre, + elasticidad, + precioInicial: Math.max(1, precioInicial), + cantidadInicial: Math.max(10, cantidadInicial), + precioActual: Math.max(1, precioActual), + cantidadActual: Math.max(10, cantidadActual), + descripcion: descripciones[nombre] || 'Producto con características estándar.' + }; +}; + +export const ElasticidadIngresoTotal: React.FC = () => { + const [escenario, setEscenario] = useState(generarEscenario()); + const [decision, setDecision] = useState<'subir' | 'bajar' | null>(null); + const [resultado, setResultado] = useState<{ + correcto: boolean; + mensaje: string; + ingresoInicial: number; + ingresoNuevo: number; + mostrarAnalisis: boolean; + } | null>(null); + const [puntuacion, setPuntuacion] = useState(0); + const [intentos, setIntentos] = useState(0); + + const ingresoInicial = escenario.precioInicial * escenario.cantidadInicial; + const ingresoActual = escenario.precioActual * escenario.cantidadActual; + const cambioPrecio = ((escenario.precioActual - escenario.precioInicial) / escenario.precioInicial) * 100; + const elasticidadCalculada = Math.abs( + ((escenario.cantidadActual - escenario.cantidadInicial) / ((escenario.cantidadInicial + escenario.cantidadActual) / 2)) / + ((escenario.precioActual - escenario.precioInicial) / ((escenario.precioInicial + escenario.precioActual) / 2)) + ); + + const verificarDecision = (dec: 'subir' | 'bajar') => { + if (resultado) return; + + setDecision(dec); + setIntentos(prev => prev + 1); + + const demandaElastica = elasticidadCalculada > 1; + const precioSubio = cambioPrecio > 0; + + let correcto = false; + let mensaje = ''; + + if (demandaElastica) { + // Demanda elástica: subir precio reduce ingreso, bajar precio aumenta ingreso + if (dec === 'subir') { + correcto = false; + mensaje = 'Incorrecto. Con demanda elástica, subir el precio reduce el ingreso total porque la cantidad cae proporcionalmente más.'; + } else { + correcto = true; + mensaje = '¡Correcto! Con demanda elástica, bajar el precio aumenta el ingreso total porque la cantidad vendida aumenta proporcionalmente más.'; + setPuntuacion(prev => prev + 1); + } + } else if (elasticidadCalculada < 1) { + // Demanda inelástica: subir precio aumenta ingreso, bajar precio reduce ingreso + if (dec === 'subir') { + correcto = true; + mensaje = '¡Correcto! Con demanda inelástica, subir el precio aumenta el ingreso total porque la cantidad cae proporcionalmente menos.'; + setPuntuacion(prev => prev + 1); + } else { + correcto = false; + mensaje = 'Incorrecto. Con demanda inelástica, bajar el precio reduce el ingreso total porque la cantidad no aumenta lo suficiente para compensar.'; + } + } else { + // Demanda unitaria + correcto = true; + mensaje = 'La demanda es unitaria, por lo que cualquier cambio de precio mantendrá el ingreso constante. Ambas opciones son igualmente válidas.'; + setPuntuacion(prev => prev + 1); + } + + setResultado({ + correcto, + mensaje, + ingresoInicial, + ingresoNuevo: ingresoActual, + mostrarAnalisis: true + }); + }; + + const generarNuevoEscenario = () => { + setEscenario(generarEscenario()); + setDecision(null); + setResultado(null); + }; + + const formatearDinero = (cantidad: number) => { + return new Intl.NumberFormat('es-MX', { + style: 'currency', + currency: 'MXN', + minimumFractionDigits: 2 + }).format(cantidad); + }; + + return ( +
+
+
+

Elasticidad e Ingreso Total

+

Maximiza el ingreso total tomando la decisión correcta sobre precios.

+
+
+
+

Puntuación

+

{puntuacion}/{intentos}

+
+
+
+ +
+
+
+ + + +
+
+

{escenario.nombre}

+

{escenario.descripcion}

+ +
+
+

Precio Inicial

+

{formatearDinero(escenario.precioInicial)}

+
+
+

Cantidad Inicial

+

{escenario.cantidadInicial.toLocaleString()}

+
+
+

Precio Actual

+

0 ? 'text-red-600' : 'text-green-600'}`}> + {formatearDinero(escenario.precioActual)} +

+

0 ? 'text-red-500' : 'text-green-500'}`}> + {cambioPrecio > 0 ? '+' : ''}{cambioPrecio.toFixed(1)}% +

+
+
+

Cantidad Actual

+

escenario.cantidadInicial ? 'text-green-600' : 'text-red-600'}`}> + {escenario.cantidadActual.toLocaleString()} +

+

escenario.cantidadInicial ? 'text-green-500' : 'text-red-500'}`}> + {((escenario.cantidadActual - escenario.cantidadInicial) / escenario.cantidadInicial * 100) > 0 ? '+' : ''} + {((escenario.cantidadActual - escenario.cantidadInicial) / escenario.cantidadInicial * 100).toFixed(1)}% +

+
+
+
+
+
+ +
+
+

+ + + + Ingreso Inicial +

+

{formatearDinero(ingresoInicial)}

+

+ {escenario.precioInicial} × {escenario.cantidadInicial.toLocaleString()} +

+
+ +
+

+ + + + Ingreso Actual +

+

{formatearDinero(ingresoActual)}

+

+ {escenario.precioActual} × {escenario.cantidadActual.toLocaleString()} +

+
+ +
ingresoInicial + ? 'from-green-50 to-emerald-50 border-green-200' + : 'from-red-50 to-pink-50 border-red-200' + }`}> +

ingresoInicial ? 'text-green-800' : 'text-red-800' + }`}> + + + + Cambio en Ingreso +

+

ingresoInicial ? 'text-green-700' : 'text-red-700'}`}> + {ingresoActual > ingresoInicial ? '+' : ''} + {formatearDinero(ingresoActual - ingresoInicial)} +

+

ingresoInicial ? 'text-green-600' : 'text-red-600'}`}> + {((ingresoActual - ingresoInicial) / ingresoInicial * 100) > 0 ? '+' : ''} + {((ingresoActual - ingresoInicial) / ingresoInicial * 100).toFixed(1)}% +

+
+
+ +
+

+ + + + Tu Decisión +

+

+ Basándote en los datos anteriores, ¿qué decisión de precio maximizaría el ingreso total? +

+ +
+ + + +
+
+ + {resultado && ( +
+
+
+ {resultado.correcto ? ( + + + + ) : ( + + + + )} +
+
+

+ {resultado.correcto ? '¡Decisión Correcta!' : 'Decisión Incorrecta'} +

+

+ {resultado.mensaje} +

+ +
+
+
+ Elasticidad calculada: + {elasticidadCalculada.toFixed(2)} +
+
+ Clasificación: + 1 ? 'text-green-600' : + elasticidadCalculada < 1 ? 'text-amber-600' : 'text-blue-600' + }`}> + {elasticidadCalculada > 1 ? 'Elástica' : elasticidadCalculada < 1 ? 'Inelástica' : 'Unitaria'} + +
+
+ +
+

Análisis del Ingreso Total:

+
    +
  • • Ingreso inicial: {formatearDinero(resultado.ingresoInicial)}
  • +
  • • Ingreso después del cambio: {formatearDinero(resultado.ingresoNuevo)}
  • +
  • • Diferencia: {formatearDinero(resultado.ingresoNuevo - resultado.ingresoInicial)}
  • +
+
+
+
+
+
+ )} + + {resultado && ( +
+ +
+ )} + +
+

+ + + + Regla para Maximizar Ingreso Total +

+
+
+
+ Ed > 1 + Demanda Elástica +
+

Para maximizar ingreso:

+

+ + + + BAJAR el precio +

+

+ La cantidad aumenta más que proporcionalmente al precio. +

+
+ +
+
+ Ed < 1 + Demanda Inelástica +
+

Para maximizar ingreso:

+

+ + + + SUBIR el precio +

+

+ La cantidad cae menos que proporcionalmente al precio. +

+
+
+ +
+

+ Ed = 1 (Unitaria): El ingreso total ya está maximizado. Cualquier cambio de precio mantendrá el ingreso constante. +

+
+
+
+ ); +}; + +export default ElasticidadIngresoTotal; diff --git a/frontend/src/components/exercises/modulo2/EquilibrioFinder.tsx b/frontend/src/components/exercises/modulo2/EquilibrioFinder.tsx new file mode 100644 index 0000000..f583866 --- /dev/null +++ b/frontend/src/components/exercises/modulo2/EquilibrioFinder.tsx @@ -0,0 +1,426 @@ +import React, { useState, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Calculator, Check, X, Trophy, RotateCcw, ArrowRight, Lightbulb, Target } from 'lucide-react'; + +interface EquilibrioFinderProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Problema { + id: number; + demanda: { a: number; b: number }; + oferta: { c: number; d: number }; + producto: string; + dificultad: 'facil' | 'medio' | 'dificil'; +} + +const problemas: Problema[] = [ + { + id: 1, + demanda: { a: 100, b: -2 }, + oferta: { c: 10, d: 3 }, + producto: 'Manzanas', + dificultad: 'facil' + }, + { + id: 2, + demanda: { a: 80, b: -1.5 }, + oferta: { c: 20, d: 2 }, + producto: 'Camisetas', + dificultad: 'facil' + }, + { + id: 3, + demanda: { a: 120, b: -0.8 }, + oferta: { c: 30, d: 1.2 }, + producto: 'Entradas de cine', + dificultad: 'medio' + }, + { + id: 4, + demanda: { a: 200, b: -4 }, + oferta: { c: 50, d: 2.5 }, + producto: 'Bicicletas', + dificultad: 'medio' + }, + { + id: 5, + demanda: { a: 150, b: -1.2 }, + oferta: { c: 25, d: 0.8 }, + producto: 'Consultas médicas', + dificultad: 'dificil' + } +]; + +export const EquilibrioFinder: React.FC = ({ onComplete, ejercicioId: _ejercicioId }) => { + const [problemaActual, setProblemaActual] = useState(0); + const [respuestaPrecio, setRespuestaPrecio] = useState(''); + const [respuestaCantidad, setRespuestaCantidad] = useState(''); + const [mostrarResultado, setMostrarResultado] = useState(false); + const [esCorrecto, setEsCorrecto] = useState(false); + const [score, setScore] = useState(0); + const [respuestasCorrectas, setRespuestasCorrectas] = useState(0); + const [mostrarAyuda, setMostrarAyuda] = useState(false); + const [completado, setCompletado] = useState(false); + const [_startTime] = useState(Date.now()); + + const problema = problemas[problemaActual]; + + const calcularEquilibrio = (problema: Problema) => { + const { a, b } = problema.demanda; + const { c, d } = problema.oferta; + const Q = (c - a) / (b - d); + const P = a + b * Q; + return { Q: Math.round(Q * 10) / 10, P: Math.round(P * 10) / 10 }; + }; + + const equilibrio = calcularEquilibrio(problema); + + const handleVerificar = () => { + const precioIngresado = parseFloat(respuestaPrecio); + const cantidadIngresada = parseFloat(respuestaCantidad); + + if (isNaN(precioIngresado) || isNaN(cantidadIngresada)) { + return; + } + + const margenError = 0.5; + const precioCorrecto = Math.abs(precioIngresado - equilibrio.P) <= margenError; + const cantidadCorrecta = Math.abs(cantidadIngresada - equilibrio.Q) <= margenError; + + const correcto = precioCorrecto && cantidadCorrecta; + setEsCorrecto(correcto); + setMostrarResultado(true); + + if (correcto) { + setScore(prev => prev + Math.round(100 / problemas.length)); + setRespuestasCorrectas(prev => prev + 1); + } + }; + + const handleSiguiente = () => { + if (problemaActual < problemas.length - 1) { + setProblemaActual(prev => prev + 1); + setRespuestaPrecio(''); + setRespuestaCantidad(''); + setMostrarResultado(false); + setMostrarAyuda(false); + } else { + setCompletado(true); + if (onComplete) { + onComplete(score); + } + } + }; + + const handleReiniciar = () => { + setProblemaActual(0); + setRespuestaPrecio(''); + setRespuestaCantidad(''); + setMostrarResultado(false); + setScore(0); + setRespuestasCorrectas(0); + setMostrarAyuda(false); + 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 generarPuntosCurva = (tipo: 'demanda' | 'oferta') => { + const puntos = []; + for (let Q = 0; Q <= 50; Q += 5) { + if (tipo === 'demanda') { + const P = problema.demanda.a + problema.demanda.b * Q; + if (P >= 0) puntos.push({ Q, P }); + } else { + const P = problema.oferta.c + problema.oferta.d * Q; + if (P >= 0) puntos.push({ Q, P }); + } + } + return puntos; + }; + + const scaleX = (Q: number) => 50 + (Q / 50) * 300; + const scaleY = (P: number) => 250 - (P / 150) * 200; + + const puntosDemanda = generarPuntosCurva('demanda'); + const puntosOferta = generarPuntosCurva('oferta'); + + const demandaPath = puntosDemanda.map((p, i) => + `${i === 0 ? 'M' : 'L'} ${scaleX(p.Q)} ${scaleY(p.P)}` + ).join(' '); + + const ofertaPath = puntosOferta.map((p, i) => + `${i === 0 ? 'M' : 'L'} ${scaleX(p.Q)} ${scaleY(p.P)}` + ).join(' '); + + if (completado) { + const porcentaje = Math.round((respuestasCorrectas / problemas.length) * 100); + + return ( + + +

¡Ejercicio Completado!

+

Has encontrado los puntos de equilibrio

+ +
+
{porcentaje}%
+

+ {respuestasCorrectas} de {problemas.length} problemas resueltos correctamente +

+
+ + +
+ ); + } + + return ( +
+
+
+
+ +

Buscador de Equilibrio

+
+
+ + {problema.dificultad.toUpperCase()} + + + {problemaActual + 1} de {problemas.length} + +
+ +
+
+
+

+ Calcula el precio y cantidad de equilibrio donde Qd = Qo. +

+
+ +
+
+

Gráfico de Mercado: {problema.producto}

+ + + {/* Grid */} + {Array.from({ length: 6 }).map((_, i) => ( + + + + + ))} + + {/* Ejes */} + + + + {/* Labels */} + Cantidad (Q) + Precio (P) + + {/* Curva de Demanda */} + {demandaPath && ( + + + D + + )} + + {/* Curva de Oferta */} + {ofertaPath && ( + + + S + + )} + + {/* Punto de equilibrio (mostrar si ya respondió correctamente) */} + {mostrarResultado && esCorrecto && ( + + + + E + + + + + )} + + +
+

Ecuaciones:

+
+
+ Qd = + {problema.demanda.a} {problema.demanda.b > 0 ? '+' : ''}{problema.demanda.b}P +
+
+ Qo = + {problema.oferta.c > 0 ? '' : '-'}{problema.oferta.c} {problema.oferta.d > 0 ? '+' : ''}{problema.oferta.d}P +
+
+
+
+ +
+
+

+ + Encuentra el Equilibrio +

+ +
+
+ + setRespuestaPrecio(e.target.value)} + disabled={mostrarResultado} + placeholder="Ej: 45.5" + className="w-full px-4 py-3 border-2 border-gray-200 rounded-lg focus:border-purple-500 focus:outline-none disabled:bg-gray-100" + step="0.1" + /> +
+ +
+ + setRespuestaCantidad(e.target.value)} + disabled={mostrarResultado} + placeholder="Ej: 25.3" + className="w-full px-4 py-3 border-2 border-gray-200 rounded-lg focus:border-purple-500 focus:outline-none disabled:bg-gray-100" + step="0.1" + /> +
+
+ + + + + {mostrarAyuda && ( + +

+ Tip: En equilibrio, Qd = Qo. Iguala las dos ecuaciones y despeja P. + Luego sustituye P en cualquier ecuación para encontrar Q. +

+
+ )} +
+
+ + + {mostrarResultado && ( + +
+ {esCorrecto ? ( + + ) : ( + + )} +
+

+ {esCorrecto ? '¡Correcto!' : 'Incorrecto'} +

+ {!esCorrecto && ( +
+

La respuesta correcta es:

+

P* = ${equilibrio.P}

+

Q* = {equilibrio.Q} unidades

+
+ )} +
+
+
+ )} +
+ +
+ {!mostrarResultado ? ( + + ) : ( + + )} +
+
+
+
+ ); +}; + +export default EquilibrioFinder; diff --git a/frontend/src/components/exercises/modulo2/EquilibrioGrafico.tsx b/frontend/src/components/exercises/modulo2/EquilibrioGrafico.tsx new file mode 100644 index 0000000..a7f5988 --- /dev/null +++ b/frontend/src/components/exercises/modulo2/EquilibrioGrafico.tsx @@ -0,0 +1,543 @@ +import React, { useState, useRef, useCallback, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { LineChart, Check, X, RotateCcw, Trophy, Info, MousePointer2 } from 'lucide-react'; + +interface EquilibrioGraficoProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Punto { + x: number; + y: number; +} + +const GRID_SIZE = 350; +const PADDING = 50; +const MAX_PRECIO = 100; +const MAX_CANTIDAD = 100; + +export const EquilibrioGrafico: React.FC = ({ onComplete, ejercicioId: _ejercicioId }) => { + const [demandaPoints, setDemandaPoints] = useState([]); + const [ofertaPoints, setOfertaPoints] = useState([]); + const [modoActivo, setModoActivo] = useState<'demanda' | 'oferta'>('demanda'); + const [equilibrioEncontrado, setEquilibrioEncontrado] = useState(false); + const [mostrarResultado, setMostrarResultado] = useState(false); + const [score, setScore] = useState(0); + const [mensaje, setMensaje] = useState(''); + const [showTutorial, setShowTutorial] = useState(true); + const [_startTime] = useState(Date.now()); + + const svgRef = useRef(null); + + 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) => { + if (equilibrioEncontrado) 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 newPoint: Punto = { x: cartesian.x, y: cartesian.y }; + + if (modoActivo === 'demanda') { + if (demandaPoints.length >= 2) { + setDemandaPoints([newPoint]); + } else { + setDemandaPoints(prev => [...prev, newPoint]); + } + } else { + if (ofertaPoints.length >= 2) { + setOfertaPoints([newPoint]); + } else { + setOfertaPoints(prev => [...prev, newPoint]); + } + } + setMensaje(''); + }; + + const calcularInterseccion = () => { + if (demandaPoints.length < 2 || ofertaPoints.length < 2) return null; + + const d1 = demandaPoints[0]; + const d2 = demandaPoints[1]; + const s1 = ofertaPoints[0]; + const s2 = ofertaPoints[1]; + + const m1 = (d2.y - d1.y) / (d2.x - d1.x); + const m2 = (s2.y - s1.y) / (s2.x - s1.x); + + if (Math.abs(m1 - m2) < 0.01) return null; + + const b1 = d1.y - m1 * d1.x; + const b2 = s1.y - m2 * s1.x; + + const x = (b2 - b1) / (m1 - m2); + const y = m1 * x + b1; + + return { x: Math.round(x), y: Math.round(y) }; + }; + + const validarEquilibrio = () => { + if (demandaPoints.length < 2) { + setMensaje('Necesitas 2 puntos para trazar la curva de demanda'); + return; + } + if (ofertaPoints.length < 2) { + setMensaje('Necesitas 2 puntos para trazar la curva de oferta'); + return; + } + + const d1 = demandaPoints[0]; + const d2 = demandaPoints[1]; + const s1 = ofertaPoints[0]; + const s2 = ofertaPoints[1]; + + const pendienteDemanda = (d2.y - d1.y) / (d2.x - d1.x); + const pendienteOferta = (s2.y - s1.y) / (s2.x - s1.x); + + if (pendienteDemanda >= 0) { + setMensaje('La demanda debe tener pendiente negativa (bajar de izquierda a derecha)'); + return; + } + + if (pendienteOferta <= 0) { + setMensaje('La oferta debe tener pendiente positiva (subir de izquierda a derecha)'); + return; + } + + const interseccion = calcularInterseccion(); + if (!interseccion) { + setMensaje('Las curvas no se intersectan dentro del rango válido'); + return; + } + + setEquilibrioEncontrado(true); + setMostrarResultado(true); + setScore(100); + setMensaje(''); + + setTimeout(() => { + if (onComplete) { + onComplete(100); + } + }, 3000); + }; + + const reiniciar = () => { + setDemandaPoints([]); + setOfertaPoints([]); + setModoActivo('demanda'); + setEquilibrioEncontrado(false); + setMostrarResultado(false); + setScore(0); + setMensaje(''); + setShowTutorial(true); + }; + + const renderLineaCurva = (puntos: Punto[], color: string, esPunteada: boolean = false) => { + if (puntos.length < 2) return null; + + const sorted = [...puntos].sort((a, b) => a.x - b.x); + const start = sorted[0]; + const end = sorted[sorted.length - 1]; + + const startSvg = cartesianToSvg(start.x, start.y); + const endSvg = cartesianToSvg(end.x, end.y); + + const m = (end.y - start.y) / (end.x - start.x); + const b = start.y - m * start.x; + + const yAtX0 = b; + const yAtXMax = m * MAX_CANTIDAD + b; + + const p0 = cartesianToSvg(0, Math.max(0, Math.min(MAX_PRECIO, yAtX0))); + const pMax = cartesianToSvg(MAX_CANTIDAD, Math.max(0, Math.min(MAX_PRECIO, yAtXMax))); + + return ( + + ); + }; + + const interseccion = calcularInterseccion(); + + return ( +
+
+
+
+ +

Gráfico de Equilibrio

+
+
+ Encuentra la intersección + +
+
+

+ Traza las curvas de demanda y oferta para encontrar el punto de equilibrio donde se cruzan. +

+
+ + {showTutorial && ( + + +
+

Cómo jugar:

+
    +
  • • Selecciona el modo (Demanda u Oferta) con los botones
  • +
  • • Haz clic en el gráfico para colocar 2 puntos de cada curva
  • +
  • • La demanda debe tener pendiente negativa (baja)
  • +
  • • La oferta debe tener pendiente positiva (sube)
  • +
  • • Presiona "Validar Equilibrio" cuando estén ambas curvas
  • +
+
+ +
+ )} + +
+
+
+ + {/* Grid */} + {Array.from({ length: 11 }).map((_, i) => ( + + + + + ))} + + {/* Ejes */} + + + + {/* Labels ejes */} + + Cantidad (Q) + + + Precio (P) + + + {/* Curvas */} + {renderLineaCurva(demandaPoints, '#3b82f6')} + {renderLineaCurva(ofertaPoints, '#22c55e')} + + {/* Labels de curvas */} + {demandaPoints.length >= 2 && ( + D + )} + {ofertaPoints.length >= 2 && ( + S + )} + + {/* Puntos Demanda */} + {demandaPoints.map((punto, index) => { + const svg = cartesianToSvg(punto.x, punto.y); + return ( + + + + D{index + 1} + + + ); + })} + + {/* Puntos Oferta */} + {ofertaPoints.map((punto, index) => { + const svg = cartesianToSvg(punto.x, punto.y); + return ( + + + + S{index + 1} + + + ); + })} + + {/* Punto de Equilibrio */} + {equilibrioEncontrado && interseccion && ( + + + + E ({interseccion.x}, {interseccion.y}) + + + {/* Líneas guía */} + + + + )} + +
+
+ +
+
+ + +
+ +
+

+ + Estado +

+
+
+ Puntos Demanda: + = 2 ? 'text-blue-600' : 'text-gray-400'}`}> + {demandaPoints.length}/2 + +
+
+ +
+ +
+ Puntos Oferta: + = 2 ? 'text-green-600' : 'text-gray-400'}`}> + {ofertaPoints.length}/2 + +
+
+ +
+
+
+ + {interseccion && !equilibrioEncontrado && ( + +

+ Intersección detectada: +

+

+ Q* = {interseccion.x}, P* = {interseccion.y} +

+
+ )} + + {mensaje && ( + + +

{mensaje}

+
+ )} + + + + + {mostrarResultado && ( + + +

¡Equilibrio Encontrado!

+ {interseccion && ( +
+

Precio de equilibrio: ${interseccion.y}

+

Cantidad de equilibrio: {interseccion.x} unidades

+
+ )} +
+ )} +
+ +
+

+ Recuerda: +

+
    +
  • • Demanda: pendiente negativa ↘️
  • +
  • • Oferta: pendiente positiva ↗️
  • +
  • • El equilibrio es donde se cruzan
  • +
+
+
+
+
+ ); +}; + +export default EquilibrioGrafico; diff --git a/frontend/src/components/exercises/modulo2/ExcesoDemandaEscasez.tsx b/frontend/src/components/exercises/modulo2/ExcesoDemandaEscasez.tsx new file mode 100644 index 0000000..349f0c9 --- /dev/null +++ b/frontend/src/components/exercises/modulo2/ExcesoDemandaEscasez.tsx @@ -0,0 +1,454 @@ +import React, { useState, useMemo } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { AlertTriangle, TrendingDown, ArrowRight, CheckCircle2, XCircle, Trophy, RotateCcw, BookOpen } from 'lucide-react'; + +interface ExcesoDemandaEscasezProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Escenario { + id: number; + producto: string; + precioEquilibrio: number; + cantidadEquilibrio: number; + precioControl: number; + demanda: { a: number; b: number }; + oferta: { c: number; d: number }; + contexto: string; + dificultad: 'facil' | 'medio' | 'dificil'; +} + +const escenarios: Escenario[] = [ + { + id: 1, + producto: 'Pan', + precioEquilibrio: 50, + cantidadEquilibrio: 100, + precioControl: 30, + demanda: { a: 150, b: -1 }, + oferta: { c: 0, d: 2 }, + contexto: 'El gobierno fija un precio máximo de $30 para el pan para proteger a los consumidores.', + dificultad: 'facil' + }, + { + id: 2, + producto: 'Gasolina', + precioEquilibrio: 80, + cantidadEquilibrio: 60, + precioControl: 50, + demanda: { a: 140, b: -1 }, + oferta: { c: 20, d: 0.75 }, + contexto: 'Se impone un precio máximo de $50 por galón ante el alza de precios internacionales.', + dificultad: 'medio' + }, + { + id: 3, + producto: 'Vivienda', + precioEquilibrio: 1200, + cantidadEquilibrio: 500, + precioControl: 800, + demanda: { a: 2200, b: -2 }, + oferta: { c: 200, d: 2 }, + contexto: 'Control de rentas fija el precio máximo en $800 para hacer la vivienda accesible.', + dificultad: 'dificil' + } +]; + +export const ExcesoDemandaEscasez: React.FC = ({ onComplete, ejercicioId: _ejercicioId }) => { + const [escenarioActual, setEscenarioActual] = useState(0); + const [respuestaExceso, setRespuestaExceso] = useState(''); + const [mostrarResultado, setMostrarResultado] = useState(false); + const [esCorrecto, setEsCorrecto] = useState(false); + const [score, setScore] = useState(0); + const [respuestasCorrectas, setRespuestasCorrectas] = useState(0); + const [completado, setCompletado] = useState(false); + const [_startTime] = useState(Date.now()); + + const escenario = escenarios[escenarioActual]; + + const calcularCantidades = (precio: number, escenario: Escenario) => { + const Qd = escenario.demanda.a + escenario.demanda.b * precio; + const Qo = escenario.oferta.c + escenario.oferta.d * precio; + return { Qd: Math.max(0, Qd), Qo: Math.max(0, Qo) }; + }; + + const equilibrio = calcularCantidades(escenario.precioEquilibrio, escenario); + const conControl = calcularCantidades(escenario.precioControl, escenario); + const excesoDemandaReal = conControl.Qd - conControl.Qo; + + const handleVerificar = () => { + const respuesta = parseFloat(respuestaExceso); + if (isNaN(respuesta)) return; + + const margenError = escenario.precioEquilibrio * 0.1; + const correcto = Math.abs(respuesta - excesoDemandaReal) <= margenError; + + setEsCorrecto(correcto); + setMostrarResultado(true); + + if (correcto) { + setScore(prev => prev + Math.round(100 / escenarios.length)); + setRespuestasCorrectas(prev => prev + 1); + } + }; + + const handleSiguiente = () => { + if (escenarioActual < escenarios.length - 1) { + setEscenarioActual(prev => prev + 1); + setRespuestaExceso(''); + setMostrarResultado(false); + } else { + setCompletado(true); + if (onComplete) { + onComplete(score); + } + } + }; + + const handleReiniciar = () => { + setEscenarioActual(0); + setRespuestaExceso(''); + 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 generarPuntosCurva = (tipo: 'demanda' | 'oferta', escenario: Escenario) => { + const puntos = []; + const maxP = Math.max(escenario.precioEquilibrio * 1.5, escenario.precioControl * 1.2); + for (let P = 0; P <= maxP; P += maxP / 20) { + if (tipo === 'demanda') { + const Q = escenario.demanda.a + escenario.demanda.b * P; + if (Q >= 0) puntos.push({ Q, P }); + } else { + const Q = escenario.oferta.c + escenario.oferta.d * P; + if (Q >= 0) puntos.push({ Q, P }); + } + } + return puntos; + }; + + const maxQ = Math.max(equilibrio.Qd, conControl.Qd) * 1.3; + const maxP = escenario.precioEquilibrio * 1.5; + + const scaleX = (Q: number) => 50 + (Q / maxQ) * 350; + const scaleY = (P: number) => 300 - (P / maxP) * 250; + + const puntosDemanda = generarPuntosCurva('demanda', escenario); + const puntosOferta = generarPuntosCurva('oferta', escenario); + + const demandaPath = puntosDemanda.map((p, i) => + `${i === 0 ? 'M' : 'L'} ${scaleX(p.Q)} ${scaleY(p.P)}` + ).join(' '); + + const ofertaPath = puntosOferta.map((p, i) => + `${i === 0 ? 'M' : 'L'} ${scaleX(p.Q)} ${scaleY(p.P)}` + ).join(' '); + + if (completado) { + const porcentaje = Math.round((respuestasCorrectas / escenarios.length) * 100); + + return ( + + +

¡Ejercicio Completado!

+

Has analizado escenarios de escasez

+ +
+
{porcentaje}%
+

+ {respuestasCorrectas} de {escenarios.length} respuestas correctas +

+
+ + +
+ ); + } + + return ( +
+
+
+
+ +

Exceso de Demanda (Escasez)

+
+
+ + {escenario.dificultad.toUpperCase()} + + + {escenarioActual + 1} de {escenarios.length} + +
+ +
+
+
+

+ Analiza qué sucede cuando el precio está por debajo del equilibrio. +

+
+ +
+
+

Mercado de {escenario.producto}

+ + + {/* Grid */} + {Array.from({ length: 6 }).map((_, i) => ( + + + + + ))} + + {/* Ejes */} + + + + {/* Labels */} + Cantidad (Q) + Precio (P) + + {/* Curva de Demanda */} + + D + + {/* Curva de Oferta */} + + S + + {/* Punto de equilibrio */} + + + E + + + {/* Línea de precio de control */} + + + Pmax=${escenario.precioControl} + + + {/* Zona de exceso de demanda */} + + + + Escasez + + + + {/* Puntos de cantidad */} + + + Qd + + + + + Qo + + + +
+
+ Equilibrio: +

P=${escenario.precioEquilibrio}, Q={Math.round(equilibrio.Qd)}

+
+
+ Con Pmax: +

Qd={Math.round(conControl.Qd)}, Qo={Math.round(conControl.Qo)}

+
+
+
+ +
+
+
+ +
+

Escenario

+

{escenario.contexto}

+
+
+
+ +
+

+ + Calcula el Exceso de Demanda +

+ +
+
+ + setRespuestaExceso(e.target.value)} + disabled={mostrarResultado} + placeholder="Ingresa el valor numérico" + className="w-full px-4 py-3 border-2 border-red-200 rounded-lg focus:border-red-500 focus:outline-none disabled:bg-gray-100" + /> +

+ Fórmula: Exceso de Demanda = Qd - Qo (al precio de control) +

+
+
+
+ + + {mostrarResultado && ( + +
+ {esCorrecto ? ( + + ) : ( + + )} +
+

+ {esCorrecto ? '¡Correcto!' : 'Incorrecto'} +

+
+

Al precio de ${escenario.precioControl}:

+
    +
  • • Cantidad demandada: {Math.round(conControl.Qd)} unidades
  • +
  • • Cantidad ofrecida: {Math.round(conControl.Qo)} unidades
  • +
  • • Exceso de demanda: {Math.round(excesoDemandaReal)} unidades
  • +
+ {!esCorrecto && ( +

+ La respuesta correcta es: {Math.round(excesoDemandaReal)} unidades +

+ )} +
+
+
+
+ )} +
+ +
+ {!mostrarResultado ? ( + + ) : ( + + )} +
+ +
+

+ Consecuencias del exceso de demanda: +

+
    +
  • • Largas filas y esperas
  • +
  • • Racionamiento del producto
  • +
  • • Mercados negros
  • +
  • • Pérdida de peso muerto
  • +
+
+
+
+
+ ); +}; + +export default ExcesoDemandaEscasez; diff --git a/frontend/src/components/exercises/modulo2/ExcesoOfertaSuperavit.tsx b/frontend/src/components/exercises/modulo2/ExcesoOfertaSuperavit.tsx new file mode 100644 index 0000000..82cd710 --- /dev/null +++ b/frontend/src/components/exercises/modulo2/ExcesoOfertaSuperavit.tsx @@ -0,0 +1,454 @@ +import React, { useState, useMemo } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Package, TrendingUp, ArrowRight, CheckCircle2, XCircle, Trophy, RotateCcw, BookOpen } from 'lucide-react'; + +interface ExcesoOfertaSuperavitProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Escenario { + id: number; + producto: string; + precioEquilibrio: number; + cantidadEquilibrio: number; + precioMinimo: number; + demanda: { a: number; b: number }; + oferta: { c: number; d: number }; + contexto: string; + dificultad: 'facil' | 'medio' | 'dificil'; +} + +const escenarios: Escenario[] = [ + { + id: 1, + producto: 'Leche', + precioEquilibrio: 40, + cantidadEquilibrio: 80, + precioMinimo: 60, + demanda: { a: 120, b: -1 }, + oferta: { c: 0, d: 2 }, + contexto: 'El gobierno establece un precio mínimo de $60 para proteger a los productores lecheros.', + dificultad: 'facil' + }, + { + id: 2, + producto: 'Trigo', + precioEquilibrio: 100, + cantidadEquilibrio: 200, + precioMinimo: 140, + demanda: { a: 300, b: -1 }, + oferta: { c: -100, d: 2 }, + contexto: 'Se fija un precio de sustento de $140 para garantizar ingresos a los agricultores.', + dificultad: 'medio' + }, + { + id: 3, + producto: 'Trabajo no calificado', + precioEquilibrio: 50, + cantidadEquilibrio: 1000, + precioMinimo: 80, + demanda: { a: 150, b: -0.1 }, + oferta: { c: 0, d: 20 }, + contexto: 'El salario mínimo se fija en $80, por encima del salario de equilibrio del mercado.', + dificultad: 'dificil' + } +]; + +export const ExcesoOfertaSuperavit: React.FC = ({ onComplete, ejercicioId: _ejercicioId }) => { + const [escenarioActual, setEscenarioActual] = useState(0); + const [respuestaExceso, setRespuestaExceso] = useState(''); + const [mostrarResultado, setMostrarResultado] = useState(false); + const [esCorrecto, setEsCorrecto] = useState(false); + const [score, setScore] = useState(0); + const [respuestasCorrectas, setRespuestasCorrectas] = useState(0); + const [completado, setCompletado] = useState(false); + const [_startTime] = useState(Date.now()); + + const escenario = escenarios[escenarioActual]; + + const calcularCantidades = (precio: number, escenario: Escenario) => { + const Qd = escenario.demanda.a + escenario.demanda.b * precio; + const Qo = escenario.oferta.c + escenario.oferta.d * precio; + return { Qd: Math.max(0, Qd), Qo: Math.max(0, Qo) }; + }; + + const equilibrio = calcularCantidades(escenario.precioEquilibrio, escenario); + const conMinimo = calcularCantidades(escenario.precioMinimo, escenario); + const excesoOfertaReal = conMinimo.Qo - conMinimo.Qd; + + const handleVerificar = () => { + const respuesta = parseFloat(respuestaExceso); + if (isNaN(respuesta)) return; + + const margenError = escenario.precioEquilibrio * 0.15; + const correcto = Math.abs(respuesta - excesoOfertaReal) <= margenError; + + setEsCorrecto(correcto); + setMostrarResultado(true); + + if (correcto) { + setScore(prev => prev + Math.round(100 / escenarios.length)); + setRespuestasCorrectas(prev => prev + 1); + } + }; + + const handleSiguiente = () => { + if (escenarioActual < escenarios.length - 1) { + setEscenarioActual(prev => prev + 1); + setRespuestaExceso(''); + setMostrarResultado(false); + } else { + setCompletado(true); + if (onComplete) { + onComplete(score); + } + } + }; + + const handleReiniciar = () => { + setEscenarioActual(0); + setRespuestaExceso(''); + 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 generarPuntosCurva = (tipo: 'demanda' | 'oferta', escenario: Escenario) => { + const puntos = []; + const maxP = Math.max(escenario.precioMinimo * 1.2, escenario.precioEquilibrio * 1.5); + for (let P = 0; P <= maxP; P += maxP / 20) { + if (tipo === 'demanda') { + const Q = escenario.demanda.a + escenario.demanda.b * P; + if (Q >= 0) puntos.push({ Q, P }); + } else { + const Q = escenario.oferta.c + escenario.oferta.d * P; + if (Q >= 0) puntos.push({ Q, P }); + } + } + return puntos; + }; + + const maxQ = Math.max(conMinimo.Qo, equilibrio.Qd) * 1.3; + const maxP = Math.max(escenario.precioMinimo, escenario.precioEquilibrio) * 1.3; + + const scaleX = (Q: number) => 50 + (Q / maxQ) * 350; + const scaleY = (P: number) => 300 - (P / maxP) * 250; + + const puntosDemanda = generarPuntosCurva('demanda', escenario); + const puntosOferta = generarPuntosCurva('oferta', escenario); + + const demandaPath = puntosDemanda.map((p, i) => + `${i === 0 ? 'M' : 'L'} ${scaleX(p.Q)} ${scaleY(p.P)}` + ).join(' '); + + const ofertaPath = puntosOferta.map((p, i) => + `${i === 0 ? 'M' : 'L'} ${scaleX(p.Q)} ${scaleY(p.P)}` + ).join(' '); + + if (completado) { + const porcentaje = Math.round((respuestasCorrectas / escenarios.length) * 100); + + return ( + + +

¡Ejercicio Completado!

+

Has analizado escenarios de superávit

+ +
+
{porcentaje}%
+

+ {respuestasCorrectas} de {escenarios.length} respuestas correctas +

+
+ + +
+ ); + } + + return ( +
+
+
+
+ +

Exceso de Oferta (Superávit)

+
+
+ + {escenario.dificultad.toUpperCase()} + + + {escenarioActual + 1} de {escenarios.length} + +
+ +
+
+
+

+ Analiza qué sucede cuando el precio está por encima del equilibrio. +

+
+ +
+
+

Mercado de {escenario.producto}

+ + + {/* Grid */} + {Array.from({ length: 6 }).map((_, i) => ( + + + + + ))} + + {/* Ejes */} + + + + {/* Labels */} + Cantidad (Q) + Precio (P) + + {/* Curva de Demanda */} + + D + + {/* Curva de Oferta */} + + S + + {/* Punto de equilibrio */} + + + E + + + {/* Línea de precio mínimo */} + + + Pmin=${escenario.precioMinimo} + + + {/* Zona de exceso de oferta */} + + + + Superávit + + + + {/* Puntos de cantidad */} + + + Qd + + + + + Qo + + + +
+
+ Equilibrio: +

P=${escenario.precioEquilibrio}, Q={Math.round(equilibrio.Qd)}

+
+
+ Con Pmin: +

Qd={Math.round(conMinimo.Qd)}, Qo={Math.round(conMinimo.Qo)}

+
+
+
+ +
+
+
+ +
+

Escenario

+

{escenario.contexto}

+
+
+
+ +
+

+ + Calcula el Exceso de Oferta +

+ +
+
+ + setRespuestaExceso(e.target.value)} + disabled={mostrarResultado} + placeholder="Ingresa el valor numérico" + className="w-full px-4 py-3 border-2 border-amber-200 rounded-lg focus:border-amber-500 focus:outline-none disabled:bg-gray-100" + /> +

+ Fórmula: Exceso de Oferta = Qo - Qd (al precio mínimo) +

+
+
+
+ + + {mostrarResultado && ( + +
+ {esCorrecto ? ( + + ) : ( + + )} +
+

+ {esCorrecto ? '¡Correcto!' : 'Incorrecto'} +

+
+

Al precio de ${escenario.precioMinimo}:

+
    +
  • • Cantidad ofrecida: {Math.round(conMinimo.Qo)} unidades
  • +
  • • Cantidad demandada: {Math.round(conMinimo.Qd)} unidades
  • +
  • • Exceso de oferta: {Math.round(excesoOfertaReal)} unidades
  • +
+ {!esCorrecto && ( +

+ La respuesta correcta es: {Math.round(excesoOfertaReal)} unidades +

+ )} +
+
+
+
+ )} +
+ +
+ {!mostrarResultado ? ( + + ) : ( + + )} +
+ +
+

+ Consecuencias del exceso de oferta: +

+
    +
  • • Acumulación de inventarios
  • +
  • • Presión para bajar precios
  • +
  • • Necesidad de compras gubernamentales
  • +
  • • Desperdicio de recursos
  • +
+
+
+
+
+ ); +}; + +export default ExcesoOfertaSuperavit; diff --git a/frontend/src/components/exercises/modulo2/FactoresDesplazanDemanda.tsx b/frontend/src/components/exercises/modulo2/FactoresDesplazanDemanda.tsx new file mode 100644 index 0000000..fbe77b3 --- /dev/null +++ b/frontend/src/components/exercises/modulo2/FactoresDesplazanDemanda.tsx @@ -0,0 +1,339 @@ +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { TrendingUp, TrendingDown, Users, DollarSign, Heart, Briefcase, Check, X, Trophy, RotateCcw } from 'lucide-react'; + +interface FactoresDesplazanDemandaProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Factor { + id: string; + nombre: string; + icono: React.ReactNode; + descripcion: string; + efecto: 'aumenta' | 'disminuye'; + explicacion: string; + ejemplo: string; +} + +const factores: Factor[] = [ + { + id: 'ingreso', + nombre: 'Ingreso de los consumidores', + icono: , + descripcion: 'Cuando los ingresos cambian, la demanda de bienes normales se desplaza', + efecto: 'aumenta', + explicacion: 'Si los ingresos suben, los consumidores pueden comprar más de casi todo (bienes normales).', + ejemplo: 'Un aumento de sueldo permite comprar más ropa, comer fuera más seguido, etc.' + }, + { + id: 'precios_relacionados', + nombre: 'Precios de bienes relacionados', + icono: , + descripcion: 'El precio de sustitutos y complementos afecta la demanda', + efecto: 'aumenta', + explicacion: 'Si el precio del café sube, la demanda de té (sustituto) aumenta.', + ejemplo: 'Si la gasolina sube, más personas quieren autos eléctricos (sustitutos del consumo de gasolina).' + }, + { + id: 'gustos', + nombre: 'Gustos y preferencias', + icono: , + descripcion: 'Las modas, publicidad y cambios culturales desplazan la demanda', + efecto: 'aumenta', + explicacion: 'Si un producto se vuelve popular, más personas lo quieren independientemente del precio.', + ejemplo: 'La moda de los celulares inteligentes desplazó la demanda de cámaras fotográficas tradicionales.' + }, + { + id: 'expectativas', + nombre: 'Expectativas de precios futuros', + icono: , + descripcion: 'Lo que esperamos que pase con los precios afecta la demanda actual', + efecto: 'aumenta', + explicacion: 'Si esperamos que suban los precios, compramos más ahora (demanda aumenta).', + ejemplo: 'Antes de un aumento de impuestos a autos, la gente compra vehículos anticipadamente.' + }, + { + id: 'poblacion', + nombre: 'Número de compradores', + icono: , + descripcion: 'Más consumidores en el mercado aumentan la demanda total', + efecto: 'aumenta', + explicacion: 'El crecimiento poblacional o la apertura de nuevos mercados aumenta la demanda.', + ejemplo: 'Una nueva colonia residencial aumenta la demanda de supermercados cercanos.' + }, + { + id: 'demografia', + nombre: 'Cambios demográficos', + icono: , + descripcion: 'La edad, género, ocupación y educación de la población afectan la demanda', + efecto: 'aumenta', + explicacion: 'Diferentes grupos demográficos tienen diferentes necesidades y preferencias.', + ejemplo: 'El envejecimiento poblacional aumenta la demanda de servicios de salud y centros de jubilados.' + } +]; + +interface PreguntaFactor { + factorId: string; + pregunta: string; + respuestaCorrecta: boolean; + escenario: string; +} + +const preguntas: PreguntaFactor[] = [ + { + factorId: 'ingreso', + pregunta: '¿Qué sucede con la demanda de bienes normales si los ingresos de los consumidores aumentan?', + respuestaCorrecta: true, + escenario: 'La economía está creciendo y los salarios suben un 10%' + }, + { + factorId: 'precios_relacionados', + pregunta: 'Si el precio de la mantequilla sube mucho, ¿qué pasa con la demanda de margarina?', + respuestaCorrecta: true, + escenario: 'La mantequilla cuesta $10 y la margarina $5' + }, + { + factorId: 'gustos', + pregunta: 'Un influencer popular recomienda un nuevo celular. ¿Qué sucede con su demanda?', + respuestaCorrecta: true, + escenario: 'El video del influencer tiene 10 millones de views' + }, + { + factorId: 'expectativas', + pregunta: 'Se anuncia que el precio de la gasolina subirá mañana. ¿Qué pasa hoy con la demanda?', + respuestaCorrecta: true, + escenario: 'Todos los noticieros anuncian el aumento de precios' + }, + { + factorId: 'poblacion', + pregunta: 'Una nueva fábrica trae 5,000 trabajadores a una ciudad pequeña. ¿Qué pasa con la demanda de vivienda?', + respuestaCorrecta: true, + escenario: 'La población de la ciudad se duplica en un año' + }, + { + factorId: 'demografia', + pregunta: 'En un país, el 30% de la población tiene más de 60 años. ¿Qué servicios verán aumentada demanda?', + respuestaCorrecta: true, + escenario: 'La población está envejeciendo rápidamente' + } +]; + +export const FactoresDesplazanDemanda: React.FC = ({ + ejercicioId: _ejercicioId, + onComplete +}) => { + const [modo, setModo] = useState<'aprender' | 'practicar'>('aprender'); + const [preguntaActual, setPreguntaActual] = useState(0); + const [respuestas, setRespuestas] = useState([]); + const [mostrarFeedback, setMostrarFeedback] = useState(false); + const [score, setScore] = useState(0); + const [completado, setCompletado] = useState(false); + + const handleRespuesta = (respuesta: boolean) => { + const esCorrecta = respuesta === preguntas[preguntaActual].respuestaCorrecta; + setRespuestas(prev => [...prev, esCorrecta]); + setMostrarFeedback(true); + }; + + const handleSiguiente = () => { + if (preguntaActual < preguntas.length - 1) { + setPreguntaActual(prev => prev + 1); + setMostrarFeedback(false); + } else { + const correctas = respuestas.filter(r => r).length + (mostrarFeedback && respuestas.length === preguntaActual ? 1 : 0); + const puntuacion = Math.round((correctas / preguntas.length) * 100); + setScore(puntuacion); + setCompletado(true); + setTimeout(() => { + if (onComplete) { + onComplete(puntuacion); + } + }, 3000); + } + }; + + const reiniciar = () => { + setModo('aprender'); + setPreguntaActual(0); + setRespuestas([]); + setMostrarFeedback(false); + setScore(0); + setCompletado(false); + }; + + const pregunta = preguntas[preguntaActual]; + const factorActual = factores.find(f => f.id === pregunta.factorId); + const ultimaRespuestaCorrecta = respuestas[respuestas.length - 1]; + + if (completado) { + return ( +
+ + + +

¡Ejercicio Completado!

+

Has identificado correctamente {respuestas.filter(r => r).length} de {preguntas.length} factores

+
{score}/100
+

Puntuación final

+
+ ); + } + + if (modo === 'aprender') { + return ( +
+
+

Factores que Desplazan la Curva de Demanda

+

+ Estos 6 factores hacen que la curva de demanda se desplace (aumente o disminuya), + a diferencia de un movimiento a lo largo de la curva que solo el precio puede causar. +

+
+ +
+ {factores.map((factor, index) => ( + +
+
+ {factor.icono} +
+
+

{factor.nombre}

+

{factor.descripcion}

+
+ Ejemplo: {factor.ejemplo} +
+
+
+
+ ))} +
+ +
+ +
+
+ ); + } + + return ( +
+
+
+

Identifica el Factor

+ Pregunta {preguntaActual + 1} de {preguntas.length} +
+
+ +
+
+ + {factorActual && ( +
+
+ {factorActual.icono} +
+ Factor: {factorActual.nombre} +
+ )} + +
+
+ Escenario: +

{pregunta.escenario}

+
+

{pregunta.pregunta}

+
+ + + {mostrarFeedback && ( + +
+
+ {ultimaRespuestaCorrecta ? ( + + ) : ( + + )} +
+

+ {ultimaRespuestaCorrecta ? '¡Correcto!' : 'Incorrecto'} +

+ {factorActual && ( +

+ {factorActual.explicacion} +

+ )} +
+
+
+
+ )} +
+ +
+ {!mostrarFeedback ? ( + <> + + + + ) : ( + + )} +
+ + +
+ ); +}; + +export default FactoresDesplazanDemanda; diff --git a/frontend/src/components/exercises/modulo2/FactoresDesplazanOferta.tsx b/frontend/src/components/exercises/modulo2/FactoresDesplazanOferta.tsx new file mode 100644 index 0000000..0247a5e --- /dev/null +++ b/frontend/src/components/exercises/modulo2/FactoresDesplazanOferta.tsx @@ -0,0 +1,440 @@ +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { ArrowRightLeft, CheckCircle2, XCircle, Trophy, RotateCcw, Factory, DollarSign, Users, Zap, Truck, AlertTriangle } from 'lucide-react'; + +interface FactoresDesplazanOfertaProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +type Direccion = 'izquierda' | 'derecha' | 'ninguna'; +type Categoria = 'tecnologia' | 'insumos' | 'competidores' | 'expectativas' | 'impuestos'; + +interface Factor { + id: number; + nombre: string; + icono: React.ReactNode; + descripcion: string; + efecto: Direccion; + explicacion: string; + categoria: Categoria; + color: string; +} + +const factores: Factor[] = [ + { + id: 1, + nombre: 'Tecnología', + icono: , + descripcion: 'Nueva maquinaria reduce el tiempo de producción a la mitad', + efecto: 'derecha', + explicacion: 'La tecnología mejora la productividad, permitiendo producir más al mismo costo. La oferta aumenta (se desplaza a la derecha).', + categoria: 'tecnologia', + color: 'blue' + }, + { + id: 2, + nombre: 'Costo de Insumos', + icono: , + descripcion: 'El precio del petróleo (materia prima) sube un 50%', + efecto: 'izquierda', + explicacion: 'Al subir los costos de producción, es menos rentable fabricar. La oferta disminuye (se desplaza a la izquierda).', + categoria: 'insumos', + color: 'red' + }, + { + id: 3, + nombre: 'Número de Vendedores', + icono: , + descripcion: 'Muchas nuevas empresas entran al mercado', + efecto: 'derecha', + explicacion: 'Más vendedores en el mercado significa más producción total. La oferta aumenta (se desplaza a la derecha).', + categoria: 'competidores', + color: 'blue' + }, + { + id: 4, + nombre: 'Expectativas', + icono: , + descripcion: 'Los productores esperan que el precio suba el próximo mes', + efecto: 'izquierda', + explicacion: 'Si esperan precios más altos mañana, retienen producción hoy. La oferta actual disminuye (se desplaza a la izquierda).', + categoria: 'expectativas', + color: 'orange' + }, + { + id: 5, + nombre: 'Impuestos', + icono: , + descripcion: 'El gobierno elimina un impuesto a la producción', + efecto: 'derecha', + explicacion: 'Sin el impuesto, los costos de producción bajan. La oferta aumenta (se desplaza a la derecha).', + categoria: 'impuestos', + color: 'blue' + }, + { + id: 6, + nombre: 'Subsidios', + icono: , + descripcion: 'El gobierno cancela un subsidio a los agricultores', + efecto: 'izquierda', + explicacion: 'Sin el subsidio, los costos de producción suben. La oferta disminuye (se desplaza a la izquierda).', + categoria: 'impuestos', + color: 'red' + } +]; + +export const FactoresDesplazanOferta: React.FC = ({ + onComplete, + ejercicioId: _ejercicioId +}) => { + const [factorActual, setFactorActual] = useState(0); + const [respuestaSeleccionada, setRespuestaSeleccionada] = useState(null); + const [mostrarResultado, setMostrarResultado] = useState(false); + const [score, setScore] = useState(0); + const [respuestasCorrectas, setRespuestasCorrectas] = useState(0); + const [completado, setCompletado] = useState(false); + + const factor = factores[factorActual]; + + const handleSeleccionar = (direccion: Direccion) => { + if (mostrarResultado) return; + setRespuestaSeleccionada(direccion); + }; + + const handleVerificar = () => { + if (!respuestaSeleccionada) return; + + const esCorrecta = respuestaSeleccionada === factor.efecto; + setMostrarResultado(true); + + if (esCorrecta) { + setScore(prev => prev + Math.round(100 / factores.length)); + setRespuestasCorrectas(prev => prev + 1); + } + }; + + const handleSiguiente = () => { + if (factorActual < factores.length - 1) { + setFactorActual(prev => prev + 1); + setRespuestaSeleccionada(null); + setMostrarResultado(false); + } else { + setCompletado(true); + if (onComplete) { + onComplete(score); + } + } + }; + + const handleReiniciar = () => { + setFactorActual(0); + setRespuestaSeleccionada(null); + setMostrarResultado(false); + setScore(0); + setRespuestasCorrectas(0); + setCompletado(false); + }; + + const renderGrafico = () => { + const isRight = factor.efecto === 'derecha'; + + return ( + + {/* Grid */} + {Array.from({ length: 6 }).map((_, i) => ( + + + + + ))} + + {/* Ejes */} + + + + {/* Curva original S1 */} + + S₁ + + {/* Curva desplazada S2 */} + + + S₂ + + + {/* Flecha de dirección */} + {mostrarResultado && ( + + )} + + {/* Defs para flecha */} + + + + + + + {/* Labels */} + Cantidad + Precio + + ); + }; + + if (completado) { + const porcentaje = Math.round((respuestasCorrectas / factores.length) * 100); + + return ( + + +

¡Ejercicio Completado!

+

Has identificado los factores que desplazan la oferta

+ +
+
{porcentaje}%
+

+ {respuestasCorrectas} de {factores.length} respuestas correctas +

+
+ +
+
+

+ {factores.filter(f => f.efecto === 'derecha').length} +

+

Aumentan oferta →

+
+
+

+ {factores.filter(f => f.efecto === 'izquierda').length} +

+

Disminuyen oferta ←

+
+
+ + +
+ ); + } + + return ( +
+
+
+
+ +

Factores que Desplazan la Oferta

+
+
+ + {factorActual + 1} de {factores.length} + +
+ +
+
+
+

+ Identifica en qué dirección se desplaza la curva de oferta ante cada situación. +

+
+ +
+
+ +
+
+ {factor.icono} +
+

{factor.nombre}

+
+

{factor.descripcion}

+
+ +
+

¿Qué ocurre con la oferta?

+ + {(['izquierda', 'derecha'] as Direccion[]).map((direccion) => { + const isSelected = respuestaSeleccionada === direccion; + const isCorrect = mostrarResultado && direccion === factor.efecto; + const isWrong = mostrarResultado && isSelected && direccion !== factor.efecto; + + let buttonClass = 'w-full p-4 rounded-lg border-2 transition-all flex items-center gap-3 '; + + if (isCorrect) { + buttonClass += 'border-green-500 bg-green-50'; + } else if (isWrong) { + buttonClass += 'border-red-500 bg-red-50'; + } else if (isSelected) { + buttonClass += 'border-green-500 bg-green-50'; + } else { + buttonClass += 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'; + } + + return ( + handleSeleccionar(direccion)} + disabled={mostrarResultado} + whileHover={!mostrarResultado ? { scale: 1.02 } : {}} + whileTap={!mostrarResultado ? { scale: 0.98 } : {}} + className={buttonClass} + > + + → + + + {direccion === 'derecha' ? 'Aumenta (derecha)' : 'Disminuye (izquierda)'} + + {isCorrect && } + {isWrong && } + + ); + })} +
+ + + {mostrarResultado && ( + +
+ {respuestaSeleccionada === factor.efecto ? ( + + ) : ( + + )} +
+

+ {respuestaSeleccionada === factor.efecto ? '¡Correcto!' : 'Incorrecto'} +

+

{factor.explicacion}

+
+
+
+ )} +
+ +
+ {!mostrarResultado ? ( + + ) : ( + + )} +
+
+ +
+

Visualización del Desplazamiento

+ {renderGrafico()} + +
+
+
+ S₁: Oferta original +
+
+
+ S₂: Nueva oferta +
+
+ +
+

+ Recuerda: +

+
    +
  • → Derecha: Oferta aumenta
  • +
  • ← Izquierda: Oferta disminuye
  • +
  • • El precio del bien NO desplaza la curva
  • +
+
+
+
+ + {/* Indicadores de progreso */} +
+ {factores.map((_, index) => ( +
+ ))} +
+
+ ); +}; + +export default FactoresDesplazanOferta; diff --git a/frontend/src/components/exercises/modulo2/FactoresElasticidad.tsx b/frontend/src/components/exercises/modulo2/FactoresElasticidad.tsx new file mode 100644 index 0000000..84bb743 --- /dev/null +++ b/frontend/src/components/exercises/modulo2/FactoresElasticidad.tsx @@ -0,0 +1,448 @@ +import React, { useState } from 'react'; + +interface PreguntaFactor { + id: number; + pregunta: string; + opciones: string[]; + respuestaCorrecta: number; + explicacion: string; + categoria: 'sustitutos' | 'necesidad' | 'porcion' | 'tiempo' | 'definicion'; +} + +const preguntas: PreguntaFactor[] = [ + { + id: 1, + pregunta: "¿Qué sucede con la elasticidad de la demanda cuando aumenta la disponibilidad de bienes sustitutos?", + opciones: [ + "La elasticidad disminuye (se vuelve más inelástica)", + "La elasticidad aumenta (se vuelve más elástica)", + "La elasticidad no se ve afectada", + "La elasticidad se vuelve unitaria" + ], + respuestaCorrecta: 1, + explicacion: "Cuanto más sustitutos disponibles tenga un bien, más elástica será su demanda. Los consumidores pueden cambiar fácilmente a alternativas cuando el precio sube, haciendo que la cantidad demandada responda más al cambio de precio.", + categoria: 'sustitutos' + }, + { + id: 2, + pregunta: "¿Cuál de los siguientes bienes probablemente tenga la demanda más inelástica?", + opciones: [ + "Un yate de lujo", + "Agua embotellada en un día normal", + "Entradas para un concierto de una banda específica", + "Una marca particular de cereal" + ], + respuestaCorrecta: 1, + explicacion: "El agua es una necesidad básica sin sustitutos cercanos en la mayoría de situaciones. La demanda de necesidades es inelástica porque los consumidores la necesitan independientemente del precio, dentro de rangos razonables.", + categoria: 'necesidad' + }, + { + id: 3, + pregunta: "Si el precio de la sal aumenta un 50%, ¿por qué la cantidad demandada probablemente no cambie significativamente?", + opciones: [ + "Porque la sal es un lujo que todos quieren", + "Porque representa una pequeña porción del presupuesto y es una necesidad", + "Porque hay muchos sustitutos para la sal", + "Porque la ley prohíbe cambiar el consumo de sal" + ], + respuestaCorrecta: 1, + explicacion: "La sal representa una porción muy pequeña del ingreso de los consumidores y es una necesidad básica. Incluso si el precio sube mucho, el impacto económico es mínimo y no hay sustitutos directos para su función en la alimentación.", + categoria: 'porcion' + }, + { + id: 4, + pregunta: "¿Por qué la demanda de gasolina es más elástica a largo plazo que a corto plazo?", + opciones: [ + "Porque la gasolina es más barata a largo plazo", + "Porque los consumidores pueden ajustar su comportamiento (comprar autos eficientes, mudarse, etc.)", + "Porque hay más estaciones de gasolina a largo plazo", + "Porque el gobierno regula los precios a largo plazo" + ], + respuestaCorrecta: 1, + explicacion: "A corto plazo, los consumidores están 'atrapados' con sus vehículos y rutas actuales. A largo plazo, pueden hacer cambios significativos como comprar autos más eficientes, usar transporte público, mudarse más cerca del trabajo, etc., haciendo la demanda más sensible al precio.", + categoria: 'tiempo' + }, + { + id: 5, + pregunta: "¿Qué relación existe entre el lujo/necesidad y la elasticidad de la demanda?", + opciones: [ + "Los lujos tienen demanda inelástica; las necesidades tienen demanda elástica", + "Los lujos tienen demanda elástica; las necesidades tienen demanda inelástica", + "Ambos tienen la misma elasticidad", + "La elasticidad depende únicamente del precio, no del tipo de bien" + ], + respuestaCorrecta: 1, + explicacion: "Los bienes de lujo tienen demanda elástica porque son discrecionales - los consumidores pueden reducir su consumo o eliminarlo si el precio sube. Las necesidades tienen demanda inelástica porque se requieren independientemente del precio.", + categoria: 'definicion' + }, + { + id: 6, + pregunta: "Un bien representa el 30% del presupuesto mensual de una familia. ¿Qué podemos esperar sobre su elasticidad?", + opciones: [ + "Será inelástica porque representa una porción grande del presupuesto", + "Será elástica porque los cambios de precio tendrán impacto significativo", + "La elasticidad solo depende de si es necesidad o lujo", + "No se puede determinar sin más información" + ], + respuestaCorrecta: 1, + explicacion: "Cuando un bien representa una porción significativa del presupuesto, los consumidores son más sensibles a los cambios de precio. Un aumento de precio significaría un impacto sustancial en sus finanzas, por lo que buscarán alternativas o reducirán consumo, haciendo la demanda más elástica.", + categoria: 'porcion' + }, + { + id: 7, + pregunta: "¿Cuál factor NO es determinante de la elasticidad precio de la demanda?", + opciones: [ + "Disponibilidad de sustitutos", + "Naturaleza del bien (necesidad vs lujo)", + "Porción del ingreso que representa", + "El color del empaque del producto" + ], + respuestaCorrecta: 3, + explicacion: "El color del empaque puede afectar las preferencias pero no determina la elasticidad precio de la demanda. Los factores clave son: disponibilidad de sustitutos, naturaleza del bien (necesidad/lujo), porción del ingreso, y horizonte temporal (corto vs largo plazo).", + categoria: 'definicion' + }, + { + id: 8, + pregunta: "¿Por qué la demanda de medicamentos específicos para enfermedades crónicas es extremadamente inelástica?", + opciones: [ + "Porque son muy baratos", + "Porque no tienen sustitutos y son necesarios para la salud", + "Porque hay muchas marcas competidoras", + "Porque representan una pequeña porción del ingreso" + ], + respuestaCorrecta: 1, + explicacion: "Los medicamentos para enfermedades crónicas combinan dos factores de inelasticidad: son necesidades absolutas (sin ellos la salud se deteriora) y frecuentemente no tienen sustitutos terapéuticos equivalentes. Los pacientes deben comprarlos independientemente del precio.", + categoria: 'sustitutos' + } +]; + +export const FactoresElasticidad: React.FC = () => { + const [preguntaActual, setPreguntaActual] = useState(0); + const [respuestaSeleccionada, setRespuestaSeleccionada] = useState(null); + const [resultado, setResultado] = useState<{ + correcto: boolean; + mostrarResultado: boolean; + } | null>(null); + const [puntuacion, setPuntuacion] = useState(0); + const [respondidas, setRespondidas] = useState(0); + const [mostrarResumen, setMostrarResumen] = useState(false); + + const pregunta = preguntas[preguntaActual]; + + const verificarRespuesta = (indice: number) => { + if (resultado?.mostrarResultado) return; + + setRespuestaSeleccionada(indice); + const correcto = indice === pregunta.respuestaCorrecta; + + if (correcto) { + setPuntuacion(prev => prev + 1); + } + + setResultado({ + correcto, + mostrarResultado: true + }); + + setRespondidas(prev => prev + 1); + }; + + const siguientePregunta = () => { + if (preguntaActual < preguntas.length - 1) { + setPreguntaActual(prev => prev + 1); + setRespuestaSeleccionada(null); + setResultado(null); + } else { + setMostrarResumen(true); + } + }; + + const reiniciarQuiz = () => { + setPreguntaActual(0); + setRespuestaSeleccionada(null); + setResultado(null); + setPuntuacion(0); + setRespondidas(0); + setMostrarResumen(false); + }; + + const getCategoriaIcon = (categoria: string) => { + switch (categoria) { + case 'sustitutos': + return ( + + + + ); + case 'necesidad': + return ( + + + + ); + case 'porcion': + return ( + + + + ); + case 'tiempo': + return ( + + + + ); + default: + return ( + + + + ); + } + }; + + const getCategoriaNombre = (categoria: string) => { + switch (categoria) { + case 'sustitutos': return 'Sustitutos'; + case 'necesidad': return 'Lujo vs Necesidad'; + case 'porcion': return 'Porción del Ingreso'; + case 'tiempo': return 'Horizonte Temporal'; + default: return 'Definiciones'; + } + }; + + const getCategoriaColor = (categoria: string) => { + switch (categoria) { + case 'sustitutos': return 'bg-purple-100 text-purple-700 border-purple-200'; + case 'necesidad': return 'bg-pink-100 text-pink-700 border-pink-200'; + case 'porcion': return 'bg-green-100 text-green-700 border-green-200'; + case 'tiempo': return 'bg-blue-100 text-blue-700 border-blue-200'; + default: return 'bg-gray-100 text-gray-700 border-gray-200'; + } + }; + + if (mostrarResumen) { + const porcentaje = Math.round((puntuacion / preguntas.length) * 100); + + return ( +
+
+
+ {porcentaje}% +
+ +

¡Quiz Completado!

+

+ Has respondido {puntuacion} de {preguntas.length} preguntas correctamente +

+ +
+
+ Respuestas correctas: + {puntuacion} +
+
+ Respuestas incorrectas: + {preguntas.length - puntuacion} +
+
+
+
+
+ + +
+
+ ); + } + + return ( +
+
+
+

Factores de la Elasticidad

+

Identifica cómo diferentes factores afectan la elasticidad de la demanda.

+
+
+
+

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

+

{puntuacion}/{respondidas}

+
+
+
+ +
+
+
+
+
+ +
+
+
+ {getCategoriaIcon(pregunta.categoria)} +
+
+ + {getCategoriaNombre(pregunta.categoria)} + +

{pregunta.pregunta}

+
+
+
+ +
+ {pregunta.opciones.map((opcion, indice) => ( + + ))} +
+ + {resultado?.mostrarResultado && ( +
+
+
+ {resultado.correcto ? ( + + + + ) : ( + + + + )} +
+
+

+ {resultado.correcto ? '¡Respuesta Correcta!' : 'Respuesta Incorrecta'} +

+ +
+

Explicación:

+

{pregunta.explicacion}

+
+ + {!resultado.correcto && ( +

+ La respuesta correcta es: {pregunta.opciones[pregunta.respuestaCorrecta]} +

+ )} +
+
+
+ )} + + {resultado?.mostrarResultado && ( + + )} + +
+

+ + + + Factores Clave de la Elasticidad +

+
+
+
+ + + +
+

Sustitutos

+

Más sustitutos = más elástica

+
+
+
+ + + +
+

Necesidad

+

Necesidades = inelástica

+
+
+
+ + + +
+

Porción

+

% mayor del ingreso = más elástica

+
+
+
+ + + +
+

Tiempo

+

Largo plazo = más elástica

+
+
+
+
+ ); +}; + +export default FactoresElasticidad; diff --git a/frontend/src/components/exercises/modulo2/IdentificarShocks.tsx b/frontend/src/components/exercises/modulo2/IdentificarShocks.tsx new file mode 100644 index 0000000..25292d5 --- /dev/null +++ b/frontend/src/components/exercises/modulo2/IdentificarShocks.tsx @@ -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: , color: 'green' }, + { value: 'oferta-down', label: 'Oferta ↓', icon: , color: 'red' }, + { value: 'demanda-up', label: 'Demanda ↑', icon: , color: 'blue' }, + { value: 'demanda-down', label: 'Demanda ↓', icon: , color: 'orange' }, +]; + +export const IdentificarShocks: React.FC = ({ onComplete, ejercicioId: _ejercicioId }) => { + const [escenarioActual, setEscenarioActual] = useState(0); + const [respuestaSeleccionada, setRespuestaSeleccionada] = useState(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 ( + + {/* Grid */} + {Array.from({ length: 6 }).map((_, i) => ( + + + + + ))} + + {/* Ejes */} + + + + {/* Curva original */} + {isOferta ? ( + + ) : ( + + )} + + {isOferta ? 'S₁' : 'D₁'} + + + {/* Curva desplazada */} + + {isOferta ? ( + + ) : ( + + )} + + {isOferta ? 'S₂' : 'D₂'} + + + + {/* Flecha de dirección */} + + + {/* Defs para flechas */} + + + + + + + + + + ); + }; + + if (completado) { + const porcentaje = Math.round((respuestasCorrectas / escenarios.length) * 100); + + return ( + + +

¡Ejercicio Completado!

+

Has identificado shocks del mercado

+ +
+
{porcentaje}%
+

+ {respuestasCorrectas} de {escenarios.length} respuestas correctas +

+
+ + +
+ ); + } + + return ( +
+
+
+
+ +

Identificar Shocks del Mercado

+
+
+ + {escenario.dificultad.toUpperCase()} + + + {escenarioActual + 1} de {escenarios.length} + +
+ +
+
+
+

+ Lee cada escenario e identifica qué curva se desplaza y en qué dirección. +

+
+ +
+
+
+
+ +
+

Escenario {escenario.id}

+

{escenario.descripcion}

+
+
+
+ +
+ {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 ( + handleSeleccionar(opcion.value)} + disabled={mostrarResultado} + whileHover={!mostrarResultado ? { scale: 1.02 } : {}} + whileTap={!mostrarResultado ? { scale: 0.98 } : {}} + className={buttonClass} + > + {opcion.icon} + + {opcion.label} + + {isCorrect && } + {isWrong && } + + ); + })} +
+ + + {mostrarResultado && ( + +
+ {respuestaSeleccionada === escenario.respuesta ? ( + + ) : ( + + )} +
+

+ {respuestaSeleccionada === escenario.respuesta ? '¡Correcto!' : 'Incorrecto'} +

+

{escenario.explicacion}

+
+
+
+ )} +
+ +
+ {!mostrarResultado ? ( + + ) : ( + + )} +
+
+ +
+

Visualización del Shock

+ {renderGraficoShock()} + +
+

Leyenda:

+
+
+
+ Curva de Demanda (D) +
+
+
+ Curva de Oferta (S) +
+
+
+ Curva después del shock +
+
+
+ +
+

+ Tip: Recuerda que: +

+
    +
  • • Factores de oferta: tecnología, insumos, número de vendedores
  • +
  • • Factores de demanda: ingreso, preferencias, precios relacionados
  • +
+
+
+
+ +
+ + +
+ {escenarios.map((_, index) => ( +
+ ))} +
+ + +
+
+ ); +}; + +export default IdentificarShocks; diff --git a/frontend/src/components/exercises/modulo2/LeyDemandaQuiz.tsx b/frontend/src/components/exercises/modulo2/LeyDemandaQuiz.tsx new file mode 100644 index 0000000..221a5f1 --- /dev/null +++ b/frontend/src/components/exercises/modulo2/LeyDemandaQuiz.tsx @@ -0,0 +1,244 @@ +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { HelpCircle, Check, X, ArrowRight, Trophy } from 'lucide-react'; + +interface LeyDemandaQuizProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Pregunta { + id: number; + pregunta: string; + opciones: string[]; + correcta: number; + explicacion: string; +} + +const preguntas: Pregunta[] = [ + { + id: 1, + pregunta: "¿Qué sucede con la cantidad demandada cuando el precio de un bien aumenta?", + opciones: [ + "Aumenta proporcionalmente", + "Disminuye (ley de la demanda)", + "Se mantiene constante", + "Depende del tipo de bien" + ], + correcta: 1, + explicacion: "Según la Ley de la Demanda, existe una relación inversa entre precio y cantidad demandada: cuando el precio sube, la cantidad demandada baja." + }, + { + id: 2, + pregunta: "¿Cuál es la forma típica de la curva de demanda?", + opciones: [ + "Línea horizontal", + "Línea vertical", + "Pendiente descendente (de izquierda a derecha)", + "Pendiente ascendente" + ], + correcta: 2, + explicacion: "La curva de demanda tiene pendiente descendente porque a precios más bajos, los consumidores están dispuestos a comprar más cantidad." + }, + { + id: 3, + pregunta: "Si el precio del helado baja de $5 a $3, ¿qué esperamos que ocurra?", + opciones: [ + "La gente comprará menos helado", + "No cambiará la cantidad demandada", + "La gente comprará más helado", + "Solo los ricos comprarán helado" + ], + correcta: 2, + explicacion: "Una disminución en el precio genera un aumento en la cantidad demandada (movimiento a lo largo de la curva)." + }, + { + id: 4, + pregunta: "¿Qué representa el eje vertical (Y) en un gráfico de demanda?", + opciones: [ + "Cantidad demandada", + "Precio del bien", + "Ingreso de los consumidores", + "Tiempo" + ], + correcta: 1, + explicacion: "En un gráfico de demanda estándar, el eje Y representa el Precio y el eje X representa la Cantidad." + }, + { + id: 5, + pregunta: "Complete: 'A mayor precio, ______ cantidad demandada'", + opciones: [ + "Mayor", + "Menor", + "Igual", + "No hay relación" + ], + correcta: 1, + explicacion: "La ley de la demanda establece que a mayor precio, menor cantidad demandada (relación inversa)." + } +]; + +export const LeyDemandaQuiz: React.FC = ({ ejercicioId: _ejercicioId, onComplete }) => { + const [preguntaActual, setPreguntaActual] = useState(0); + const [respuestaSeleccionada, setRespuestaSeleccionada] = useState(null); + const [mostrarFeedback, setMostrarFeedback] = useState(false); + const [respuestasCorrectas, setRespuestasCorrectas] = useState(0); + const [completado, setCompletado] = useState(false); + + const pregunta = preguntas[preguntaActual]; + const esCorrecta = respuestaSeleccionada === pregunta.correcta; + + const handleSeleccionar = (index: number) => { + if (mostrarFeedback) return; + setRespuestaSeleccionada(index); + }; + + const handleValidar = () => { + if (respuestaSeleccionada === null) return; + + setMostrarFeedback(true); + if (esCorrecta) { + setRespuestasCorrectas(prev => prev + 1); + } + }; + + const handleSiguiente = () => { + if (preguntaActual < preguntas.length - 1) { + setPreguntaActual(prev => prev + 1); + setRespuestaSeleccionada(null); + setMostrarFeedback(false); + } else { + const puntuacion = Math.round((respuestasCorrectas + (esCorrecta ? 1 : 0)) / preguntas.length * 100); + setCompletado(true); + if (onComplete) { + onComplete(puntuacion); + } + } + }; + + const calcularProgreso = () => ((preguntaActual + 1) / preguntas.length) * 100; + + if (completado) { + const puntuacionFinal = Math.round(respuestasCorrectas / preguntas.length * 100); + return ( +
+ + + +

¡Quiz Completado!

+

Has respondido {respuestasCorrectas} de {preguntas.length} preguntas correctamente

+
{puntuacionFinal}/100
+

Puntuación final

+
+ ); + } + + return ( +
+
+
+

Quiz: Ley de la Demanda

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

{pregunta.pregunta}

+
+ {pregunta.opciones.map((opcion, index) => ( + handleSeleccionar(index)} + disabled={mostrarFeedback} + whileHover={!mostrarFeedback ? { scale: 1.02 } : {}} + whileTap={!mostrarFeedback ? { scale: 0.98 } : {}} + className={`w-full p-4 rounded-lg border-2 text-left transition-all ${ + respuestaSeleccionada === index + ? mostrarFeedback + ? index === pregunta.correcta + ? 'border-green-500 bg-green-50' + : 'border-red-500 bg-red-50' + : 'border-blue-500 bg-blue-50' + : mostrarFeedback && index === pregunta.correcta + ? 'border-green-500 bg-green-50' + : 'border-gray-200 hover:border-blue-300' + }`} + > +
+ {opcion} + {mostrarFeedback && index === pregunta.correcta && ( + + )} + {mostrarFeedback && respuestaSeleccionada === index && index !== pregunta.correcta && ( + + )} +
+
+ ))} +
+
+ + + {mostrarFeedback && ( + +
+
+ {esCorrecta ? ( + + ) : ( + + )} +
+

+ {esCorrecta ? '¡Correcto!' : 'Incorrecto'} +

+

+ {pregunta.explicacion} +

+
+
+
+
+ )} +
+ +
+ {!mostrarFeedback ? ( + + ) : ( + + )} +
+
+ ); +}; + +export default LeyDemandaQuiz; diff --git a/frontend/src/components/exercises/modulo2/LeyOfertaQuiz.tsx b/frontend/src/components/exercises/modulo2/LeyOfertaQuiz.tsx new file mode 100644 index 0000000..ba897b1 --- /dev/null +++ b/frontend/src/components/exercises/modulo2/LeyOfertaQuiz.tsx @@ -0,0 +1,340 @@ +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { TrendingUp, CheckCircle2, XCircle, Trophy, RotateCcw, BookOpen, ArrowRight, ArrowLeft } from 'lucide-react'; + +interface LeyOfertaQuizProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Pregunta { + id: number; + pregunta: string; + opciones: string[]; + respuestaCorrecta: number; + explicacion: string; +} + +const preguntas: Pregunta[] = [ + { + id: 1, + pregunta: 'Según la Ley de la Oferta, ¿qué ocurre cuando el precio de un bien aumenta?', + opciones: [ + 'Los productores ofrecen menos cantidad', + 'Los productores ofrecen más cantidad', + 'La cantidad ofrecida no cambia', + 'La demanda aumenta' + ], + respuestaCorrecta: 1, + explicacion: 'La Ley de la Oferta establece que existe una relación directa entre precio y cantidad ofrecida: cuando sube el precio, los productores quieren vender más.' + }, + { + id: 2, + pregunta: '¿Por qué la curva de oferta tiene pendiente positiva?', + opciones: [ + 'Porque a mayor precio, mayor es el costo de producción', + 'Porque los consumidores compran más cuando bajan los precios', + 'Porque a mayor precio, más rentable es producir y vender', + 'Porque el gobierno lo establece así' + ], + respuestaCorrecta: 2, + explicacion: 'La pendiente positiva refleja que a precios más altos, la producción es más rentable, incentivando a los productores a ofrecer más cantidad.' + }, + { + id: 3, + pregunta: 'Un agricultor vende manzanas. Si el precio pasa de $2 a $4 por kg, ¿qué esperamos?', + opciones: [ + 'Venderá la misma cantidad de siempre', + 'Querrá vender menos porque es más caro', + 'Querrá vender más manzanas al mercado', + 'Dejará de vender manzanas' + ], + respuestaCorrecta: 2, + explicacion: 'Al duplicar el precio, el agricultor tiene más incentivo para llevar más manzanas al mercado, aumentando su oferta.' + }, + { + id: 4, + pregunta: '¿Cuál de los siguientes es un movimiento A LO LARGO de la curva de oferta?', + opciones: [ + 'Mejora tecnológica que reduce costos', + 'Aumento del precio del petróleo (insumo)', + 'Subida del precio del bien, aumentando cantidad ofrecida', + 'Entrada de nuevos competidores al mercado' + ], + respuestaCorrecta: 2, + explicacion: 'El movimiento a lo largo de la curva ocurre solo cuando cambia el precio del propio bien. Los otros factores desplazan toda la curva.' + }, + { + id: 5, + pregunta: 'La relación precio-cantidad ofrecida es:', + opciones: [ + 'Inversa (negativa)', + 'Directa (positiva)', + 'No existe relación', + 'Depende del tipo de bien' + ], + respuestaCorrecta: 1, + explicacion: 'La relación es directa o positiva: a mayor precio, mayor cantidad ofrecida. Esto es lo opuesto a la demanda, que tiene relación inversa.' + } +]; + +export const LeyOfertaQuiz: React.FC = ({ onComplete, ejercicioId: _ejercicioId }) => { + const [preguntaActual, setPreguntaActual] = useState(0); + const [respuestaSeleccionada, setRespuestaSeleccionada] = useState(null); + const [mostrarResultado, setMostrarResultado] = useState(false); + const [score, setScore] = useState(0); + const [respuestasCorrectas, setRespuestasCorrectas] = useState(0); + const [completado, setCompletado] = useState(false); + + const pregunta = preguntas[preguntaActual]; + + const handleSeleccionar = (index: number) => { + if (mostrarResultado) return; + setRespuestaSeleccionada(index); + }; + + const handleVerificar = () => { + if (respuestaSeleccionada === null) return; + + const esCorrecta = respuestaSeleccionada === pregunta.respuestaCorrecta; + setMostrarResultado(true); + + if (esCorrecta) { + setScore(prev => prev + Math.round(100 / preguntas.length)); + setRespuestasCorrectas(prev => prev + 1); + } + }; + + const handleSiguiente = () => { + if (preguntaActual < preguntas.length - 1) { + setPreguntaActual(prev => prev + 1); + setRespuestaSeleccionada(null); + setMostrarResultado(false); + } else { + setCompletado(true); + if (onComplete) { + onComplete(score); + } + } + }; + + const handleReiniciar = () => { + setPreguntaActual(0); + setRespuestaSeleccionada(null); + setMostrarResultado(false); + setScore(0); + setRespuestasCorrectas(0); + setCompletado(false); + }; + + if (completado) { + const porcentaje = Math.round((respuestasCorrectas / preguntas.length) * 100); + + return ( + + +

¡Quiz Completado!

+

Has demostrado tu comprensión de la Ley de la Oferta

+ +
+
{porcentaje}%
+

+ {respuestasCorrectas} de {preguntas.length} respuestas correctas +

+
+ + +
+ ); + } + + return ( +
+
+
+
+ +

Ley de la Oferta

+
+
+ + {preguntaActual + 1} de {preguntas.length} + +
+ +
+
+
+

+ Responde las preguntas sobre la relación entre precio y cantidad ofrecida. +

+
+ +
+
+ +
+

Pregunta {pregunta.id}

+

{pregunta.pregunta}

+
+
+
+ +
+ {pregunta.opciones.map((opcion, index) => { + const isSelected = respuestaSeleccionada === index; + const isCorrect = mostrarResultado && index === pregunta.respuestaCorrecta; + const isWrong = mostrarResultado && isSelected && index !== pregunta.respuestaCorrecta; + + let buttonClass = 'w-full p-4 rounded-lg border-2 text-left transition-all flex items-center gap-3 '; + + if (isCorrect) { + buttonClass += 'border-green-500 bg-green-50'; + } else if (isWrong) { + buttonClass += 'border-red-500 bg-red-50'; + } else if (isSelected) { + buttonClass += 'border-green-500 bg-green-50'; + } else { + buttonClass += 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'; + } + + return ( + handleSeleccionar(index)} + disabled={mostrarResultado} + whileHover={!mostrarResultado ? { scale: 1.01 } : {}} + whileTap={!mostrarResultado ? { scale: 0.99 } : {}} + className={buttonClass} + > + + {String.fromCharCode(65 + index)} + + + {opcion} + + {isCorrect && } + {isWrong && } + + ); + })} +
+ + + {mostrarResultado && ( + +
+ {respuestaSeleccionada === pregunta.respuestaCorrecta ? ( + + ) : ( + + )} +
+

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

+

{pregunta.explicacion}

+
+
+
+ )} +
+ +
+ {!mostrarResultado ? ( + + ) : ( + + )} +
+ +
+ + +
+ {preguntas.map((_, index) => ( +
+ ))} +
+ + +
+
+ ); +}; + +export default LeyOfertaQuiz; diff --git a/frontend/src/components/exercises/modulo2/OfertaCortoLargoPlazo.tsx b/frontend/src/components/exercises/modulo2/OfertaCortoLargoPlazo.tsx new file mode 100644 index 0000000..0954d55 --- /dev/null +++ b/frontend/src/components/exercises/modulo2/OfertaCortoLargoPlazo.tsx @@ -0,0 +1,443 @@ +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Clock, CheckCircle2, XCircle, Trophy, RotateCcw, TrendingUp, AlertCircle, BookOpen } from 'lucide-react'; + +interface OfertaCortoLargoPlazoProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Escenario { + id: number; + tipo: 'corto' | 'largo'; + descripcion: string; + tiempo: string; + opciones: string[]; + respuestaCorrecta: number; + explicacion: string; +} + +const escenarios: Escenario[] = [ + { + id: 1, + tipo: 'corto', + descripcion: 'El precio del café sube de $5 a $10 por libra. Los agricultores tienen 1 mes para reaccionar.', + tiempo: 'Plazo: 1 mes', + opciones: [ + 'Pueden plantar más árboles de café y aumentar significativamente la producción', + 'Solo pueden cosechar más del cultivo existente, aumento limitado de oferta', + 'La oferta no cambia en absoluto', + 'Pueden contratar más trabajadores inmediatamente y duplicar la producción' + ], + respuestaCorrecta: 1, + explicacion: 'En el corto plazo, los agricultores no pueden plantar nuevos árboles (toman 3-4 años en producir). Solo pueden cosechar más del cultivo existente, por lo que el aumento de oferta es limitado.' + }, + { + id: 2, + tipo: 'largo', + descripcion: 'El precio del café se mantiene alto a $10 por libra durante 5 años. Los agricultores pueden planificar a futuro.', + tiempo: 'Plazo: 5 años', + opciones: [ + 'La oferta permanece igual que al inicio', + 'Solo pueden vender lo que ya tenían almacenado', + 'Pueden plantar nuevos árboles, expandir fincas y aumentar significativamente la oferta', + 'El gobierno controla cuánto pueden producir' + ], + respuestaCorrecta: 2, + explicacion: 'En el largo plazo, los agricultores pueden hacer todo: plantar nuevos árboles, comprar más tierra, invertir en tecnología. La oferta es mucho más elástica.' + }, + { + id: 3, + tipo: 'corto', + descripcion: 'Una fábrica de autos recibe un pedido urgente. Necesita aumentar la producción esta semana.', + tiempo: 'Plazo: 1 semana', + opciones: [ + 'Puede construir una nueva planta de producción rápidamente', + 'Solo puede aumentar turnos existentes y usar inventarios, aumento limitado', + 'Puede contratar y entrenar a 500 nuevos trabajadores en 2 días', + 'La producción se duplica automáticamente' + ], + respuestaCorrecta: 1, + explicacion: 'En el corto plazo, la fábrica no puede construir nuevas instalaciones. Solo puede aumentar turnos, usar inventarios o pedir horas extras. La capacidad de aumentar oferta es limitada.' + }, + { + id: 4, + tipo: 'largo', + descripcion: 'La demanda de software crece constantemente durante 3 años. Las empresas tecnológicas responden.', + tiempo: 'Plazo: 3 años', + opciones: [ + 'No pueden hacer nada, la oferta de programadores es fija', + 'Pueden contratar algunos freelancers temporalmente', + 'Pueden contratar y formar programadores, expandir oficinas, adaptar toda su capacidad', + 'El precio sube pero la cantidad ofrecida no cambia' + ], + respuestaCorrecta: 2, + explicacion: 'En el largo plazo, las empresas pueden formar nuevos programadores (universidades, bootcamps), abrir oficinas en nuevas ciudades, adaptar completamente su capacidad productiva.' + }, + { + id: 5, + tipo: 'corto', + descripcion: 'Un huracán destruye refinerías de petróleo. El precio sube. ¿Qué pueden hacer otras refinerías?', + tiempo: 'Plazo: Inmediato', + opciones: [ + 'Construir nuevas refinerías en un mes', + 'Operar al máximo de su capacidad existente, aumento muy limitado', + 'Descubrir petróleo nuevo en semanas', + 'La oferta de petróleo es infinitamente elástica' + ], + respuestaCorrecta: 1, + explicacion: 'Las refinerías existentes ya operan cerca de su capacidad máxima. En el corto plazo no pueden construir nuevas instalaciones (toma años). Solo pueden intentar operar al máximo.' + } +]; + +export const OfertaCortoLargoPlazo: React.FC = ({ + onComplete, + ejercicioId: _ejercicioId +}) => { + const [escenarioActual, setEscenarioActual] = useState(0); + const [respuestaSeleccionada, setRespuestaSeleccionada] = useState(null); + const [mostrarResultado, setMostrarResultado] = useState(false); + const [score, setScore] = useState(0); + const [respuestasCorrectas, setRespuestasCorrectas] = useState(0); + const [completado, setCompletado] = useState(false); + + const escenario = escenarios[escenarioActual]; + + const handleSeleccionar = (index: number) => { + if (mostrarResultado) return; + setRespuestaSeleccionada(index); + }; + + const handleVerificar = () => { + if (respuestaSeleccionada === null) return; + + const esCorrecta = respuestaSeleccionada === escenario.respuestaCorrecta; + 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); + }; + + if (completado) { + const porcentaje = Math.round((respuestasCorrectas / escenarios.length) * 100); + + return ( + + +

¡Ejercicio Completado!

+

Has comprendido la elasticidad temporal de la oferta

+ +
+
{porcentaje}%
+

+ {respuestasCorrectas} de {escenarios.length} respuestas correctas +

+
+ +
+
+ +

Corto Plazo

+

Oferta inelástica

+
+
+ +

Largo Plazo

+

Oferta más elástica

+
+
+ + +
+ ); + } + + return ( +
+
+
+
+ +

Oferta: Corto vs Largo Plazo

+
+
+ + {escenario.tipo === 'corto' ? 'CORTO PLAZO' : 'LARGO PLAZO'} + + + {escenarioActual + 1} de {escenarios.length} + +
+ +
+
+
+

+ Determina si el escenario describe el corto plazo (oferta inelástica) o largo plazo (oferta elástica). +

+
+ +
+
+ +
+ +
+

+ Escenario {escenario.id} +

+

+ {escenario.tiempo} +

+

{escenario.descripcion}

+
+
+
+ +
+ {escenario.opciones.map((opcion, index) => { + const isSelected = respuestaSeleccionada === index; + const isCorrect = mostrarResultado && index === escenario.respuestaCorrecta; + const isWrong = mostrarResultado && isSelected && index !== escenario.respuestaCorrecta; + + let buttonClass = 'w-full p-4 rounded-lg border-2 text-left transition-all '; + + if (isCorrect) { + buttonClass += 'border-green-500 bg-green-50'; + } else if (isWrong) { + buttonClass += 'border-red-500 bg-red-50'; + } else if (isSelected) { + buttonClass += 'border-green-500 bg-green-50'; + } else { + buttonClass += 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'; + } + + return ( + handleSeleccionar(index)} + disabled={mostrarResultado} + whileHover={!mostrarResultado ? { scale: 1.01 } : {}} + whileTap={!mostrarResultado ? { scale: 0.99 } : {}} + className={buttonClass} + > +
+ + {String.fromCharCode(65 + index)} + + + {opcion} + + {isCorrect && } + {isWrong && } +
+
+ ); + })} +
+ + + {mostrarResultado && ( + +
+ {respuestaSeleccionada === escenario.respuestaCorrecta ? ( + + ) : ( + + )} +
+

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

+

{escenario.explicacion}

+
+
+
+ )} +
+ +
+ {!mostrarResultado ? ( + + ) : ( + + )} +
+
+ +
+ {/* Gráfico de elasticidad */} +
+

Elasticidad de la Oferta

+ + {/* Grid */} + {Array.from({ length: 6 }).map((_, i) => ( + + + + + ))} + + {/* Ejes */} + + + + {/* Curva corto plazo (más vertical) */} + + CP + + {/* Curva largo plazo (más horizontal) */} + + LP + + {/* Labels */} + Cantidad + Precio + + +
+
+
+ Corto plazo: inelástica +
+
+
+ Largo plazo: elástica +
+
+
+ + {/* Info boxes */} +
+
+ +

Corto Plazo

+
+
    +
  • • Algunos factores son fijos
  • +
  • • Difícil cambiar capacidad
  • +
  • • Oferta poco sensible a precios
  • +
+
+ +
+
+ +

Largo Plazo

+
+
    +
  • • Todos los factores son variables
  • +
  • • Pueden expandir capacidad
  • +
  • • Oferta muy sensible a precios
  • +
+
+
+
+ + {/* Indicadores de progreso */} +
+ {escenarios.map((_, index) => ( +
+ ))} +
+
+ ); +}; + +export default OfertaCortoLargoPlazo; diff --git a/frontend/src/components/exercises/modulo2/PrecioMaximoTecho.tsx b/frontend/src/components/exercises/modulo2/PrecioMaximoTecho.tsx new file mode 100644 index 0000000..cd893c8 --- /dev/null +++ b/frontend/src/components/exercises/modulo2/PrecioMaximoTecho.tsx @@ -0,0 +1,412 @@ +import React, { useState, useMemo } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { ArrowDown, AlertTriangle, Home, Scale, Info, CheckCircle2, XCircle } from 'lucide-react'; + +interface PrecioMaximoTechoProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Escenario { + id: number; + nombre: string; + descripcion: string; + pe: number; + qe: number; + pmax: number; + contexto: string; + consecuencias: string[]; + icono: React.ReactNode; +} + +const escenarios: Escenario[] = [ + { + id: 1, + nombre: "Control de Alquileres", + descripcion: "El gobierno establece un precio máximo de $600 para apartamentos cuando el equilibrio está en $800.", + pe: 800, + qe: 50, + pmax: 600, + contexto: "Mercado de vivienda en alquiler", + consecuencias: [ + "Escasez de apartamentos disponibles", + "Listas de espera cada vez más largas", + "Deterioro de la calidad de las viviendas", + "Mercado negro de alquileres" + ], + icono: + }, + { + id: 2, + nombre: "Gasolina Subsidiada", + descripcion: "El precio de la gasolina se congela en $3/galón cuando el precio de mercado es $5/galón.", + pe: 5, + qe: 100, + pmax: 3, + contexto: "Mercado de combustibles", + consecuencias: [ + "Largas filas en gasolineras", + "Desabastecimiento periódico", + "Contrabando de combustible", + "Inversión insuficiente en refinación" + ], + icono: + }, + { + id: 3, + nombre: "Medicamentos Esenciales", + descripcion: "Precio máximo en medicamentos básicos: $20 cuando cuestan $35 producirlos.", + pe: 35, + qe: 80, + pmax: 20, + contexto: "Mercado farmacéutico", + consecuencias: [ + "Desaparición de medicamentos del mercado", + "Reducción de la investigación", + "Mercado negro de medicinas", + "Importación irregular" + ], + icono: + } +]; + +export const PrecioMaximoTecho: React.FC = ({ onComplete, ejercicioId: _ejercicioId }) => { + const [escenarioActual, setEscenarioActual] = useState(0); + const [respuestas, setRespuestas] = useState>({}); + const [mostrarExplicacion, setMostrarExplicacion] = useState(false); + const [score, setScore] = useState(0); + + const escenario = escenarios[escenarioActual]; + + // Cálculos para el gráfico + const calcularInterseccion = (precio: number) => { + const qd = Math.max(0, escenario.qe + (escenario.pe - precio) * 2); + const qo = Math.max(0, escenario.qe - (escenario.pe - precio) * 1.5); + return { qd, qo }; + }; + + const datosGrafico = useMemo(() => { + const { qd, qo } = calcularInterseccion(escenario.pmax); + const excesoDemanda = Math.max(0, qd - qo); + return { + qd, + qo, + excesoDemanda, + cantidadTransada: Math.min(qd, qo) + }; + }, [escenario]); + + const verificarRespuesta = (hayEscasez: boolean) => { + const correcto = hayEscasez === true; + setRespuestas(prev => ({ ...prev, [escenario.id]: correcto })); + setMostrarExplicacion(true); + + if (correcto) { + setScore(prev => prev + 33); + } + + if (escenarioActual === escenarios.length - 1 && correcto) { + setTimeout(() => { + onComplete?.(Math.min(100, score + 33)); + }, 2000); + } + }; + + const siguienteEscenario = () => { + if (escenarioActual < escenarios.length - 1) { + setEscenarioActual(prev => prev + 1); + setMostrarExplicacion(false); + } + }; + + // Configuración del gráfico SVG + const width = 400; + const height = 300; + const padding = 50; + const graphWidth = width - 2 * padding; + const graphHeight = height - 2 * padding; + + const maxP = Math.max(escenario.pe, escenario.pmax) * 1.2; + const maxQ = escenario.qe * 1.5; + + const scaleX = (q: number) => padding + (q / maxQ) * graphWidth; + const scaleY = (p: number) => padding + graphHeight - (p / maxP) * graphHeight; + + // Generar puntos de curvas + const puntosDemanda = []; + const puntosOferta = []; + + for (let q = 0; q <= maxQ; q += 2) { + const pd = escenario.pe + (escenario.pe / escenario.qe) * (escenario.qe - q); + const po = escenario.pe * 0.3 + (escenario.pe / escenario.qe) * 0.7 * q; + if (pd > 0 && pd <= maxP) puntosDemanda.push({ q, p: pd }); + if (po > 0 && po <= maxP) puntosOferta.push({ q, p: po }); + } + + const pathDemanda = puntosDemanda.map((p, i) => + `${i === 0 ? 'M' : 'L'} ${scaleX(p.q)} ${scaleY(p.p)}` + ).join(' '); + + const pathOferta = puntosOferta.map((p, i) => + `${i === 0 ? 'M' : 'L'} ${scaleX(p.q)} ${scaleY(p.p)}` + ).join(' '); + + return ( +
+ {/* Header */} +
+
+
+
+ +
+
+

Precio Máximo (Techo)

+

Analiza los efectos de los controles de precios máximos

+
+
+
+ Ejercicio {escenarioActual + 1} de {escenarios.length} +
+ +
+
+
+
+ + {/* Contenido principal */} +
+ {/* Panel izquierdo: Escenario y gráfico */} +
+ {/* Tarjeta del escenario */} + +
+
+ {escenario.icono} +
+
+

{escenario.nombre}

+ {escenario.contexto} +
+
+

{escenario.descripcion}

+
+ + {/* Gráfico interactivo */} +
+

+ + Análisis Gráfico +

+ + {/* Grid */} + {Array.from({ length: 6 }).map((_, i) => ( + + + + + ))} + + {/* Ejes */} + + + + {/* Etiquetas */} + Cantidad + Precio + + {/* Curva de demanda */} + + D + + {/* Curva de oferta */} + + S + + {/* Punto de equilibrio */} + + E + + {/* Línea de precio máximo */} + + Pmax + + {/* Puntos de intersección con Pmax */} + + + + {/* Zona de escasez */} + {datosGrafico.excesoDemanda > 0 && ( + + + + Escasez: {datosGrafico.excesoDemanda.toFixed(1)} + + + )} + +
+
+ + {/* Panel derecho: Pregunta y consecuencias */} +
+ {/* Pregunta */} + + {!mostrarExplicacion ? ( + +

+ ¿Qué ocurrirá en este mercado con el precio máximo establecido? +

+
+ + +
+
+ ) : ( + +
+ {respuestas[escenario.id] ? ( + + ) : ( + + )} +

+ {respuestas[escenario.id] ? '¡Correcto!' : 'Incorrecto'} +

+
+

+ Al fijar un precio máximo por debajo del precio de equilibrio (${escenario.pe}), + se crea una escasez porque: +

+
    +
  • + + Los productores reducen la cantidad ofrecida a {datosGrafico.qo.toFixed(1)} unidades +
  • +
  • + + Los consumidores aumentan la cantidad demandada a {datosGrafico.qd.toFixed(1)} unidades +
  • +
  • + ! + Resultado: Exceso de demanda de {datosGrafico.excesoDemanda.toFixed(1)} unidades +
  • +
+ + {escenarioActual < escenarios.length - 1 ? ( + + ) : ( +
+

¡Ejercicio completado!

+

Has analizado todos los escenarios

+
+ )} +
+ )} +
+ + {/* Consecuencias */} +
+

+ + Consecuencias Típicas +

+
    + {escenario.consecuencias.map((consecuencia, idx) => ( + + + {consecuencia} + + ))} +
+
+
+
+
+ ); +}; + +export default PrecioMaximoTecho; diff --git a/frontend/src/components/exercises/modulo2/PrecioMinimoPiso.tsx b/frontend/src/components/exercises/modulo2/PrecioMinimoPiso.tsx new file mode 100644 index 0000000..b7d90ff --- /dev/null +++ b/frontend/src/components/exercises/modulo2/PrecioMinimoPiso.tsx @@ -0,0 +1,432 @@ +import React, { useState, useMemo } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { ArrowUp, AlertTriangle, Briefcase, Wheat, Info, CheckCircle2, XCircle } from 'lucide-react'; + +interface PrecioMinimoPisoProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Escenario { + id: number; + nombre: string; + descripcion: string; + pe: number; + qe: number; + pmin: number; + contexto: string; + consecuencias: string[]; + icono: React.ReactNode; +} + +const escenarios: Escenario[] = [ + { + id: 1, + nombre: "Salario Mínimo", + descripcion: "El salario mínimo se fija en $15/hora cuando el equilibrio del mercado laboral está en $10/hora.", + pe: 10, + qe: 1000, + pmin: 15, + contexto: "Mercado laboral", + consecuencias: [ + "Reducción de la demanda de trabajadores", + "Aumento del desempleo", + "Beneficio para trabajadores que conservan empleo", + "Posible mercado laboral informal" + ], + icono: + }, + { + id: 2, + nombre: "Precio de Soporte Agrícola", + descripcion: "El gobierno garantiza $5/bushel de trigo cuando el precio de mercado es $3/bushel.", + pe: 3, + qe: 500, + pmin: 5, + contexto: "Mercado agrícola", + consecuencias: [ + "Superávit de producción agrícola", + "El gobierno debe comprar el exceso", + "Costos fiscales significativos", + "Posible despilfarro de recursos" + ], + icono: + }, + { + id: 3, + nombre: "Tarifa Mínima de Taxis", + descripcion: "La tarifa mínima se establece en $25 cuando el precio de equilibrio es $15 por viaje.", + pe: 15, + qe: 200, + pmin: 25, + contexto: "Mercado de transporte", + consecuencias: [ + "Menor demanda de servicios de taxi", + "Exceso de oferta (taxis vacíos)", + "Aparición de competencia informal", + "Beneficio para conductores con clientes" + ], + icono: + } +]; + +export const PrecioMinimoPiso: React.FC = ({ onComplete, ejercicioId: _ejercicioId }) => { + const [escenarioActual, setEscenarioActual] = useState(0); + const [respuestas, setRespuestas] = useState>({}); + const [mostrarExplicacion, setMostrarExplicacion] = useState(false); + const [score, setScore] = useState(0); + + const escenario = escenarios[escenarioActual]; + + // Cálculos para el gráfico + const calcularInterseccion = (precio: number) => { + const qd = Math.max(0, escenario.qe - (precio - escenario.pe) * 1.5); + const qo = Math.max(0, escenario.qe + (precio - escenario.pe) * 2); + return { qd, qo }; + }; + + const datosGrafico = useMemo(() => { + const { qd, qo } = calcularInterseccion(escenario.pmin); + const excesoOferta = Math.max(0, qo - qd); + return { + qd, + qo, + excesoOferta, + cantidadTransada: Math.min(qd, qo) + }; + }, [escenario]); + + const verificarRespuesta = (haySuperavit: boolean) => { + const correcto = haySuperavit === true; + setRespuestas(prev => ({ ...prev, [escenario.id]: correcto })); + setMostrarExplicacion(true); + + if (correcto) { + setScore(prev => prev + 33); + } + + if (escenarioActual === escenarios.length - 1 && correcto) { + setTimeout(() => { + onComplete?.(Math.min(100, score + 33)); + }, 2000); + } + }; + + const siguienteEscenario = () => { + if (escenarioActual < escenarios.length - 1) { + setEscenarioActual(prev => prev + 1); + setMostrarExplicacion(false); + } + }; + + // Configuración del gráfico SVG + const width = 400; + const height = 300; + const padding = 50; + const graphWidth = width - 2 * padding; + const graphHeight = height - 2 * padding; + + const maxP = escenario.pmin * 1.2; + const maxQ = Math.max(datosGrafico.qo, escenario.qe) * 1.3; + + const scaleX = (q: number) => padding + (q / maxQ) * graphWidth; + const scaleY = (p: number) => padding + graphHeight - (p / maxP) * graphHeight; + + // Generar puntos de curvas + const puntosDemanda = []; + const puntosOferta = []; + + for (let q = 0; q <= maxQ; q += 5) { + const pd = escenario.pe + (escenario.pe / escenario.qe) * (escenario.qe - q); + const po = escenario.pe * 0.5 + (escenario.pe / escenario.qe) * q; + if (pd > 0 && pd <= maxP) puntosDemanda.push({ q, p: pd }); + if (po > 0 && po <= maxP) puntosOferta.push({ q, p: po }); + } + + const pathDemanda = puntosDemanda.map((p, i) => + `${i === 0 ? 'M' : 'L'} ${scaleX(p.q)} ${scaleY(p.p)}` + ).join(' '); + + const pathOferta = puntosOferta.map((p, i) => + `${i === 0 ? 'M' : 'L'} ${scaleX(p.q)} ${scaleY(p.p)}` + ).join(' '); + + return ( +
+ {/* Header */} +
+
+
+
+ +
+
+

Precio Mínimo (Piso)

+

Analiza los efectos de los precios mínimos o precios de soporte

+
+
+
+ Ejercicio {escenarioActual + 1} de {escenarios.length} +
+ +
+
+
+
+ + {/* Contenido principal */} +
+ {/* Panel izquierdo: Escenario y gráfico */} +
+ {/* Tarjeta del escenario */} + +
+
+ {escenario.icono} +
+
+

{escenario.nombre}

+ {escenario.contexto} +
+
+

{escenario.descripcion}

+
+ + {/* Gráfico interactivo */} +
+

+ + Análisis Gráfico +

+ + {/* Grid */} + {Array.from({ length: 6 }).map((_, i) => ( + + + + + ))} + + {/* Ejes */} + + + + {/* Etiquetas */} + Cantidad + Precio + + {/* Curva de demanda */} + + D + + {/* Curva de oferta */} + + S + + {/* Punto de equilibrio */} + + E + + {/* Línea de precio mínimo */} + + Pmin + + {/* Puntos de intersección con Pmin */} + + + + {/* Zona de superávit */} + {datosGrafico.excesoOferta > 0 && ( + + + + Superávit: {datosGrafico.excesoOferta.toFixed(0)} + + + )} + + {/* Flechas indicadoras */} + {datosGrafico.excesoOferta > 0 && ( + + + + + + + + + )} + +
+
+ + {/* Panel derecho: Pregunta y consecuencias */} +
+ {/* Pregunta */} + + {!mostrarExplicacion ? ( + +

+ ¿Qué ocurrirá en este mercado con el precio mínimo establecido? +

+
+ + +
+
+ ) : ( + +
+ {respuestas[escenario.id] ? ( + + ) : ( + + )} +

+ {respuestas[escenario.id] ? '¡Correcto!' : 'Incorrecto'} +

+
+

+ Al fijar un precio mínimo por encima del precio de equilibrio (${escenario.pe}), + se crea un superávit porque: +

+
    +
  • + + Los productores aumentan la cantidad ofrecida a {datosGrafico.qo.toFixed(0)} unidades +
  • +
  • + + Los consumidores reducen la cantidad demandada a {datosGrafico.qd.toFixed(0)} unidades +
  • +
  • + ! + Resultado: Exceso de oferta de {datosGrafico.excesoOferta.toFixed(0)} unidades +
  • +
+ + {escenarioActual < escenarios.length - 1 ? ( + + ) : ( +
+

¡Ejercicio completado!

+

Has analizado todos los escenarios

+
+ )} +
+ )} +
+ + {/* Consecuencias */} +
+

+ + Consecuencias Típicas +

+
    + {escenario.consecuencias.map((consecuencia, idx) => ( + + + {consecuencia} + + ))} +
+
+
+
+
+ ); +}; + +export default PrecioMinimoPiso; diff --git a/frontend/src/components/exercises/modulo2/SimuladorControles.tsx b/frontend/src/components/exercises/modulo2/SimuladorControles.tsx new file mode 100644 index 0000000..ace44a2 --- /dev/null +++ b/frontend/src/components/exercises/modulo2/SimuladorControles.tsx @@ -0,0 +1,600 @@ +import React, { useState, useMemo } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Play, RotateCcw, TrendingDown, TrendingUp, Scale, AlertTriangle, Calculator, BarChart3 } from 'lucide-react'; + +interface SimuladorControlesProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface ResultadoSimulacion { + tipo: 'equilibrio' | 'precio-maximo' | 'precio-minimo'; + precio: number; + cantidad: number; + excesoDemanda: number; + excesoOferta: number; + pesoMuerto: number; + mensaje: string; +} + +interface EscenarioPredefinido { + id: string; + nombre: string; + descripcion: string; + tipo: 'maximo' | 'minimo' | 'libre'; + precio: number; + pe: number; + qe: number; +} + +const escenariosPredefinidos: EscenarioPredefinido[] = [ + { + id: 'libre', + nombre: "Mercado Libre", + descripcion: "Sin intervención gubernamental", + tipo: 'libre', + precio: 0, + pe: 50, + qe: 100 + }, + { + id: 'rent-control', + nombre: "Control de Alquileres", + descripcion: "Precio máximo de $35 (equilibrio: $50)", + tipo: 'maximo', + precio: 35, + pe: 50, + qe: 100 + }, + { + id: 'salario-minimo', + nombre: "Salario Mínimo", + descripcion: "Precio mínimo de $65 (equilibrio: $50)", + tipo: 'minimo', + precio: 65, + pe: 50, + qe: 100 + } +]; + +export const SimuladorControles: React.FC = ({ onComplete, ejercicioId: _ejercicioId }) => { + const [escenarioActivo, setEscenarioActivo] = useState(null); + const [precioControl, setPrecioControl] = useState(50); + const [tipoControl, setTipoControl] = useState<'maximo' | 'minimo' | null>(null); + const [historial, setHistorial] = useState([]); + const [score, setScore] = useState(0); + + const pe = 50; // Precio de equilibrio + const qe = 100; // Cantidad de equilibrio + + // Funciones de demanda y oferta lineales + const calcularCantidades = (p: number) => { + // Demanda: Qd = 150 - 1*P (pendiente negativa) + const qd = Math.max(0, 150 - p); + // Oferta: Qo = 0 + 2*P (pendiente positiva) + const qo = Math.max(0, 2 * p); + return { qd, qo }; + }; + + const resultado = useMemo((): ResultadoSimulacion => { + if (!tipoControl) { + return { + tipo: 'equilibrio', + precio: pe, + cantidad: qe, + excesoDemanda: 0, + excesoOferta: 0, + pesoMuerto: 0, + mensaje: 'Mercado en equilibrio libre' + }; + } + + const { qd, qo } = calcularCantidades(precioControl); + const cantidadTransada = Math.min(qd, qo); + const excesoDemanda = Math.max(0, qd - qo); + const excesoOferta = Math.max(0, qo - qd); + + // Calcular pérdida de peso muerto (triángulo) + const base = qe - cantidadTransada; + const altura = tipoControl === 'maximo' + ? pe - precioControl + : precioControl - pe; + const pesoMuerto = 0.5 * base * altura; + + if (tipoControl === 'maximo' && precioControl < pe) { + return { + tipo: 'precio-maximo', + precio: precioControl, + cantidad: cantidadTransada, + excesoDemanda, + excesoOferta: 0, + pesoMuerto: Math.max(0, pesoMuerto), + mensaje: `Precio máximo crea escasez de ${excesoDemanda.toFixed(1)} unidades` + }; + } + + if (tipoControl === 'minimo' && precioControl > pe) { + return { + tipo: 'precio-minimo', + precio: precioControl, + cantidad: cantidadTransada, + excesoDemanda: 0, + excesoOferta, + pesoMuerto: Math.max(0, pesoMuerto), + mensaje: `Precio mínimo crea superávit de ${excesoOferta.toFixed(1)} unidades` + }; + } + + return { + tipo: 'equilibrio', + precio: precioControl, + cantidad: Math.min(qd, qo), + excesoDemanda: 0, + excesoOferta: 0, + pesoMuerto: 0, + mensaje: tipoControl === 'maximo' + ? 'El precio máximo no es restrictivo (está por encima del equilibrio)' + : 'El precio mínimo no es restrictivo (está por debajo del equilibrio)' + }; + }, [precioControl, tipoControl]); + + const aplicarEscenario = (escenario: EscenarioPredefinido) => { + setEscenarioActivo(escenario.id); + + if (escenario.tipo === 'libre') { + setTipoControl(null); + setPrecioControl(escenario.pe); + } else { + setTipoControl(escenario.tipo); + setPrecioControl(escenario.precio); + } + + // Agregar al historial + const nuevoResultado: ResultadoSimulacion = { + tipo: escenario.tipo === 'libre' ? 'equilibrio' : escenario.tipo === 'maximo' ? 'precio-maximo' : 'precio-minimo', + precio: escenario.tipo === 'libre' ? escenario.pe : escenario.precio, + cantidad: escenario.qe, + excesoDemanda: escenario.tipo === 'maximo' ? 30 : 0, + excesoOferta: escenario.tipo === 'minimo' ? 30 : 0, + pesoMuerto: escenario.tipo === 'libre' ? 0 : 225, + mensaje: escenario.nombre + }; + + setHistorial(prev => [...prev.slice(-2), nuevoResultado]); + + if (score < 100) { + setScore(prev => Math.min(100, prev + 25)); + } + + if (historial.length >= 2) { + setTimeout(() => { + onComplete?.(100); + }, 2000); + } + }; + + const reset = () => { + setEscenarioActivo(null); + setTipoControl(null); + setPrecioControl(50); + setHistorial([]); + setScore(0); + }; + + // Configuración del gráfico SVG + const width = 450; + const height = 350; + const padding = 50; + const graphWidth = width - 2 * padding; + const graphHeight = height - 2 * padding; + + const maxP = 80; + const maxQ = 150; + + const scaleX = (q: number) => padding + (q / maxQ) * graphWidth; + const scaleY = (p: number) => padding + graphHeight - (p / maxP) * graphHeight; + + // Puntos para curvas + const puntosDemanda = []; + const puntosOferta = []; + + for (let p = 0; p <= maxP; p += 2) { + const { qd, qo } = calcularCantidades(p); + puntosDemanda.push({ p, q: qd }); + puntosOferta.push({ p, q: qo }); + } + + const pathDemanda = puntosDemanda + .filter(p => p.q >= 0 && p.q <= maxQ) + .map((p, i) => `${i === 0 ? 'M' : 'L'} ${scaleX(p.q)} ${scaleY(p.p)}`) + .join(' '); + + const pathOferta = puntosOferta + .filter(p => p.q >= 0 && p.q <= maxQ) + .map((p, i) => `${i === 0 ? 'M' : 'L'} ${scaleX(p.q)} ${scaleY(p.p)}`) + .join(' '); + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

Simulador de Controles

+

Experimenta con diferentes controles de precio y observa las consecuencias

+
+
+
+
+ +
+ +
+
+ +
+ {/* Panel izquierdo: Escenarios */} +
+

+ + Escenarios Predefinidos +

+ + {escenariosPredefinidos.map((escenario) => ( + + ))} + + {/* Control manual */} +
+

Control Manual

+ +
+
+ + + +
+ + {tipoControl && ( +
+ + setPrecioControl(Number(e.target.value))} + className={`w-full ${ + tipoControl === 'maximo' ? 'accent-red-500' : 'accent-amber-500' + }`} + /> +
+ $10 + Equilibrio: $50 + $75 +
+
+ )} +
+
+
+ + {/* Panel central: Gráfico */} +
+

+ + Gráfico de Mercado +

+ + + {/* Grid */} + {Array.from({ length: 6 }).map((_, i) => ( + + + + + ))} + + {/* Ejes */} + + + + {/* Etiquetas */} + Cantidad + Precio ($) + + {/* Curvas */} + + D + + + S + + {/* Punto de equilibrio */} + + E ($50) + + {/* Línea de control */} + + {tipoControl && ( + + + + {tipoControl === 'maximo' ? 'Pmax' : 'Pmin'}: ${precioControl} + + + )} + + + {/* Indicadores de desequilibrio */} + + {resultado.excesoDemanda > 0 && ( + + + + Escasez + + + )} + + + + {resultado.excesoOferta > 0 && ( + + + + Superávit + + + )} + + +
+ + {/* Panel derecho: Resultados */} +
+ {/* Resultado actual */} + + +
+ {resultado.tipo === 'equilibrio' ? ( + + ) : resultado.tipo === 'precio-maximo' ? ( + + ) : ( + + )} + + {resultado.mensaje} + +
+ +
+
+ Precio: +

${resultado.precio.toFixed(0)}

+
+
+ Cantidad: +

{resultado.cantidad.toFixed(0)} un

+
+ + {resultado.excesoDemanda > 0 && ( +
+ Exceso de demanda: +

{resultado.excesoDemanda.toFixed(1)} unidades

+
+ )} + + {resultado.excesoOferta > 0 && ( +
+ Exceso de oferta: +

{resultado.excesoOferta.toFixed(1)} unidades

+
+ )} + + {resultado.pesoMuerto > 0 && ( +
+ + + Pérdida de peso muerto: + +

${resultado.pesoMuerto.toFixed(0)}

+
+ )} +
+
+
+ + {/* Historial */} +
+

Historial

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

Selecciona un escenario para comenzar

+ ) : ( + historial.map((h, idx) => ( + +
{h.mensaje}
+
+ P: ${h.precio} | Q: {h.cantidad} + {h.excesoDemanda > 0 && ` | Esc: ${h.excesoDemanda.toFixed(0)}`} + {h.excesoOferta > 0 && ` | Sup: ${h.excesoOferta.toFixed(0)}`} +
+
+ )) + )} +
+
+ + {/* Instrucciones */} +
+

💡 Consejos

+
    +
  • • Prueba los 3 escenarios predefinidos
  • +
  • • Observa cómo cambian las cantidades
  • +
  • • Identifica escasez vs superávit
  • +
  • • La pérdida de peso muerto es ineficiencia
  • +
+
+
+
+
+ ); +}; + +export default SimuladorControles; diff --git a/frontend/src/components/exercises/modulo2/SimuladorPrecios.tsx b/frontend/src/components/exercises/modulo2/SimuladorPrecios.tsx new file mode 100644 index 0000000..9c81994 --- /dev/null +++ b/frontend/src/components/exercises/modulo2/SimuladorPrecios.tsx @@ -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 = ({ onComplete, ejercicioId: _ejercicioId }) => { + const [params, _setParams] = useState(DEFAULT_PARAMS); + const [precioMaximo, setPrecioMaximo] = useState(null); + const [precioMinimo, setPrecioMinimo] = useState(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 ( +
+
+

+ + Simulador de Precios Intervenidos +

+

+ Experimenta con precios máximos y mínimos para ver cómo afectan el equilibrio de mercado. +

+
+ + {showInfo && ( + + +
+

Cómo usar:

+
    +
  • • Ajusta los sliders para establecer un precio máximo o mínimo
  • +
  • • Observa cómo cambian las cantidades demandadas y ofrecidas
  • +
  • • Identifica escasez (exceso de demanda) o superávit (exceso de oferta)
  • +
  • • La pérdida de peso muerto representa la ineficiencia creada
  • +
+
+ +
+ )} + +
+
+
+

Gráfico de Mercado

+ +
+ + + {/* Grid */} + {Array.from({ length: 11 }).map((_, i) => ( + + + + + ))} + + {/* Ejes */} + + + + {/* Labels */} + Cantidad + Precio + + {/* Curva de Demanda */} + + D + + {/* Curva de Oferta */} + + S + + {/* Punto de equilibrio */} + + + E + + + {/* Línea de precio máximo */} + {precioMaximo !== null && ( + + + Pmáx + + {/* Zona de escasez */} + {analisis.excesoDemanda > 0 && ( + + )} + + )} + + {/* Línea de precio mínimo */} + {precioMinimo !== null && ( + + + Pmín + + )} + + {/* Indicador de pérdida de peso muerto */} + {analisis.deadweightLoss > 0 && ( + + Pérdida de peso muerto + + )} + +
+ +
+
+

Controles de Precio

+ +
+
+ + { + setPrecioMaximo(Number(e.target.value) || null); + setPrecioMinimo(null); + setHasInteracted(true); + }} + className="w-full accent-red-500" + /> +
+ $0 + + {precioMaximo !== null ? `$${precioMaximo}` : 'Desactivado'} + + ${Math.round(equilibrio.P)} +
+
+ +
+ + { + setPrecioMinimo(Number(e.target.value) || null); + setPrecioMaximo(null); + setHasInteracted(true); + }} + className="w-full accent-amber-500" + /> +
+ ${Math.round(equilibrio.P)} + + {precioMinimo !== null ? `$${precioMinimo}` : 'Desactivado'} + + $100 +
+
+
+
+ + + +
+ {analisis.tipo === 'equilibrio' ? ( + + ) : analisis.tipo === 'precio-maximo' ? ( + + ) : ( + + )} +

+ {analisis.mensaje} +

+
+ +
+
+ Precio de equilibrio: +

${equilibrio.P.toFixed(1)}

+
+
+ Cantidad de equilibrio: +

{equilibrio.Q.toFixed(1)} unidades

+
+ {precioMaximo !== null && ( +
+ Precio máximo: +

${precioMaximo}

+
+ )} + {precioMinimo !== null && ( +
+ Precio mínimo: +

${precioMinimo}

+
+ )} + {analisis.excesoDemanda > 0 && ( +
+ Exceso de demanda (escasez): +

{analisis.excesoDemanda.toFixed(1)} unidades

+
+ )} + {analisis.excesoOferta > 0 && ( +
+ Exceso de oferta (superávit): +

{analisis.excesoOferta.toFixed(1)} unidades

+
+ )} + {analisis.deadweightLoss > 0 && ( +
+ Pérdida de peso muerto: +

${analisis.deadweightLoss.toFixed(1)}

+
+ )} +
+
+
+ +
+

Resultado

+

+ Cantidad transada: {analisis.cantidadTransada.toFixed(1)} unidades +

+

+ {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.'} +

+
+
+
+
+ ); +}; + +export default SimuladorPrecios; diff --git a/frontend/src/components/exercises/modulo2/TablaDemanda.tsx b/frontend/src/components/exercises/modulo2/TablaDemanda.tsx new file mode 100644 index 0000000..1209b88 --- /dev/null +++ b/frontend/src/components/exercises/modulo2/TablaDemanda.tsx @@ -0,0 +1,262 @@ +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Table, Check, X, Trophy, RotateCcw, Calculator } from 'lucide-react'; + +interface TablaDemandaProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface FilaDemanda { + precio: number; + cantidadCorrecta: number; + cantidadUsuario: string; +} + +const datosDemanda: FilaDemanda[] = [ + { precio: 10, cantidadCorrecta: 100, cantidadUsuario: '' }, + { precio: 20, cantidadCorrecta: 80, cantidadUsuario: '' }, + { precio: 30, cantidadCorrecta: 60, cantidadUsuario: '' }, + { precio: 40, cantidadCorrecta: 40, cantidadUsuario: '' }, + { precio: 50, cantidadCorrecta: 20, cantidadUsuario: '' }, +]; + +export const TablaDemanda: React.FC = ({ + ejercicioId: _ejercicioId, + onComplete +}) => { + const [filas, setFilas] = useState(datosDemanda); + const [mostrarResultados, setMostrarResultados] = useState(false); + const [score, setScore] = useState(0); + const [intentos, setIntentos] = useState(0); + const [completado, setCompletado] = useState(false); + + const handleCantidadChange = (index: number, valor: string) => { + if (mostrarResultados) return; + const nuevasFilas = [...filas]; + nuevasFilas[index].cantidadUsuario = valor; + setFilas(nuevasFilas); + }; + + const validarRespuestas = () => { + setIntentos(prev => prev + 1); + + // Verificar que todos los campos estén llenos + const camposVacios = filas.some(fila => fila.cantidadUsuario === ''); + if (camposVacios) { + alert('Por favor completa todas las cantidades antes de validar'); + return; + } + + setMostrarResultados(true); + + // Calcular puntuación + let correctas = 0; + filas.forEach(fila => { + if (parseInt(fila.cantidadUsuario) === fila.cantidadCorrecta) { + correctas++; + } + }); + + let puntuacion = Math.round((correctas / filas.length) * 100); + // Penalización por intentos + if (intentos >= 1) puntuacion -= 10; + if (intentos >= 2) puntuacion -= 10; + puntuacion = Math.max(puntuacion, 20); + + setScore(puntuacion); + + if (correctas === filas.length) { + setCompletado(true); + setTimeout(() => { + if (onComplete) { + onComplete(puntuacion); + } + }, 2000); + } + }; + + const reiniciar = () => { + setFilas(datosDemanda.map(f => ({ ...f, cantidadUsuario: '' }))); + setMostrarResultados(false); + setScore(0); + setCompletado(false); + }; + + const handleFinalizar = () => { + if (onComplete) { + onComplete(score); + } + }; + + const tablaCompletada = filas.every(fila => fila.cantidadUsuario !== ''); + + return ( +
+
+
+
+ +

Tabla de Demanda

+ + + +

+ Completa la tabla de demanda siguiendo la ley de la demanda: a mayor precio, menor cantidad demandada. +
+ + Pista: Por cada $10 que sube el precio, la cantidad baja 20 unidades. + +

+ + +
+
+ + + + + {mostrarResultados && ( + + )} + + + + {filas.map((fila, index) => ( + + + + {mostrarResultados && ( + + )} + + ))} + +
+ Precio ($) + + Cantidad Demandada + + Resultado +
+ ${fila.precio} + + handleCantidadChange(index, e.target.value)} + disabled={mostrarResultados} + placeholder="¿Cuántas unidades?" + className={`w-full max-w-xs px-4 py-2 border-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all ${ + mostrarResultados + ? parseInt(fila.cantidadUsuario) === fila.cantidadCorrecta + ? 'border-green-500 bg-green-50' + : 'border-red-500 bg-red-50' + : 'border-gray-300 focus:border-blue-500' + }`} + /> + + {parseInt(fila.cantidadUsuario) === fila.cantidadCorrecta ? ( + + ) : ( +
+ + + {fila.cantidadCorrecta} + +
+ )} +
+
+ + + {mostrarResultados && ( + +
parseInt(f.cantidadUsuario) === f.cantidadCorrecta) + ? 'bg-green-50 border-green-200' + : 'bg-yellow-50 border-yellow-200' + }`}> +
+ {filas.every(f => parseInt(f.cantidadUsuario) === f.cantidadCorrecta) ? ( + + ) : ( + + )} + parseInt(f.cantidadUsuario) === f.cantidadCorrecta) + ? 'text-green-800' + : 'text-yellow-800' + }`}> + {filas.every(f => parseInt(f.cantidadUsuario) === f.cantidadCorrecta) + ? '¡Perfecto! Todas las respuestas son correctas' + : 'Algunas respuestas necesitan revisión'} + +
+

+ Puntuación: {score}/100 +

+ {!filas.every(f => parseInt(f.cantidadUsuario) === f.cantidadCorrecta) && ( +

+ Las cantidades correctas se muestran en verde debajo de las respuestas incorrectas. + Observa el patrón: la cantidad disminuye 20 unidades por cada aumento de $10 en el precio. +

+ )} +
+
+ )} +
+ +
+ {!mostrarResultados ? ( + + ) : ( + <> + {!completado && ( + + )} + + + )} +
+ + {intentos > 0 && ( +
+ Intentos realizados: {intentos} +
+ )} +
+ ); +}; + +export default TablaDemanda; diff --git a/frontend/src/components/exercises/modulo2/TablaOferta.tsx b/frontend/src/components/exercises/modulo2/TablaOferta.tsx new file mode 100644 index 0000000..e88f52b --- /dev/null +++ b/frontend/src/components/exercises/modulo2/TablaOferta.tsx @@ -0,0 +1,289 @@ +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Table, Check, X, RotateCcw, Trophy, Calculator, HelpCircle } from 'lucide-react'; + +interface TablaOfertaProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface FilaTabla { + precio: number; + cantidad: number | null; + respuestaCorrecta: number; +} + +const datosIniciales: FilaTabla[] = [ + { precio: 10, cantidad: null, respuestaCorrecta: 20 }, + { precio: 20, cantidad: null, respuestaCorrecta: 40 }, + { precio: 30, cantidad: null, respuestaCorrecta: 60 }, + { precio: 40, cantidad: null, respuestaCorrecta: 80 }, + { precio: 50, cantidad: null, respuestaCorrecta: 100 }, +]; + +export const TablaOferta: React.FC = ({ onComplete, ejercicioId: _ejercicioId }) => { + const [filas, setFilas] = useState(datosIniciales); + const [respuestasUsuario, setRespuestasUsuario] = useState<{[key: number]: string}>({}); + const [mostrarResultados, setMostrarResultados] = useState(false); + const [score, setScore] = useState(0); + const [intentos, setIntentos] = useState(0); + const [completado, setCompletado] = useState(false); + + const handleInputChange = (precio: number, valor: string) => { + if (mostrarResultados) return; + setRespuestasUsuario(prev => ({ + ...prev, + [precio]: valor + })); + }; + + const verificarRespuestas = () => { + let correctas = 0; + const nuevasFilas = filas.map(fila => { + const respuestaUsuario = parseInt(respuestasUsuario[fila.precio] || '0'); + const esCorrecta = respuestaUsuario === fila.respuestaCorrecta; + if (esCorrecta) correctas++; + return { + ...fila, + cantidad: respuestaUsuario + }; + }); + + setFilas(nuevasFilas); + setMostrarResultados(true); + setIntentos(prev => prev + 1); + + // Calcular puntuación + const porcentajeCorrectas = correctas / filas.length; + const bonusIntentos = intentos === 0 ? 20 : intentos === 1 ? 10 : 0; + const puntajeFinal = Math.round((porcentajeCorrectas * 80) + bonusIntentos); + + setScore(puntajeFinal); + + if (correctas === filas.length) { + setCompletado(true); + setTimeout(() => { + if (onComplete) { + onComplete(puntajeFinal); + } + }, 1500); + } + }; + + const reiniciar = () => { + setFilas(datosIniciales); + setRespuestasUsuario({}); + setMostrarResultados(false); + setScore(0); + setIntentos(0); + setCompletado(false); + }; + + const todasRespondidas = filas.every(fila => respuestasUsuario[fila.precio] !== undefined && respuestasUsuario[fila.precio] !== ''); + + return ( +
+
+
+
+ +

Completar Tabla de Oferta

+ +
+ {completado && ( + {score} pts + )} + +
+ +

+ Completa la tabla de oferta siguiendo la relación directa entre precio y cantidad. + Cuando el precio se duplica, la cantidad ofrecida también se duplica. +

+ + +
+
+ +
+

Datos del problema:

+

+ Un productor de camisetas está dispuesto a vender 20 unidades a $10 cada una. + La función de oferta es lineal: Q = 2 × P +

+
+
+
+ +
+
+ + + + + + + + + {filas.map((fila, index) => { + const respuestaUsuario = respuestasUsuario[fila.precio] || ''; + const esCorrecta = mostrarResultados && parseInt(respuestaUsuario) === fila.respuestaCorrecta; + const esIncorrecta = mostrarResultados && parseInt(respuestaUsuario) !== fila.respuestaCorrecta; + + return ( + + + + + + ); + })} + +
+
+ Precio ($) +
+
+
+ Cantidad Ofrecida (unidades) + +
+
+ Estado +
+ + ${fila.precio} + + +
+ handleInputChange(fila.precio, e.target.value)} + disabled={mostrarResultados} + placeholder="¿Cuántas unidades?" + className={` + w-full max-w-xs px-4 py-2 border-2 rounded-lg text-lg font-medium + focus:outline-none focus:ring-2 focus:ring-green-500 + ${esCorrecta ? 'border-green-500 bg-green-50 text-green-700' : ''} + ${esIncorrecta ? 'border-red-500 bg-red-50 text-red-700' : ''} + ${!mostrarResultados ? 'border-gray-300 hover:border-green-400' : ''} + `} + /> + {mostrarResultados && esIncorrecta && ( + + Correcto: {fila.respuestaCorrecta} + + )} +
+
+ + {mostrarResultados && ( + + {esCorrecta ? ( + + ) : ( + + )} + + )} + +
+
+ + {/* Visualización de la relación */} +
+

Patrón a seguir:

+
+
+ P = $10 + + Q = 20 +
+ | +
+ P = $20 + + Q = 40 +
+ | +
+ P = $30 + + Q = 60 +
+ | + ¿Sigues el patrón? +
+
+ +
+
+ {mostrarResultados && ( + + Correctas: + {filas.filter(f => f.cantidad === f.respuestaCorrecta).length} + de {filas.length} + + )} +
+ + {!completado ? ( + + ) : ( + + +
+

¡Completado!

+

Puntuación: {score}/100

+
+
+ )} +
+ + {mostrarResultados && !completado && ( + +

+ Consejo: La cantidad ofrecida siempre es el doble del precio. + Por ejemplo: si P = $40, entonces Q = 2 × 40 = 80 unidades. +

+
+ )} +
+ ); +}; + +export default TablaOferta; diff --git a/frontend/src/components/exercises/modulo2/index.ts b/frontend/src/components/exercises/modulo2/index.ts new file mode 100644 index 0000000..867b688 --- /dev/null +++ b/frontend/src/components/exercises/modulo2/index.ts @@ -0,0 +1,28 @@ +export { LeyDemandaQuiz } from './LeyDemandaQuiz'; +export { LeyOfertaQuiz } from './LeyOfertaQuiz'; +export { TablaDemanda } from './TablaDemanda'; +export { TablaOferta } from './TablaOferta'; +export { CurvaDemandaConstructor } from './CurvaDemandaConstructor'; +export { CurvaOfertaConstructor } from './CurvaOfertaConstructor'; +export { EquilibrioFinder } from './EquilibrioFinder'; +export { EquilibrioGrafico } from './EquilibrioGrafico'; +export { FactoresDesplazanDemanda } from './FactoresDesplazanDemanda'; +export { FactoresDesplazanOferta } from './FactoresDesplazanOferta'; +export { DesplazamientoVsMovimiento } from './DesplazamientoVsMovimiento'; +export { ExcesoDemandaEscasez } from './ExcesoDemandaEscasez'; +export { ExcesoOfertaSuperavit } from './ExcesoOfertaSuperavit'; +export { DemandaIndividualVsMercado } from './DemandaIndividualVsMercado'; +export { OfertaCortoLargoPlazo } from './OfertaCortoLargoPlazo'; +export { AjusteEquilibrio } from './AjusteEquilibrio'; +export { CambiosEquilibrio } from './CambiosEquilibrio'; +export { CalculoElasticidadPrecio } from './CalculoElasticidadPrecio'; +export { ElasticidadElasticaInelastica } from './ElasticidadElasticaInelastica'; +export { FactoresElasticidad } from './FactoresElasticidad'; +export { ElasticidadIngresoTotal } from './ElasticidadIngresoTotal'; +export { PrecioMaximoTecho } from './PrecioMaximoTecho'; +export { PrecioMinimoPiso } from './PrecioMinimoPiso'; +export { SimuladorControles } from './SimuladorControles'; +export { ControlesVidaReal } from './ControlesVidaReal'; +export { ConstructorCurvas } from './ConstructorCurvas'; +export { SimuladorPrecios } from './SimuladorPrecios'; +export { IdentificarShocks } from './IdentificarShocks'; diff --git a/frontend/src/components/exercises/modulo3/BienesLujoNecesarios.tsx b/frontend/src/components/exercises/modulo3/BienesLujoNecesarios.tsx new file mode 100644 index 0000000..4b2e597 --- /dev/null +++ b/frontend/src/components/exercises/modulo3/BienesLujoNecesarios.tsx @@ -0,0 +1,286 @@ +import { useState } from 'react'; +import { Button } from '../../ui/Button'; +import { Card, CardHeader } from '../../ui/Card'; + +interface Ejercicio { + id: number; + bien: string; + descripcion: string; + escenario: string; + elasticidad: number; + respuestaCorrecta: 'lujo' | 'necesario'; + explicacion: string; +} + +const ejercicios: Ejercicio[] = [ + { + id: 1, + bien: "Caviar", + descripcion: "Alimento de lujo", + escenario: "Cuando el ingreso aumenta 10%, el consumo de caviar aumenta 35%", + elasticidad: 3.5, + respuestaCorrecta: 'lujo', + explicacion: "Ei = 3.5 > 1, por lo que es un bien de lujo. El consumo aumenta más que proporcionalmente al ingreso." + }, + { + id: 2, + bien: "Pan", + descripcion: "Alimento básico", + escenario: "Cuando el ingreso aumenta 20%, el consumo de pan aumenta solo 6%", + elasticidad: 0.3, + respuestaCorrecta: 'necesario', + explicacion: "Ei = 0.3 (entre 0 y 1), por lo que es un bien necesario. El consumo aumenta menos que proporcionalmente al ingreso." + }, + { + id: 3, + bien: "Yates privados", + descripcion: "Embarcaciones recreativas", + escenario: "Un aumento del 15% en ingreso produce un aumento del 60% en la demanda de yates", + elasticidad: 4.0, + respuestaCorrecta: 'lujo', + explicacion: "Ei = 4.0 > 1, claramente un bien de lujo. Los bienes de lujo tienen elasticidad ingreso mayor a 1." + }, + { + id: 4, + bien: "Sal", + descripcion: "Condimento esencial", + escenario: "Aunque el ingreso aumente 50%, el consumo de sal apenas varía un 2%", + elasticidad: 0.04, + respuestaCorrecta: 'necesario', + explicacion: "Ei = 0.04 ≈ 0, típico de bienes necesarios básicos. El consumo es relativamente independiente del ingreso." + }, + { + id: 5, + bien: "Viajes en primera clase", + descripcion: "Transporte aéreo de lujo", + escenario: "El ingreso aumenta 25% y los viajes en primera clase aumentan 70%", + elasticidad: 2.8, + respuestaCorrecta: 'lujo', + explicacion: "Ei = 2.8 > 1, por lo que es un bien de lujo. Solo personas con alto ingreso pueden acceder a él." + }, + { + id: 6, + bien: "Medicinas genéricas", + descripcion: "Productos farmacéuticos básicos", + escenario: "El ingreso sube 30% pero el consumo solo aumenta 6%", + elasticidad: 0.2, + respuestaCorrecta: 'necesario', + explicacion: "Ei = 0.2 (entre 0 y 1), por lo que es un bien necesario. La salud es prioritaria independiente del ingreso." + } +]; + +interface Respuesta { + tipo: 'lujo' | 'necesario' | null; + esCorrecta: boolean | null; +} + +interface BienesLujoNecesariosProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +export function BienesLujoNecesarios({ ejercicioId: _ejercicioId, onComplete }: BienesLujoNecesariosProps) { + const [respuestas, setRespuestas] = useState>({}); + const [mostrarResultados, setMostrarResultados] = useState(false); + + const seleccionarRespuesta = (ejercicioId: number, tipo: 'lujo' | 'necesario') => { + if (mostrarResultados) return; + + setRespuestas(prev => ({ + ...prev, + [ejercicioId]: { tipo, esCorrecta: null } + })); + }; + + const verificarTodo = () => { + const nuevasRespuestas: Record = {}; + + ejercicios.forEach(ej => { + const respuesta = respuestas[ej.id]; + if (respuesta?.tipo) { + nuevasRespuestas[ej.id] = { + tipo: respuesta.tipo, + esCorrecta: respuesta.tipo === ej.respuestaCorrecta + }; + } + }); + + setRespuestas(nuevasRespuestas); + setMostrarResultados(true); + + const correctas = Object.values(nuevasRespuestas).filter(r => r.esCorrecta).length; + if (onComplete) { + onComplete(Math.round((correctas / ejercicios.length) * 100)); + } + }; + + const reiniciar = () => { + setRespuestas({}); + setMostrarResultados(false); + }; + + const getCardStyle = (ejercicioId: number) => { + const respuesta = respuestas[ejercicioId]; + if (!mostrarResultados || !respuesta?.tipo) { + return 'bg-white border-gray-200'; + } + return respuesta.esCorrecta + ? 'bg-green-50 border-green-400' + : 'bg-red-50 border-red-400'; + }; + + const correctas = Object.values(respuestas).filter(r => r.esCorrecta).length; + const totalRespondidas = Object.keys(respuestas).length; + + return ( + + + +
+
+
+

Bien de Lujo

+

Ei > 1

+

+ El gasto aumenta más que proporcionalmente al ingreso. Son bienes que se consumen más a medida que las personas tienen más dinero disponible. +

+

+ Ejemplos: joyería, yates, viajes de lujo, arte +

+
+
+

Bien Necesario

+

0 < Ei < 1

+

+ El gasto aumenta menos que proporcionalmente al ingreso. Son bienes esenciales cuyo consumo no varía mucho con el ingreso. +

+

+ Ejemplos: alimentos básicos, medicinas, servicios públicos +

+
+
+ +
+ {ejercicios.map((ejercicio) => { + const respuesta = respuestas[ejercicio.id]; + + return ( +
+
+
+
+

{ejercicio.bien}

+ + {ejercicio.descripcion} + +
+ +

{ejercicio.escenario}

+ + {mostrarResultados && ( +
+

+ Ei = {ejercicio.elasticidad} +

+

+ + {ejercicio.respuestaCorrecta === 'lujo' + ? 'Bien de Lujo' + : 'Bien Necesario'} + +

+

{ejercicio.explicacion}

+
+ )} +
+ +
+ + +
+
+
+ ); + })} +
+ +
+
+ Progreso: {totalRespondidas} / {ejercicios.length} +
+ + {!mostrarResultados ? ( + + ) : ( +
+
+

Puntuación

+

+ {correctas} / {ejercicios.length} +

+
+ +
+ )} +
+ + {mostrarResultados && ( +
= ejercicios.length / 2 + ? 'bg-yellow-100 border border-yellow-300' + : 'bg-red-100 border border-red-300' + }`}> +

+ {correctas === ejercicios.length + ? '¡Excelente! Has clasificado todos los bienes correctamente.' + : correctas >= ejercicios.length / 2 + ? '¡Buen trabajo! Algunos bienes necesitan más atención.' + : 'Necesitas repasar la diferencia entre bienes de lujo y necesarios.'} +

+
+ )} +
+
+ ); +} + +export default BienesLujoNecesarios; diff --git a/frontend/src/components/exercises/modulo3/BienesNormalesInferiores.tsx b/frontend/src/components/exercises/modulo3/BienesNormalesInferiores.tsx new file mode 100644 index 0000000..8a2efee --- /dev/null +++ b/frontend/src/components/exercises/modulo3/BienesNormalesInferiores.tsx @@ -0,0 +1,292 @@ +import { useState } from 'react'; +import { Button } from '../../ui/Button'; +import { Card, CardHeader } from '../../ui/Card'; + +interface Caso { + id: number; + bien: string; + descripcion: string; + i1: number; + i2: number; + q1: number; + q2: number; + respuestaCorrecta: 'normal' | 'inferior'; +} + +const casos: Caso[] = [ + { + id: 1, + bien: "Arroz", + descripcion: "Alimento básico de consumo diario", + i1: 1000, + i2: 1500, + q1: 20, + q2: 22, + respuestaCorrecta: 'normal' + }, + { + id: 2, + bien: "Autobuses urbanos", + descripcion: "Transporte público económico", + i1: 2000, + i2: 3000, + q1: 50, + q2: 30, + respuestaCorrecta: 'inferior' + }, + { + id: 3, + bien: "Pan blanco común", + descripcion: "Pan básico de bajo costo", + i1: 1500, + i2: 2500, + q1: 30, + q2: 15, + respuestaCorrecta: 'inferior' + }, + { + id: 4, + bien: "Leche", + descripcion: "Producto lácteo básico", + i1: 2000, + i2: 3500, + q1: 12, + q2: 18, + respuestaCorrecta: 'normal' + }, + { + id: 5, + bien: "Frijoles enlatados", + descripcion: "Versión económica vs. frescos", + i1: 3000, + i2: 5000, + q1: 20, + q2: 8, + respuestaCorrecta: 'inferior' + } +]; + +interface Respuesta { + tipo: 'normal' | 'inferior' | null; + esCorrecta: boolean | null; +} + +interface BienesNormalesInferioresProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +export function BienesNormalesInferiores({ ejercicioId: _ejercicioId, onComplete }: BienesNormalesInferioresProps) { + const [respuestas, setRespuestas] = useState>({}); + const [mostrarResultados, setMostrarResultados] = useState(false); + + const calcularElasticidad = (caso: Caso) => { + const deltaQ = caso.q2 - caso.q1; + const deltaI = caso.i2 - caso.i1; + const qPromedio = (caso.q1 + caso.q2) / 2; + const iPromedio = (caso.i1 + caso.i2) / 2; + const porcentajeQ = (deltaQ / qPromedio) * 100; + const porcentajeI = (deltaI / iPromedio) * 100; + return porcentajeQ / porcentajeI; + }; + + const seleccionarRespuesta = (casoId: number, tipo: 'normal' | 'inferior') => { + if (mostrarResultados) return; + + setRespuestas(prev => ({ + ...prev, + [casoId]: { tipo, esCorrecta: null } + })); + }; + + const verificarTodo = () => { + const nuevasRespuestas: Record = {}; + + casos.forEach(caso => { + const respuesta = respuestas[caso.id]; + if (respuesta?.tipo) { + nuevasRespuestas[caso.id] = { + tipo: respuesta.tipo, + esCorrecta: respuesta.tipo === caso.respuestaCorrecta + }; + } + }); + + setRespuestas(nuevasRespuestas); + setMostrarResultados(true); + + const correctas = Object.values(nuevasRespuestas).filter(r => r.esCorrecta).length; + if (onComplete) { + onComplete(Math.round((correctas / casos.length) * 100)); + } + }; + + const reiniciar = () => { + setRespuestas({}); + setMostrarResultados(false); + }; + + const getCardStyle = (caso: Caso) => { + const respuesta = respuestas[caso.id]; + if (!mostrarResultados || !respuesta?.tipo) { + return 'bg-white border-gray-200'; + } + return respuesta.esCorrecta + ? 'bg-green-50 border-green-400' + : 'bg-red-50 border-red-400'; + }; + + const correctas = Object.values(respuestas).filter(r => r.esCorrecta).length; + const totalRespondidas = Object.keys(respuestas).length; + + return ( + + + +
+
+
+

Bien Normal

+

Ei > 0

+

+ La demanda aumenta cuando sube el ingreso +

+
+
+

Bien Inferior

+

Ei < 0

+

+ La demanda disminuye cuando sube el ingreso +

+
+
+ +
+ {casos.map((caso) => { + const elasticidad = calcularElasticidad(caso); + const respuesta = respuestas[caso.id]; + + return ( +
+
+
+

{caso.bien}

+

{caso.descripcion}

+ +
+
+ I₁ +

${caso.i1}

+
+
+ I₂ +

${caso.i2}

+
+
+ Q₁ +

{caso.q1}

+
+
+ Q₂ +

{caso.q2}

+
+
+ + {mostrarResultados && respuesta?.tipo && ( +
+

+ Ei = {elasticidad.toFixed(2)} → {' '} + 0 ? 'text-blue-600' : 'text-red-600'}> + {elasticidad > 0 ? 'Bien Normal' : 'Bien Inferior'} + +

+
+ )} +
+ +
+ + +
+
+
+ ); + })} +
+ +
+
+ Progreso: {totalRespondidas} / {casos.length} +
+ + {!mostrarResultados ? ( + + ) : ( +
+
+

Puntuación

+

+ {correctas} / {casos.length} +

+
+ +
+ )} +
+ + {mostrarResultados && ( +
= casos.length / 2 + ? 'bg-yellow-100 border border-yellow-300' + : 'bg-red-100 border border-red-300' + }`}> +

+ {correctas === casos.length + ? '¡Excelente! Has identificado todos los bienes correctamente.' + : correctas >= casos.length / 2 + ? '¡Buen trabajo! Algunos bienes necesitan más atención.' + : 'Necesitas repasar la diferencia entre bienes normales e inferiores.'} +

+
+ )} +
+
+ ); +} + +export default BienesNormalesInferiores; diff --git a/frontend/src/components/exercises/modulo3/CalculadoraElasticidad.tsx b/frontend/src/components/exercises/modulo3/CalculadoraElasticidad.tsx new file mode 100644 index 0000000..31449f0 --- /dev/null +++ b/frontend/src/components/exercises/modulo3/CalculadoraElasticidad.tsx @@ -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('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 = { + precio: 'Elasticidad Precio de la Demanda', + ingreso: 'Elasticidad Ingreso', + cruzada: 'Elasticidad Cruzada' + }; + + return ( + + + +
+
+ {(['precio', 'ingreso', 'cruzada'] as ElasticidadTipo[]).map((t) => ( + + ))} +
+ +
+

{tipoLabels[tipo]}

+

+ {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'} +

+
+ +
+ setQ1(e.target.value)} + placeholder="Ej: 100" + /> + setQ2(e.target.value)} + placeholder="Ej: 80" + /> + setP1(e.target.value)} + placeholder="Ej: 10" + /> + setP2(e.target.value)} + placeholder="Ej: 12" + /> +
+ + {error && ( +

{error}

+ )} + + {resultado && ( +
+
+

Desarrollo paso a paso:

+ +
+
+

Paso 1: Calcular cambios

+

+ ΔQ = Q2 - Q1 = {q2} - {q1} = {resultado.deltaQ.toFixed(2)} +

+

+ ΔP = P2 - P1 = {p2} - {p1} = {resultado.deltaP.toFixed(2)} +

+
+ +
+

Paso 2: Calcular promedios

+

+ Q̄ = (Q1 + Q2) / 2 = ({q1} + {q2}) / 2 = {resultado.qPromedio.toFixed(2)} +

+

+ P̄ = (P1 + P2) / 2 = ({p1} + {p2}) / 2 = {resultado.pPromedio.toFixed(2)} +

+
+ +
+

Paso 3: Calcular variaciones porcentuales

+

+ %ΔQ = (ΔQ / Q̄) × 100 = ({resultado.deltaQ.toFixed(2)} / {resultado.qPromedio.toFixed(2)}) × 100 = {resultado.porcentajeQ.toFixed(2)}% +

+

+ %ΔP = (ΔP / P̄) × 100 = ({resultado.deltaP.toFixed(2)} / {resultado.pPromedio.toFixed(2)}) × 100 = {resultado.porcentajeP.toFixed(2)}% +

+
+ +
+

Paso 4: Calcular elasticidad

+

+ E = %ΔQ / %ΔP = {resultado.porcentajeQ.toFixed(2)} / {resultado.porcentajeP.toFixed(2)} = {resultado.elasticidad.toFixed(4)} +

+
+
+
+ +
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') + }`}> +

Interpretación:

+

{resultado.interpretacion}

+ {tipo === 'precio' && Math.abs(resultado.elasticidad) > 1 && ( +

+ El ingreso total aumentará si se reduce el precio (efecto cantidad domina). +

+ )} + {tipo === 'precio' && Math.abs(resultado.elasticidad) < 1 && ( +

+ El ingreso total aumentará si se aumenta el precio (efecto precio domina). +

+ )} +
+
+ )} +
+
+ ); +} + +export default CalculadoraElasticidad; diff --git a/frontend/src/components/exercises/modulo3/CanastaOptima.tsx b/frontend/src/components/exercises/modulo3/CanastaOptima.tsx new file mode 100644 index 0000000..eb48687 --- /dev/null +++ b/frontend/src/components/exercises/modulo3/CanastaOptima.tsx @@ -0,0 +1,334 @@ +import { useState } from 'react'; +import { Button } from '../../ui/Button'; +import { Card, CardHeader } from '../../ui/Card'; + +interface CanastaOptimaProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Canasta { + id: string; + nombre: string; + descripcion: string; + items: { nombre: string; cantidad: number; precio: number }[]; + utTotal: number; + esOptima: boolean; + razonamiento: string; +} + +const canastas: Canasta[] = [ + { + id: 'a', + nombre: 'Canasta A', + descripcion: 'Mayor cantidad de bienes baratos', + items: [ + { nombre: 'Manzanas 🍎', cantidad: 8, precio: 2 }, + { nombre: 'Naranjas 🍊', cantidad: 3, precio: 3 }, + ], + utTotal: 68, + esOptima: false, + razonamiento: 'Aunque tiene muchas manzanas, la UMg/P de las naranjas es mayor al inicio. No maximiza la utilidad.' + }, + { + id: 'b', + nombre: 'Canasta B', + descripcion: 'Combinación balanceada', + items: [ + { nombre: 'Manzanas 🍎', cantidad: 5, precio: 2 }, + { nombre: 'Naranjas 🍊', cantidad: 5, precio: 3 }, + ], + utTotal: 85, + esOptima: true, + razonamiento: '¡Óptima! En esta combinación, UMg/P de manzanas ≈ UMg/P de naranjas. Maximiza la utilidad dado el presupuesto de $25.' + }, + { + id: 'c', + nombre: 'Canasta C', + descripcion: 'Muchas naranjas', + items: [ + { nombre: 'Manzanas 🍎', cantidad: 2, precio: 2 }, + { nombre: 'Naranjas 🍊', cantidad: 7, precio: 3 }, + ], + utTotal: 78, + esOptima: false, + razonamiento: 'Demasiadas naranjas. La UMg de las últimas unidades es muy baja comparada con las manzanas que podría haber comprado.' + }, + { + id: 'd', + nombre: 'Canasta D', + descripcion: 'Excede el presupuesto', + items: [ + { nombre: 'Manzanas 🍎', cantidad: 6, precio: 2 }, + { nombre: 'Naranjas 🍊', cantidad: 6, precio: 3 }, + ], + utTotal: 90, + esOptima: false, + razonamiento: 'Costo total: $30. ¡Excede el presupuesto de $25! No es factible.' + } +]; + +const datosUtilidad = { + manzanas: { + um: [12, 10, 8, 6, 4, 2, 0, -2], + precio: 2 + }, + naranjas: { + um: [15, 12, 9, 6, 3, 0, -3], + precio: 3 + } +}; + +export function CanastaOptima({ ejercicioId: _ejercicioId, onComplete }: CanastaOptimaProps) { + const [canastaSeleccionada, setCanastaSeleccionada] = useState(null); + const [mostrarResultado, setMostrarResultado] = useState(false); + const [etapa, setEtapa] = useState(0); + + const presupuesto = 25; + + const handleSeleccion = (id: string) => { + setCanastaSeleccionada(id); + setMostrarResultado(false); + }; + + const verificar = () => { + setMostrarResultado(true); + const canasta = canastas.find(c => c.id === canastaSeleccionada); + if (canasta?.esOptima && onComplete) { + onComplete(100); + } + }; + + const calcularCosto = (canasta: Canasta) => { + return canasta.items.reduce((total, item) => total + item.cantidad * item.precio, 0); + }; + + return ( + + + +
+
+

Problema

+

+ Tienes un presupuesto de ${presupuesto} para gastar en manzanas y naranjas. +

+
+
+

🍎 Manzanas: $2 cada una

+

UMg: 12, 10, 8, 6, 4, 2, 0, -2

+
+
+

🍊 Naranjas: $3 cada una

+

UMg: 15, 12, 9, 6, 3, 0, -3

+
+
+
+ +
+ {canastas.map((canasta) => { + const costo = calcularCosto(canasta); + const dentroPresupuesto = costo <= presupuesto; + + return ( +
handleSeleccion(canasta.id)} + className={`p-4 rounded-lg border-2 cursor-pointer transition-all ${ + canastaSeleccionada === canasta.id + ? 'border-primary bg-blue-50' + : 'border-gray-200 hover:border-gray-300' + } ${mostrarResultado && canasta.esOptima ? 'ring-2 ring-green-500' : ''}`} + > +
+

{canasta.nombre}

+ {mostrarResultado && canasta.esOptima && ( + ÓPTIMA ✓ + )} +
+ +

{canasta.descripcion}

+ +
+ {canasta.items.map((item) => ( +
+ {item.nombre} + {item.cantidad} × ${item.precio} = ${item.cantidad * item.precio} +
+ ))} +
+ +
+
+ Costo Total: + + ${costo} {dentroPresupuesto ? '✓' : '✗'} + +
+
+ Utilidad Total: + {canasta.utTotal} +
+
+ + {mostrarResultado && ( +
+ {canasta.razonamiento} +
+ )} +
+ ); + })} +
+ +
+
+ {canastaSeleccionada && ( + <>Seleccionado: {canastas.find(c => c.id === canastaSeleccionada)?.nombre} + )} +
+ +
+ + {mostrarResultado && ( +
c.id === canastaSeleccionada)?.esOptima + ? 'bg-green-100 border border-green-300' + : 'bg-red-100 border border-red-300' + }`}> +

+ {canastas.find(c => c.id === canastaSeleccionada)?.esOptima + ? '¡Correcto! Has identificado la canasta óptima.' + : 'Incorrecto. Revisa el razonamiento de cada canasta y encuentra la que maximiza la utilidad sin exceder el presupuesto.' + } +

+
+ )} + +
+
+ {['Datos', 'Proceso', 'Análisis'].map((etapaNombre, idx) => ( + + ))} +
+ + {etapa === 0 && ( +
+

Tabla de Utilidad Marginal por Precio (UMg/P)

+
+ + + + + + + + + + + + {[1, 2, 3, 4, 5, 6, 7].map((i) => ( + + + + + + + + ))} + +
UnidadUMg ManzanaUMg/P ManzanaUMg NaranjaUMg/P Naranja
{i}{datosUtilidad.manzanas.um[i-1] || '-'} + {datosUtilidad.manzanas.um[i-1] ? (datosUtilidad.manzanas.um[i-1] / 2).toFixed(1) : '-'} + {datosUtilidad.naranjas.um[i-1] || '-'} + {datosUtilidad.naranjas.um[i-1] ? (datosUtilidad.naranjas.um[i-1] / 3).toFixed(1) : '-'} +
+
+
+ )} + + {etapa === 1 && ( +
+

Proceso de Optimización

+
+

Paso 1: Ordenar todas las unidades por UMg/P (de mayor a menor):

+
+ 1. Naranja 1: 15/3 = 5.0
+ 2. Manzana 1: 12/2 = 6.0 ✓
+ 3. Naranja 2: 12/3 = 4.0
+ 4. Manzana 2: 10/2 = 5.0
+ 5. Naranja 3: 9/3 = 3.0
+ 6. Manzana 3: 8/2 = 4.0
+ 7. Naranja 4: 6/3 = 2.0
+ 8. Manzana 4: 6/2 = 3.0
+ 9. Manzana 5: 4/2 = 2.0
+ 10. Naranja 5: 3/3 = 1.0
+ ... +
+

Paso 2: Seleccionar unidades hasta agotar el presupuesto de $25:

+
+ • Manzana 1: $2 (Total: $2)
+ • Naranja 1: $3 (Total: $5)
+ • Manzana 2: $2 (Total: $7)
+ • Naranja 2: $3 (Total: $10)
+ • Manzana 3: $2 (Total: $12)
+ • Naranja 3: $3 (Total: $15)
+ • Manzana 4: $2 (Total: $17)
+ • Naranja 4: $3 (Total: $20)
+ • Manzana 5: $2 (Total: $22)
+ • Naranja 5: $3 (Total: $25) ✓
+ Resultado: 5 manzanas + 5 naranjas = $25 +
+
+
+ )} + + {etapa === 2 && ( +
+

Análisis de la Canasta Óptima

+
+

La canasta óptima debe cumplir dos condiciones:

+
    +
  1. Agotar el presupuesto: Gastar exactamente $25
  2. +
  3. Igualar UMg/P: La utilidad marginal por peso debe ser similar para ambos bienes
  4. +
+ +
+

En la Canasta B (Óptima):

+
    +
  • 5 manzanas × $2 = $10
  • +
  • 5 naranjas × $3 = $15
  • +
  • Total: $25
  • +
  • UMg/P de última manzana: 4/2 = 2.0
  • +
  • UMg/P de última naranja: 3/3 = 1.0
  • +
+

+ Nota: En el óptimo, las UMg/P son aproximadamente iguales (diferencias pequeñas se deben a que no podemos comprar fracciones de unidades). +

+
+
+
+ )} +
+
+
+ ); +} + +export default CanastaOptima; diff --git a/frontend/src/components/exercises/modulo3/ClasificacionElasticidad.tsx b/frontend/src/components/exercises/modulo3/ClasificacionElasticidad.tsx new file mode 100644 index 0000000..2bb3194 --- /dev/null +++ b/frontend/src/components/exercises/modulo3/ClasificacionElasticidad.tsx @@ -0,0 +1,236 @@ +import { useState, useCallback } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { CheckCircle, RotateCcw, HelpCircle, AlertCircle } from 'lucide-react'; + +interface ClasificacionElasticidadProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +type TipoElasticidad = 'elastica' | 'unitaria' | 'inelastica'; + +interface EjercicioData { + ep: number; + descripcion: string; + explicacion: string; +} + +const ejercicios: EjercicioData[] = [ + { + ep: -2.5, + descripcion: 'Un producto tiene una elasticidad precio de -2.5. ¿Cómo se clasifica?', + explicacion: '|Ep| = 2.5 > 1 → Demanda ELÁSTICA. El % de cambio en cantidad es mayor que el % de cambio en precio.', + }, + { + ep: -0.3, + descripcion: 'La elasticidad precio de un bien es -0.3. ¿Qué tipo de demanda tiene?', + explicacion: '|Ep| = 0.3 < 1 → Demanda INELÁSTICA. El % de cambio en cantidad es menor que el % de cambio en precio.', + }, + { + ep: -1.0, + descripcion: 'Un artículo tiene elasticidad precio igual a -1. ¿Cómo se clasifica?', + explicacion: '|Ep| = 1 → Demanda UNITARIA. El % de cambio en cantidad es igual al % de cambio en precio.', + }, + { + ep: -0.8, + descripcion: 'La elasticidad de un medicamento es de -0.8. ¿Qué tipo de elasticidad tiene?', + explicacion: '|Ep| = 0.8 < 1 → Demanda INELÁSTICA. Los medicamentos suelen ser inelásticos porque son necesidades básicas.', + }, + { + ep: -4.2, + descripcion: 'Un restaurante de lujo tiene elasticidad de -4.2. ¿Cómo se clasifica?', + explicacion: '|Ep| = 4.2 > 1 → Demanda ELÁSTICA. Los lujos suelen tener demanda muy elástica porque son opcionales.', + }, +]; + +export function ClasificacionElasticidad({ ejercicioId: _ejercicioId, onComplete }: ClasificacionElasticidadProps) { + const [ejercicioIndex, setEjercicioIndex] = useState(0); + const [respuesta, setRespuesta] = useState(null); + const [validado, setValidado] = useState(false); + const [aciertos, setAciertos] = useState(0); + const [completado, setCompletado] = useState(false); + + const ejercicio = ejercicios[ejercicioIndex]; + + const obtenerRespuestaCorrecta = useCallback((ep: number): TipoElasticidad => { + const valorAbs = Math.abs(ep); + if (valorAbs > 1) return 'elastica'; + if (valorAbs < 1) return 'inelastica'; + return 'unitaria'; + }, []); + + const validarRespuesta = () => { + if (!respuesta) return; + + const correcta = obtenerRespuestaCorrecta(ejercicio.ep); + const esCorrecto = respuesta === correcta; + + setValidado(true); + + if (esCorrecto) { + setAciertos((prev) => prev + 1); + } + + if (ejercicioIndex === ejercicios.length - 1) { + setCompletado(true); + if (onComplete) { + const puntuacion = Math.round((aciertos + (esCorrecto ? 1 : 0)) / ejercicios.length * 100); + onComplete(puntuacion); + } + } + }; + + const siguienteEjercicio = () => { + if (ejercicioIndex < ejercicios.length - 1) { + setEjercicioIndex((prev) => prev + 1); + setRespuesta(null); + setValidado(false); + } + }; + + const reiniciar = () => { + setEjercicioIndex(0); + setRespuesta(null); + setValidado(false); + setAciertos(0); + setCompletado(false); + }; + + const respuestaCorrecta = obtenerRespuestaCorrecta(ejercicio.ep); + + const opciones: { value: TipoElasticidad; label: string; color: string }[] = [ + { value: 'elastica', label: 'Elástica (|Ep| > 1)', color: 'bg-green-100 border-green-300 text-green-800' }, + { value: 'unitaria', label: 'Unitaria (|Ep| = 1)', color: 'bg-yellow-100 border-yellow-300 text-yellow-800' }, + { value: 'inelastica', label: 'Inelástica (|Ep| < 1)', color: 'bg-blue-100 border-blue-300 text-blue-800' }, + ]; + + return ( +
+ + + +
+
+

ELÁSTICA

+

|Ep| > 1

+

%ΔQ > %ΔP

+
+
+

UNITARIA

+

|Ep| = 1

+

%ΔQ = %ΔP

+
+
+

INELÁSTICA

+

|Ep| < 1

+

%ΔQ < %ΔP

+
+
+ +
+
+ + {ejercicioIndex + 1}/{ejercicios.length} + +

Pregunta:

+
+
+

{ejercicio.descripcion}

+

+ Ep = {ejercicio.ep} +

+
+
+ +
+ {opciones.map((opcion) => ( + + ))} +
+ + {validado && ( +
+

+ + Explicación: +

+

{ejercicio.explicacion}

+
+ )} + +
+ {!validado ? ( + + ) : ejercicioIndex < ejercicios.length - 1 ? ( + + ) : ( + + )} +
+ + {completado && ( +
+

+ ¡Completado! Has acertado {aciertos + (respuesta === respuestaCorrecta ? 1 : 0)} de{' '} + {ejercicios.length} ejercicios +

+
+ )} +
+ + +

Interpretación Económica:

+
    +
  • + Elástica (|Ep| > 1): Los consumidores son muy sensibles al precio. + Un cambio de precio genera un cambio proporcionalmente mayor en cantidad demandada. +
  • +
  • + Unitaria (|Ep| = 1): Sensibilidad proporcional. + El gasto total de los consumidores se mantiene constante ante cambios de precio. +
  • +
  • + Inelástica (|Ep| < 1): Los consumidores son poco sensibles al precio. + La cantidad demandada cambia menos que proporcionalmente al precio. +
  • +
+
+
+ ); +} + +export default ClasificacionElasticidad; diff --git a/frontend/src/components/exercises/modulo3/ClasificadorBienes.tsx b/frontend/src/components/exercises/modulo3/ClasificadorBienes.tsx new file mode 100644 index 0000000..dbd9c88 --- /dev/null +++ b/frontend/src/components/exercises/modulo3/ClasificadorBienes.tsx @@ -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>({}); + 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 ? ( + + ) : ( + + ); + }; + + const puntuacion = mostrarResultados + ? bienesActuales.filter(bien => clasificaciones[bien.id] === bien.categoriaCorrecta).length + : 0; + + return ( + + + +
+
+ {categorias.map((cat) => ( +
+

{cat.nombre}

+

{cat.rango}

+

{cat.descripcion}

+
+ ))} +
+ +
+ {bienesActuales.map((bien) => ( +
+
+
+
+

{bien.nombre}

+ {getIconoEstado(bien)} +
+

{bien.descripcion}

+ {mostrarResultados && ( +

+ Ei = {bien.elasticidad} → {categorias.find(c => c.id === bien.categoriaCorrecta)?.nombre} +

+ )} +
+ +
+ {categorias.map((cat) => ( + + ))} +
+
+
+ ))} +
+ +
+
+ Progreso: {Object.keys(clasificaciones).length} / {bienesActuales.length} +
+ + {!mostrarResultados ? ( + + ) : ( +
+
+

Puntuación

+

+ {puntuacion} / {bienesActuales.length} +

+
+ +
+ )} +
+ + {mostrarResultados && ( +
= bienesActuales.length / 2 + ? 'bg-yellow-100 border border-yellow-300' + : 'bg-red-100 border border-red-300' + }`}> +

+ {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.'} +

+
+ )} +
+
+ ); +} + +export default ClasificadorBienes; diff --git a/frontend/src/components/exercises/modulo3/CurvaEngel.tsx b/frontend/src/components/exercises/modulo3/CurvaEngel.tsx new file mode 100644 index 0000000..78c67b1 --- /dev/null +++ b/frontend/src/components/exercises/modulo3/CurvaEngel.tsx @@ -0,0 +1,326 @@ +import { useState } from 'react'; +import { Button } from '../../ui/Button'; +import { Input } from '../../ui/Input'; +import { Card, CardHeader } from '../../ui/Card'; + +interface PuntoCurva { + ingreso: number; + cantidad: number; +} + +interface Ejercicio { + id: number; + titulo: string; + descripcion: string; + bien: string; + puntos: PuntoCurva[]; + tipoBien: 'lujo' | 'necesario' | 'inferior'; +} + +const ejercicios: Ejercicio[] = [ + { + id: 1, + titulo: "Curva de Engel - Bien de Lujo", + descripcion: "La siguiente tabla muestra cómo varía el consumo de restaurantes de lujo ante diferentes niveles de ingreso mensual.", + bien: "Restaurantes de lujo", + puntos: [ + { ingreso: 1000, cantidad: 0 }, + { ingreso: 2000, cantidad: 2 }, + { ingreso: 3000, cantidad: 6 }, + { ingreso: 4000, cantidad: 12 }, + { ingreso: 5000, cantidad: 20 } + ], + tipoBien: 'lujo' + }, + { + id: 2, + titulo: "Curva de Engel - Bien Necesario", + descripcion: "La siguiente tabla muestra cómo varía el consumo de leche ante diferentes niveles de ingreso mensual.", + bien: "Leche (litros)", + puntos: [ + { ingreso: 1000, cantidad: 8 }, + { ingreso: 2000, cantidad: 10 }, + { ingreso: 3000, cantidad: 11 }, + { ingreso: 4000, cantidad: 12 }, + { ingreso: 5000, cantidad: 12.5 } + ], + tipoBien: 'necesario' + }, + { + id: 3, + titulo: "Curva de Engel - Bien Inferior", + descripcion: "La siguiente tabla muestra cómo varía el consumo de pan de bagazo ante diferentes niveles de ingreso mensual.", + bien: "Pan de bagazo (kg)", + puntos: [ + { ingreso: 1000, cantidad: 15 }, + { ingreso: 2000, cantidad: 12 }, + { ingreso: 3000, cantidad: 8 }, + { ingreso: 4000, cantidad: 4 }, + { ingreso: 5000, cantidad: 1 } + ], + tipoBien: 'inferior' + } +]; + +interface Respuesta { + tipo: string | null; + esCorrecta: boolean | null; +} + +interface CurvaEngelProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +export function CurvaEngel({ ejercicioId: _ejercicioId, onComplete }: CurvaEngelProps) { + const [ejercicioActual, setEjercicioActual] = useState(0); + const [respuestas, setRespuestas] = useState>({}); + const [mostrarResultados, setMostrarResultados] = useState(false); + const [calculosElasticidad, setCalculosElasticidad] = useState>({}); + + const ejercicio = ejercicios[ejercicioActual]; + + const calcularElasticidadIntervalo = (p1: PuntoCurva, p2: PuntoCurva) => { + const deltaQ = p2.cantidad - p1.cantidad; + const deltaI = p2.ingreso - p1.ingreso; + const qPromedio = (p1.cantidad + p2.cantidad) / 2; + const iPromedio = (p1.ingreso + p2.ingreso) / 2; + + if (qPromedio === 0 || iPromedio === 0) return 0; + + const porcentajeQ = (deltaQ / qPromedio) * 100; + const porcentajeI = (deltaI / iPromedio) * 100; + return porcentajeQ / porcentajeI; + }; + + const handleCalculoChange = (intervalo: number, valor: string) => { + setCalculosElasticidad(prev => ({ + ...prev, + [`${ejercicio.id}_${intervalo}`]: valor + })); + }; + + const seleccionarTipo = (tipo: string) => { + if (mostrarResultados) return; + + setRespuestas(prev => ({ + ...prev, + [ejercicio.id]: { tipo, esCorrecta: null } + })); + }; + + const verificar = () => { + const respuesta = respuestas[ejercicio.id]; + if (!respuesta?.tipo) return; + + const esCorrecta = respuesta.tipo === ejercicio.tipoBien; + + setRespuestas(prev => ({ + ...prev, + [ejercicio.id]: { ...respuesta, esCorrecta } + })); + setMostrarResultados(true); + + if (esCorrecta && ejercicioActual === ejercicios.length - 1 && onComplete) { + const totalCorrectas = Object.values(respuestas).filter(r => r.esCorrecta).length + 1; + onComplete(Math.round((totalCorrectas / ejercicios.length) * 100)); + } + }; + + const siguienteEjercicio = () => { + if (ejercicioActual < ejercicios.length - 1) { + setEjercicioActual(prev => prev + 1); + setMostrarResultados(false); + } + }; + + const reiniciar = () => { + setEjercicioActual(0); + setRespuestas({}); + setMostrarResultados(false); + setCalculosElasticidad({}); + }; + + const respuestaActual = respuestas[ejercicio.id]; + + const getTipoColor = (tipo: string) => { + switch (tipo) { + case 'lujo': return 'bg-purple-100 border-purple-300 text-purple-800'; + case 'necesario': return 'bg-yellow-100 border-yellow-300 text-yellow-800'; + case 'inferior': return 'bg-red-100 border-red-300 text-red-800'; + default: return 'bg-gray-100 border-gray-300 text-gray-800'; + } + }; + + return ( + + + +
+
+
+
+
+ +
+
+

{ejercicio.titulo}

+

{ejercicio.descripcion}

+

+ Bien analizado: {ejercicio.bien} +

+
+ +
+ + + + + + + + + + {ejercicio.puntos.map((punto, index) => ( + + + + + + ))} + +
Ingreso Mensual ($)Cantidad ConsumidaElasticidad del Intervalo
${punto.ingreso.toLocaleString()}{punto.cantidad} + {index < ejercicio.puntos.length - 1 ? ( +
+ handleCalculoChange(index, e.target.value)} + /> + {mostrarResultados && ( + + = {calcularElasticidadIntervalo(punto, ejercicio.puntos[index + 1]).toFixed(2)} + + )} +
+ ) : ( + - + )} +
+
+ +
+

+ Según el comportamiento de la curva, ¿qué tipo de bien es "{ejercicio.bien}"? +

+ +
+ {['lujo', 'necesario', 'inferior'].map((tipo) => ( + + ))} +
+ + {!mostrarResultados ? ( + + ) : ( +
+

+ {respuestaActual?.esCorrecta + ? '¡Correcto!' + : 'Incorrecto. El tipo de bien es: '} +

+ {!respuestaActual?.esCorrecta && ( +

+ Bien {ejercicio.tipoBien === 'lujo' ? 'de Lujo' : ejercicio.tipoBien} +

+ )} + +
+

Análisis:

+
    + {ejercicio.puntos.slice(0, -1).map((punto, idx) => { + const ei = calcularElasticidadIntervalo(punto, ejercicio.puntos[idx + 1]); + return ( +
  • + Intervalo ${punto.ingreso}-${ejercicio.puntos[idx + 1].ingreso}: + Ei = {ei.toFixed(2)} +
  • + ); + })} +
+
+
+ )} +
+ +
+ {ejercicioActual > 0 && ( + + )} + +
+ + {ejercicioActual < ejercicios.length - 1 ? ( + + ) : ( + + )} +
+
+ + ); +} + +export default CurvaEngel; diff --git a/frontend/src/components/exercises/modulo3/CurvasIndiferencia.tsx b/frontend/src/components/exercises/modulo3/CurvasIndiferencia.tsx new file mode 100644 index 0000000..608a5bc --- /dev/null +++ b/frontend/src/components/exercises/modulo3/CurvasIndiferencia.tsx @@ -0,0 +1,336 @@ +import { useState } from 'react'; +import { Button } from '../../ui/Button'; +import { Card, CardHeader } from '../../ui/Card'; + +interface CurvasIndiferenciaProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Punto { + x: number; + y: number; + utilidad: number; +} + +const curvas: Punto[][] = [ + // Curva U=10: 2x + 3y = 10 + [ + { x: 0, y: 3.33, utilidad: 10 }, + { x: 2, y: 2, utilidad: 10 }, + { x: 5, y: 0, utilidad: 10 }, + ], + // Curva U=20: 2x + 3y = 20 + [ + { x: 1, y: 6, utilidad: 20 }, + { x: 4, y: 4, utilidad: 20 }, + { x: 7, y: 2, utilidad: 20 }, + { x: 10, y: 0, utilidad: 20 }, + ], + // Curva U=30: 2x + 3y = 30 + [ + { x: 0, y: 10, utilidad: 30 }, + { x: 3, y: 8, utilidad: 30 }, + { x: 6, y: 6, utilidad: 30 }, + { x: 9, y: 4, utilidad: 30 }, + { x: 12, y: 2, utilidad: 30 }, + { x: 15, y: 0, utilidad: 30 }, + ], +]; + +const puntosEjemplo = [ + { x: 2, y: 2, label: 'A', utilidad: 10 }, + { x: 4, y: 4, label: 'B', utilidad: 20 }, + { x: 6, y: 6, label: 'C', utilidad: 30 }, + { x: 3, y: 5, label: 'D', utilidad: 21 }, + { x: 8, y: 2, label: 'E', utilidad: 22 }, +]; + +export function CurvasIndiferencia({ ejercicioId: _ejercicioId, onComplete }: CurvasIndiferenciaProps) { + const [puntoSeleccionado, setPuntoSeleccionado] = useState(null); + const [mostrarPropiedades, setMostrarPropiedades] = useState(true); + const [preguntaRespuesta, setPreguntaRespuesta] = useState>({}); + const [verificado, setVerificado] = useState(false); + + const handleSeleccionPunto = (label: string) => { + setPuntoSeleccionado(label); + setVerificado(false); + }; + + const verificarRespuesta = (pregunta: string, respuestaCorrecta: string) => { + const esCorrecta = preguntaRespuesta[pregunta] === respuestaCorrecta; + setVerificado(true); + + if (esCorrecta && onComplete) { + onComplete(100); + } + + return esCorrecta; + }; + + return ( + + + +
+
+

Definición

+

+ Una curva de indiferencia muestra todas las combinaciones de dos bienes + que proporcionan al consumidor el mismo nivel de utilidad o satisfacción. + El consumidor es "indiferente" entre cualquiera de estas combinaciones. +

+
+ +
+

Mapa de Curvas de Indiferencia

+
+
+ + + + + Bien X (Unidades) + Bien Y (Unidades) + + {[0, 3, 6, 9, 12, 15].map((val) => ( + + + {val} + + ))} + + {[0, 2, 4, 6, 8, 10].map((val) => ( + + + {val} + + ))} + + {curvas.map((curva, idx) => ( + + `${50 + p.x * 26},${280 - p.y * 24}`).join(' ')} + fill="none" + stroke={['#3b82f6', '#10b981', '#f59e0b'][idx]} + strokeWidth="2" + /> + + U={curva[0].utilidad} + + + ))} + + {puntosEjemplo.map((punto) => ( + + handleSeleccionPunto(punto.label)} + /> + + {punto.label} + + + ))} + +
+ +

+ Haz clic en los puntos (A, B, C, D, E) para ver sus características. + Observa cómo las curvas más alejadas del origen representan mayor utilidad. +

+
+ + {puntoSeleccionado && ( +
+
+ Punto {puntoSeleccionado} +
+ {(() => { + const punto = puntosEjemplo.find(p => p.label === puntoSeleccionado); + if (!punto) return null; + return ( +
+

Bien X: {punto.x} unidades

+

Bien Y: {punto.y} unidades

+

Utilidad: {punto.utilidad} utils

+

+ Este punto se encuentra en la curva de indiferencia U={punto.utilidad}. + Cualquier otro punto en esta misma curva proporciona exactamente la misma satisfacción. +

+
+ ); + })()} +
+ )} +
+ +
+

Propiedades de las Curvas de Indiferencia

+
+
+
1
+
+

No se cortan

+

Dos curvas de indiferencia nunca pueden intersectarse. Si lo hicieran, implicaría que una misma combinación tiene dos niveles de utilidad diferentes.

+
+
+ +
+
2
+
+

Tienen pendiente negativa

+

Para mantener el mismo nivel de utilidad, si consumes más de un bien debes consumir menos del otro (sustitución).

+
+
+ +
+
3
+
+

Son convexas al origen

+

La TMS (Tasa Marginal de Sustitución) disminuye a medida que te mueves hacia abajo a lo largo de la curva.

+
+
+ +
+
4
+
+

Curvas más alejadas = Mayor utilidad

+

Las curvas más alejadas del origen representan niveles de utilidad más altos (U=30 {'>'} U=20 {'>'} U=10).

+
+
+
+
+ +
+

Tasa Marginal de Sustitución (TMS)

+

+ La TMS mide cuántas unidades de Y estás dispuesto a sacrificar por una unidad adicional de X, + manteniendo constante la utilidad. +

+
+ TMS = -ΔY/ΔX = UMgX / UMgY +
+
+

Ejemplo en el punto A (2,2):

+
+

• Para aumentar X de 2 a 4 (ΔX = +2), debes reducir Y de 2 a... ¿cuánto?

+

• En U=10: Si X=4, entonces 2(4) + 3Y = 10 → Y ≈ 0.67

+

• TMS = -(0.67 - 2)/(4 - 2) = -(-1.33)/2 = 0.67

+

+ Estás dispuesto a dar up aproximadamente 0.67 unidades de Y por cada unidad adicional de X. +

+ +
+

Ejercicios de Comprensión

+
+
+

1. ¿Qué significa que dos puntos estén en la misma curva de indiferencia?

+
+ {[ + { id: '1a', texto: 'Tienen los mismos precios', correcta: false }, + { id: '1b', texto: 'Proporcionan la misma utilidad', correcta: true }, + { id: '1c', texto: 'Son igualmente caros', correcta: false }, + { id: '1d', texto: 'Son bienes sustitutos perfectos', correcta: false }, + ].map((opcion) => ( + + ))} +
+ +
+

2. Según el mapa de curvas, ¿qué punto tiene mayor utilidad?

+
+ {[ + { id: '2a', texto: 'Punto A (2,2)', correcta: false }, + { id: '2b', texto: 'Punto B (4,4)', correcta: false }, + { id: '2c', texto: 'Punto C (6,6)', correcta: true }, + { id: '2d', texto: 'Punto D (3,5)', correcta: false }, + ].map((opcion) => ( + + ))} +
+ +
+

3. ¿Por qué las curvas de indiferencia tienen pendiente negativa?

+
+ {[ + { id: '3a', texto: 'Porque los bienes son complementarios', correcta: false }, + { id: '3b', texto: 'Para mantener la utilidad constante, más de X implica menos de Y', correcta: true }, + { id: '3c', texto: 'Porque los precios son inversos', correcta: false }, + { id: '3d', texto: 'Porque la utilidad marginal es negativa', correcta: false }, + ].map((opcion) => ( + + ))} +
+
+ +
+
+ + {verificado && ( +
+

+ ¡Respuestas verificadas! Revisa cuáles fueron correctas. +

+ )} +
+
+
+ ); +} + +export default CurvasIndiferencia; diff --git a/frontend/src/components/exercises/modulo3/DecisionesPrecios.tsx b/frontend/src/components/exercises/modulo3/DecisionesPrecios.tsx new file mode 100644 index 0000000..95b6d86 --- /dev/null +++ b/frontend/src/components/exercises/modulo3/DecisionesPrecios.tsx @@ -0,0 +1,358 @@ +import { useState } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { DollarSign, TrendingUp, TrendingDown, CheckCircle, RotateCcw } from 'lucide-react'; + +interface DecisionesPreciosProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Escenario { + id: number; + producto: string; + ep: number; + situacion: string; + pregunta: string; + opciones: { + respuesta: 'subir' | 'bajar' | 'mantener'; + label: string; + explicacionCorrecta: string; + explicacionIncorrecta: string; + }[]; +} + +const escenarios: Escenario[] = [ + { + id: 1, + producto: 'Medicamentos esenciales', + ep: -0.3, + situacion: 'Tu farmacia vende medicamentos esenciales con elasticidad de -0.3. Las ventas han disminuido y necesitas aumentar tus ingresos.', + pregunta: '¿Qué decisión de precios deberías tomar?', + opciones: [ + { + respuesta: 'subir', + label: 'Subir el precio', + explicacionCorrecta: 'Correcto. Con Ep = -0.3 (inelástico), al subir el precio la cantidad cae menos que proporcionalmente, aumentando los ingresos totales.', + explicacionIncorrecta: '', + }, + { + respuesta: 'bajar', + label: 'Bajar el precio', + explicacionCorrecta: '', + explicacionIncorrecta: 'Incorrecto. Con demanda inelástica, bajar el precio aumenta la cantidad menos que proporcionalmente, reduciendo los ingresos.', + }, + { + respuesta: 'mantener', + label: 'Mantener el precio', + explicacionCorrecta: '', + explicacionIncorrecta: 'Incorrecto. Con demanda inelástica, subir precios aumentaría los ingresos totales.', + }, + ], + }, + { + id: 2, + producto: 'Restaurante de lujo', + ep: -3.5, + situacion: 'Tu restaurante de alta cocina tiene una elasticidad de -3.5. La competencia está fuerte y necesitas atraer más clientes.', + pregunta: '¿Qué estrategia de precios recomiendas?', + opciones: [ + { + respuesta: 'subir', + label: 'Subir el precio', + explicacionCorrecta: '', + explicacionIncorrecta: 'Incorrecto. Con Ep = -3.5 (muy elástico), subir precios haría que muchos clientes dejen de venir, reduciendo ingresos drásticamente.', + }, + { + respuesta: 'bajar', + label: 'Bajar el precio', + explicacionCorrecta: 'Correcto. Con Ep = -3.5 (elástico), bajar el precio aumenta la cantidad más que proporcionalmente, incrementando los ingresos totales.', + explicacionIncorrecta: '', + }, + { + respuesta: 'mantener', + label: 'Mantener el precio', + explicacionCorrecta: '', + explicacionIncorrecta: 'Incorrecto. Con demanda elástica y competencia fuerte, bajar precios atraería más clientes y aumentaría ingresos.', + }, + ], + }, + { + id: 3, + producto: 'Gasolina', + ep: -0.8, + situacion: 'Tu gasolinera tiene una elasticidad de -0.8. Los costos han subido y necesitas cubrirlos.', + pregunta: '¿Deberías subir los precios de la gasolina?', + opciones: [ + { + respuesta: 'subir', + label: 'Sí, subir el precio', + explicacionCorrecta: 'Correcto. Con Ep = -0.8 (inelástico), subir el precio aumenta los ingresos totales porque la cantidad demandada cae menos que proporcionalmente.', + explicacionIncorrecta: '', + }, + { + respuesta: 'bajar', + label: 'No, bajar el precio', + explicacionCorrecta: '', + explicacionIncorrecta: 'Incorrecto. Bajar precios con demanda inelástica reduciría los ingresos totales, no ayudando a cubrir los costos mayores.', + }, + { + respuesta: 'mantener', + label: 'Mantener igual', + explicacionCorrecta: '', + explicacionIncorrecta: 'Incorrecto. Manteniendo precios no cubrirías los mayores costos. Subir precios aumentaría ingresos con demanda inelástica.', + }, + ], + }, + { + id: 4, + producto: 'Cine (entradas)', + ep: -1.8, + situacion: 'Tu cine tiene una elasticidad de -1.8. Es temporada baja y quieres llenar las salas.', + pregunta: '¿Qué decisión de precios tomarías?', + opciones: [ + { + respuesta: 'subir', + label: 'Subir el precio', + explicacionCorrecta: '', + explicacionIncorrecta: 'Incorrecto. Con Ep = -1.8 (elástico), subir precios reduciría significativamente la asistencia y los ingresos.', + }, + { + respuesta: 'bajar', + label: 'Bajar el precio', + explicacionCorrecta: 'Correcto. Con Ep = -1.8 (elástico), bajar precios aumentaría la asistencia más que proporcionalmente, llenando las salas y aumentando ingresos.', + explicacionIncorrecta: '', + }, + { + respuesta: 'mantener', + label: 'Mantener el precio', + explicacionCorrecta: '', + explicacionIncorrecta: 'Incorrecto. Mantener precios no ayudaría a llenar las salas en temporada baja. Bajar precios sería más efectivo.', + }, + ], + }, +]; + +export function DecisionesPrecios({ ejercicioId: _ejercicioId, onComplete }: DecisionesPreciosProps) { + const [escenarioIndex, setEscenarioIndex] = useState(0); + const [respuesta, setRespuesta] = useState<'subir' | 'bajar' | 'mantener' | null>(null); + const [validado, setValidado] = useState(false); + const [aciertos, setAciertos] = useState(0); + const [completado, setCompletado] = useState(false); + + const escenario = escenarios[escenarioIndex]; + const respuestaCorrecta = escenario.opciones.find((o) => o.explicacionCorrecta)?.respuesta; + + const validarRespuesta = () => { + if (!respuesta) return; + + const esCorrecto = respuesta === respuestaCorrecta; + setValidado(true); + + if (esCorrecto) { + setAciertos((prev) => prev + 1); + } + }; + + const siguienteEscenario = () => { + if (escenarioIndex < escenarios.length - 1) { + setEscenarioIndex((prev) => prev + 1); + setRespuesta(null); + setValidado(false); + } else { + setCompletado(true); + if (onComplete) { + onComplete(Math.round((aciertos + (respuesta === respuestaCorrecta ? 1 : 0)) / escenarios.length * 100)); + } + } + }; + + const reiniciar = () => { + setEscenarioIndex(0); + setRespuesta(null); + setValidado(false); + setAciertos(0); + setCompletado(false); + }; + + const obtenerClasificacion = (ep: number): string => { + const valorAbs = Math.abs(ep); + if (valorAbs > 1) return 'Elástica'; + if (valorAbs < 1) return 'Inelástica'; + return 'Unitaria'; + }; + + return ( +
+ + + +
+
+

+ + Demanda ELÁSTICA (|Ep| > 1) +

+
    +
  • • Bajar precio → Aumentan ingresos
  • +
  • • Subir precio → Disminuyen ingresos
  • +
  • • Los consumidores son muy sensibles
  • +
+
+
+

+ + Demanda INELÁSTICA (|Ep| < 1) +

+
    +
  • • Subir precio → Aumentan ingresos
  • +
  • • Bajar precio → Disminuyen ingresos
  • +
  • • Los consumidores son poco sensibles
  • +
+
+
+ +
+
+

+ + Caso {escenarioIndex + 1} de {escenarios.length}: {escenario.producto} +

+ + Ep = {escenario.ep} + +
+ +
+

+ Situación: {escenario.situacion} +

+

{escenario.pregunta}

+
+ +
+

+ Análisis: Este producto tiene demanda{' '} + {obtenerClasificacion(escenario.ep).toUpperCase()}{' '} + (|Ep| = {Math.abs(escenario.ep).toFixed(1)}) +

+
+ +
+ {escenario.opciones.map((opcion) => ( + + ))} +
+
+ + {validado && ( +
+ {escenario.opciones.map( + (opcion) => + (respuesta === opcion.respuesta || opcion.respuesta === respuestaCorrecta) && ( +
+

+ {opcion.explicacionCorrecta || opcion.explicacionIncorrecta} +

+
+ ) + )} +
+ )} + +
+ {!validado ? ( + + ) : !completado ? ( + + ) : ( + + )} +
+ + {completado && ( +
+

+ ¡Ejercicio completado! Has acertado {aciertos + (respuesta === respuestaCorrecta ? 1 : 0)} de{' '} + {escenarios.length} casos +

+
+ )} +
+ + +

Regla de Oro para Decisiones de Precios:

+
+

+ Ingreso Total (IT) = Precio × Cantidad +

+
    +
  • + • Si |Ep| > 1 (Elástica):{' '} + IT y P se mueven en direcciones opuestas +
  • +
  • + • Si |Ep| < 1 (Inelástica):{' '} + IT y P se mueven en la misma dirección +
  • +
  • + • Si |Ep| = 1 (Unitaria):{' '} + IT es máximo, cambios en P no afectan IT +
  • +
+
+
+
+ ); +} + +export default DecisionesPrecios; diff --git a/frontend/src/components/exercises/modulo3/EjerciciosExamen.tsx b/frontend/src/components/exercises/modulo3/EjerciciosExamen.tsx new file mode 100644 index 0000000..42130f8 --- /dev/null +++ b/frontend/src/components/exercises/modulo3/EjerciciosExamen.tsx @@ -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>({}); + const [mostrarSolucion, setMostrarSolucion] = useState>({}); + 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 ( + + + +
+
+
+
+
+ +
+
+

{problema.titulo}

+

{problema.descripcion}

+ +
+ {problema.datos.map((dato, idx) => ( +
+ {dato.etiqueta} +

{dato.valor}

+
+ ))} +
+
+ +
+ {problema.preguntas.map((pregunta, idx) => { + const respuesta = respuestas[pregunta.id]; + const estado = respuesta?.esCorrecta; + + return ( +
+
+ + {idx + 1} + +

{pregunta.texto}

+
+ +
+ {pregunta.tipo === 'numero' ? ( +
+ handleRespuesta(pregunta.id, e.target.value)} + className="w-48" + placeholder="Respuesta numérica" + /> + +
+ ) : ( +
+ {pregunta.opciones?.map((opcion) => ( + + ))} + +
+ )} + + {estado !== null && ( +
+ {estado ? '¡Correcto!' : 'Incorrecto. Intenta de nuevo.'} +
+ )} + + + + {mostrarSolucion[parseInt(pregunta.id.split('_')[0])] && ( +
+

Solución paso a paso:

+
    + {pregunta.solucion.map((paso, i) => ( +
  • {paso}
  • + ))} +
+
+ )} +
+
+ ); + })} +
+ +
+ + + {problemaActual < problemas.length - 1 ? ( + + ) : ( + + )} +
+ + {problemaActual === problemas.length - 1 && ( +
+

Puntuación actual: {calcularPuntuacion()}%

+
+ )} +
+ + ); +} + +export default EjerciciosExamen; diff --git a/frontend/src/components/exercises/modulo3/ElasticidadCurva.tsx b/frontend/src/components/exercises/modulo3/ElasticidadCurva.tsx new file mode 100644 index 0000000..40f55ee --- /dev/null +++ b/frontend/src/components/exercises/modulo3/ElasticidadCurva.tsx @@ -0,0 +1,369 @@ +import { useState, useCallback, useMemo } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { MousePointer, RotateCcw, Info } from 'lucide-react'; + +interface ElasticidadCurvaProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Punto { + q: number; + p: number; + ep: number; +} + +export function ElasticidadCurva({ ejercicioId: _ejercicioId, onComplete }: ElasticidadCurvaProps) { + const [puntoSeleccionado, setPuntoSeleccionado] = useState(50); + const [validado, setValidado] = useState(false); + const [completado, setCompletado] = useState(false); + + // Curva de demanda lineal: P = 20 - 0.2Q + const generarPuntos = useCallback((): Punto[] => { + const puntos: Punto[] = []; + for (let q = 0; q <= 100; q += 5) { + const p = 20 - 0.2 * q; + // Elasticidad en curva lineal: Ep = -b * (P/Q) donde b es la pendiente + const ep = -0.2 * (p / q); + puntos.push({ q, p: Math.max(0, p), ep: q > 0 ? ep : 0 }); + } + return puntos; + }, []); + + const puntos = useMemo(() => generarPuntos(), [generarPuntos]); + + const puntoActual = useMemo(() => { + const q = puntoSeleccionado; + const p = 20 - 0.2 * q; + const ep = q > 0 ? -0.2 * (p / q) : 0; + return { q, p: Math.max(0, p), ep }; + }, [puntoSeleccionado]); + + const obtenerClasificacion = useCallback((ep: number): string => { + const valorAbs = Math.abs(ep); + if (valorAbs > 1) return 'Elástica'; + if (valorAbs < 1) return 'Inelástica'; + return 'Unitaria'; + }, []); + + const validar = () => { + setValidado(true); + setCompletado(true); + if (onComplete) { + onComplete(100); + } + }; + + const reiniciar = () => { + setPuntoSeleccionado(50); + setValidado(false); + setCompletado(false); + }; + + // SVG config + const svgWidth = 400; + const svgHeight = 300; + const margin = { top: 20, right: 30, bottom: 50, left: 60 }; + const chartWidth = svgWidth - margin.left - margin.right; + const chartHeight = svgHeight - margin.top - margin.bottom; + + const scaleX = (q: number) => margin.left + (q / 100) * chartWidth; + const scaleY = (p: number) => margin.top + chartHeight - (p / 20) * chartHeight; + + // Puntos para la curva + const pathData = puntos + .filter((p) => p.p >= 0) + .map((p, i) => `${i === 0 ? 'M' : 'L'} ${scaleX(p.q)},${scaleY(p.p)}`) + .join(' '); + + // Punto unitario (donde Ep = -1) + const qUnitaria = 50; + const pUnitaria = 10; + + return ( +
+ + + +
+

+ + Concepto Clave: +

+

+ En una curva de demanda lineal, la elasticidad NO es constante. + Va desde elástica (parte alta) a inelástica (parte baja), pasando por unitaria en el punto medio. +

+
+ +
+ + { + setPuntoSeleccionado(Number(e.target.value)); + setValidado(false); + }} + className="w-full h-2 bg-gray-200 rounded-lg cursor-pointer" + style={{ accentColor: '#2563eb' }} + /> +
+ Q = 5 + Q = 50 + Q = 95 +
+
+ +
+

Valores en el punto seleccionado:

+
+
+

Cantidad (Q)

+

{puntoActual.q.toFixed(0)}

+
+
+

Precio (P)

+

${puntoActual.p.toFixed(1)}

+
+
+

Elasticidad (Ep)

+

{puntoActual.ep.toFixed(2)}

+
+
+
+ +
+

Gráfico de Demanda

+ + {/* Grid */} + + + + + + + + {/* Ejes */} + + + + {/* Flechas */} + + + + {/* Etiquetas */} + + Cantidad (Q) + + + Precio (P) + + + {/* Marcas X */} + {[0, 25, 50, 75, 100].map((val, i) => ( + + + + {val} + + + ))} + + {/* Marcas Y */} + {[0, 5, 10, 15, 20].map((val, i) => ( + + + + ${val} + + + ))} + + {/* Curva de demanda */} + + + {/* Punto unitario marcado */} + + + Unitario + + + {/* Punto seleccionado */} + 1 ? '#10b981' : Math.abs(puntoActual.ep) < 1 ? '#3b82f6' : '#fbbf24'} + stroke="white" + strokeWidth="3" + /> + + {/* Líneas punteadas al punto */} + + + + {/* Leyenda */} + + + + + Elástica (|Ep|>1) + + + + Unitaria (|Ep|=1) + + + + Inelástica (|Ep|<1) + + + +
+ +
+

Clasificación del punto actual:

+

+ En Q = {puntoActual.q.toFixed(0)}, P = ${puntoActual.p.toFixed(1)}: +

+

+ Demanda{' '} + 1 + ? 'text-green-600' + : Math.abs(puntoActual.ep) < 1 + ? 'text-blue-600' + : 'text-yellow-600' + } + > + {obtenerClasificacion(puntoActual.ep).toUpperCase()} + +

+

+ (|Ep| = {Math.abs(puntoActual.ep).toFixed(2)}) +

+
+ +
+ + +
+ + {completado && ( +
+

+ ¡Excelente! Has explorado cómo la elasticidad varía a lo largo de la curva. +

+
+ )} +
+ + +

Fórmula para curva lineal:

+

+ Para una curva de demanda lineal P = a - bQ, la elasticidad en cualquier punto es: +

+

+ Ep = -b × (P/Q) +

+

+ En este ejemplo: P = 20 - 0.2Q, por lo que b = 0.2 +

+

+ Punto unitario: Ocurre donde P/Q = 1/b = 5, es decir, en Q = 50, P = 10 +

+
+
+ ); +} + +export default ElasticidadCurva; diff --git a/frontend/src/components/exercises/modulo3/ElasticidadRectas.tsx b/frontend/src/components/exercises/modulo3/ElasticidadRectas.tsx new file mode 100644 index 0000000..674271a --- /dev/null +++ b/frontend/src/components/exercises/modulo3/ElasticidadRectas.tsx @@ -0,0 +1,426 @@ +import { useState, useCallback } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { RotateCcw, LineChart, AlertTriangle } from 'lucide-react'; + +interface ElasticidadRectasProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface CurvaData { + id: number; + pendiente: number; + intercepto: number; + color: string; + nombre: string; + descripcion: string; +} + +const curvas: CurvaData[] = [ + { + id: 1, + pendiente: 0.1, + intercepto: 15, + color: '#10b981', + nombre: 'Curva A (Plana)', + descripcion: 'Pendiente pequena → Mayor elasticidad', + }, + { + id: 2, + pendiente: 0.2, + intercepto: 20, + color: '#3b82f6', + nombre: 'Curva B (Media)', + descripcion: 'Pendiente media → Elasticidad media', + }, + { + id: 3, + pendiente: 0.4, + intercepto: 30, + color: '#ef4444', + nombre: 'Curva C (Empinada)', + descripcion: 'Pendiente grande → Menor elasticidad', + }, +]; + +export function ElasticidadRectas({ ejercicioId: _ejercicioId, onComplete }: ElasticidadRectasProps) { + const [puntoQ, setPuntoQ] = useState(40); + const [curvaSeleccionada, setCurvaSeleccionada] = useState(2); + const [respuestaEp, setRespuestaEp] = useState(''); + const [validado, setValidado] = useState(false); + const [completado, setCompletado] = useState(false); + + const curvaActual = curvas.find((c) => c.id === curvaSeleccionada) || curvas[1]; + + const calcularElasticidad = useCallback( + (q: number, curva: CurvaData): number => { + const p = curva.intercepto - curva.pendiente * q; + if (q <= 0 || p <= 0) return 0; + return -curva.pendiente * (p / q); + }, + [] + ); + + const epCorrecto = calcularElasticidad(puntoQ, curvaActual); + const precioActual = curvaActual.intercepto - curvaActual.pendiente * puntoQ; + + const validarRespuesta = () => { + const respuestaNum = parseFloat(respuestaEp); + const tolerancia = 0.2; + + setValidado(true); + + if (Math.abs(respuestaNum - epCorrecto) <= tolerancia) { + setCompletado(true); + if (onComplete) { + onComplete(100); + } + } + }; + + const reiniciar = () => { + setPuntoQ(40); + setCurvaSeleccionada(2); + setRespuestaEp(''); + setValidado(false); + setCompletado(false); + }; + + const svgWidth = 450; + const svgHeight = 350; + const margin = { top: 20, right: 40, bottom: 50, left: 60 }; + const chartWidth = svgWidth - margin.left - margin.right; + const chartHeight = svgHeight - margin.top - margin.bottom; + + const maxQ = 150; + const maxP = 35; + + const scaleX = (q: number) => margin.left + (q / maxQ) * chartWidth; + const scaleY = (p: number) => margin.top + chartHeight - (p / maxP) * chartHeight; + + const generarPath = (curva: CurvaData): string => { + const qMax = Math.min(maxQ, curva.intercepto / curva.pendiente); + const puntos: string[] = []; + for (let q = 0; q <= qMax; q += 5) { + const p = curva.intercepto - curva.pendiente * q; + if (p >= 0) { + puntos.push(`${scaleX(q)},${scaleY(p)}`); + } + } + return puntos.length > 0 ? `M ${puntos.join(' L ')}` : ''; + }; + + return ( +
+ + + +
+

+ + Concepto Importante: +

+

+ NO confundir pendiente con elasticidad. Aunque estan relacionadas, + son conceptos distintos. Una curva mas plana (menor pendiente) tiende a ser mas elastica, + pero la elasticidad tambien depende del punto (P/Q). +

+
+ +
+ {curvas.map((curva) => ( + + ))} +
+ +
+

+ + Comparacion de Curvas +

+ + + + + + + + + + + + + Cantidad (Q) + + + Precio (P) + + + {[0, 50, 100, 150].map((val) => ( + + + + {val} + + + ))} + + {[0, 10, 20, 30].map((val) => ( + + + + ${val} + + + ))} + + {curvas.map((curva) => ( + + ))} + + + + + + + {curvas.map((curva) => { + const qLabel = 20; + const pLabel = curva.intercepto - curva.pendiente * qLabel; + return ( + + {curva.nombre} + + ); + })} + +
+ +
+
+ + { + setPuntoQ(Number(e.target.value)); + setValidado(false); + }} + className="w-full h-2 bg-gray-200 rounded-lg cursor-pointer" + style={{ accentColor: curvaActual.color }} + /> +
+ Q = 10 + Q = {puntoQ} + Q = 100 +
+
+ +
+

Datos en el punto seleccionado:

+
+
+

Cantidad (Q)

+

{puntoQ}

+
+
+

Precio (P)

+

${precioActual.toFixed(2)}

+
+
+

Pendiente (b)

+

{curvaActual.pendiente}

+
+
+
+ +
+ +

+ Usa la formula: Ep = -b x (P/Q) +

+ { + setRespuestaEp(e.target.value); + setValidado(false); + }} + placeholder="Ej: -0.75" + className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent" + /> +
+
+ +
+ + +
+ + {validado && ( +
+

+ {completado + ? `Correcto! La elasticidad es ${epCorrecto.toFixed(2)}` + : `Incorrecto. La respuesta correcta es ${epCorrecto.toFixed(2)}`} +

+ {!completado && ( +

+ Recuerda: Ep = -{curvaActual.pendiente} x ({precioActual.toFixed(2)} / {puntoQ}) = {epCorrecto.toFixed(2)} +

+ )} +
+ )} +
+ + +

Relacion Pendiente vs Elasticidad:

+
    +
  • + Pendiente (b): Indica cuanto cambia P por cada unidad de Q. + Es la inclinacion geometrica de la recta. +
  • +
  • + Elasticidad (Ep): Indica cuanto cambia Q (%) por cada cambio de P (%). + Depende de la pendiente Y de la relacion P/Q en ese punto. +
  • +
  • + Conclusion: Una curva mas plana (menor b) NO siempre es mas elastica, + porque tambien depende de donde estes en la curva (el valor P/Q). +
  • +
+
+
+ ); +} + +export default ElasticidadRectas; diff --git a/frontend/src/components/exercises/modulo3/FormulaElasticidad.tsx b/frontend/src/components/exercises/modulo3/FormulaElasticidad.tsx new file mode 100644 index 0000000..a92ac59 --- /dev/null +++ b/frontend/src/components/exercises/modulo3/FormulaElasticidad.tsx @@ -0,0 +1,236 @@ +import { useState, useCallback } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { Calculator, RotateCcw, TrendingUp, TrendingDown } from 'lucide-react'; + +interface FormulaElasticidadProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +export function FormulaElasticidad({ ejercicioId: _ejercicioId, onComplete }: FormulaElasticidadProps) { + const [qInicial, setQInicial] = useState(100); + const [qFinal, setQFinal] = useState(80); + const [pInicial, setPInicial] = useState(10); + const [pFinal, setPFinal] = useState(12); + const [respuesta, setRespuesta] = useState(''); + const [validado, setValidado] = useState(false); + const [completado, setCompletado] = useState(false); + + const porcentajeCambioQ = useCallback(() => { + return ((qFinal - qInicial) / qInicial) * 100; + }, [qInicial, qFinal]); + + const porcentajeCambioP = useCallback(() => { + return ((pFinal - pInicial) / pInicial) * 100; + }, [pInicial, pFinal]); + + const elasticidadCorrecta = useCallback(() => { + return porcentajeCambioQ() / porcentajeCambioP(); + }, [porcentajeCambioQ, porcentajeCambioP]); + + const validarRespuesta = () => { + const respuestaNum = parseFloat(respuesta); + const correcta = elasticidadCorrecta(); + const tolerancia = 0.05; + + setValidado(true); + + if (Math.abs(respuestaNum - correcta) <= tolerancia) { + setCompletado(true); + if (onComplete) { + onComplete(100); + } + } + }; + + const reiniciar = () => { + setQInicial(100); + setQFinal(80); + setPInicial(10); + setPFinal(12); + setRespuesta(''); + setValidado(false); + setCompletado(false); + }; + + const pctQ = porcentajeCambioQ(); + const pctP = porcentajeCambioP(); + const correcta = elasticidadCorrecta(); + + return ( +
+ + + +
+

Fórmula:

+

+ Ep = %ΔQ / %ΔP +

+

+ Donde: %ΔQ = (Qf - Qi) / Qi × 100 +

+

+ %ΔP = (Pf - Pi) / Pi × 100 +

+
+ +
+
+

+ + Datos Iniciales +

+
+ + { + setQInicial(Number(e.target.value)); + setValidado(false); + }} + className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent" + /> +
+
+ + { + setPInicial(Number(e.target.value)); + setValidado(false); + }} + className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent" + /> +
+
+ +
+

+ + Datos Finales +

+
+ + { + setQFinal(Number(e.target.value)); + setValidado(false); + }} + className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent" + /> +
+
+ + { + setPFinal(Number(e.target.value)); + setValidado(false); + }} + className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent" + /> +
+
+
+ +
+

Cálculo Paso a Paso:

+
+

+ %ΔQ = ({qFinal} - {qInicial}) / {qInicial} × 100 = {pctQ.toFixed(2)}% +

+

+ %ΔP = ({pFinal} - {pInicial}) / {pInicial} × 100 = {pctP.toFixed(2)}% +

+

+ Ep = {pctQ.toFixed(2)} / {pctP.toFixed(2)} = {correcta.toFixed(2)} +

+
+
+ +
+
+ + { + setRespuesta(e.target.value); + setValidado(false); + }} + placeholder="Ej: -1.25" + className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent" + /> +
+ +
+ + +
+
+ + {validado && ( +
+

+ {completado + ? '¡Correcto! La elasticidad es ' + correcta.toFixed(2) + : 'Incorrecto. La respuesta correcta es ' + correcta.toFixed(2)} +

+
+ )} +
+ + +

Importante:

+
    +
  • • La elasticidad precio suele ser negativa (ley de demanda)
  • +
  • + • Usamos valor absoluto para clasificar: |Ep| > 1 = Elástica +
  • +
  • + • |Ep| = 1 = Unitaria, |Ep| < 1 = Inelástica +
  • +
+
+
+ ); +} + +export default FormulaElasticidad; diff --git a/frontend/src/components/exercises/modulo3/FormulaElasticidadCruzada.tsx b/frontend/src/components/exercises/modulo3/FormulaElasticidadCruzada.tsx new file mode 100644 index 0000000..c8f55e9 --- /dev/null +++ b/frontend/src/components/exercises/modulo3/FormulaElasticidadCruzada.tsx @@ -0,0 +1,286 @@ +import { useState } from 'react'; +import { Button } from '../../ui/Button'; +import { Input } from '../../ui/Input'; +import { Card, CardHeader } from '../../ui/Card'; + +interface Ejercicio { + id: number; + titulo: string; + descripcion: string; + bienX: string; + bienY: string; + pY1: number; + pY2: number; + qX1: number; + qX2: number; + unidadP: string; + unidadQ: string; +} + +const ejercicios: Ejercicio[] = [ + { + id: 1, + titulo: "Elasticidad Cruzada - Sustitutos", + descripcion: "Cuando el precio del café (bien Y) aumenta, observamos cambios en la demanda de té (bien X).", + bienX: "Té", + bienY: "Café", + pY1: 5, + pY2: 7, + qX1: 100, + qX2: 140, + unidadP: "$/libra", + unidadQ: "libras mensuales" + }, + { + id: 2, + titulo: "Elasticidad Cruzada - Complementarios", + descripcion: "Cuando el precio de las impresoras (bien Y) aumenta, observamos cambios en la demanda de tinta (bien X).", + bienX: "Cartuchos de tinta", + bienY: "Impresoras", + pY1: 80, + pY2: 120, + qX1: 500, + qX2: 350, + unidadP: "$", + unidadQ: "unidades mensuales" + }, + { + id: 3, + titulo: "Elasticidad Cruzada - Bienes Independientes", + descripcion: "Analiza la relación entre helado (bien X) y gasolina (bien Y).", + bienX: "Helado", + bienY: "Gasolina", + pY1: 3, + pY2: 4.5, + qX1: 200, + qX2: 205, + unidadP: "$/galón", + unidadQ: "litros mensuales" + } +]; + +interface Respuesta { + valor: string; + esCorrecta: boolean | null; +} + +interface FormulaElasticidadCruzadaProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +export function FormulaElasticidadCruzada({ ejercicioId: _ejercicioId, onComplete }: FormulaElasticidadCruzadaProps) { + const [ejercicioActual, setEjercicioActual] = useState(0); + const [respuestas, setRespuestas] = useState>({}); + const [mostrarSolucion, setMostrarSolucion] = useState>({}); + const [mostrarFormula, setMostrarFormula] = useState(false); + + const ejercicio = ejercicios[ejercicioActual]; + + const calcularElasticidad = (ej: Ejercicio) => { + const deltaQX = ej.qX2 - ej.qX1; + const deltaPY = ej.pY2 - ej.pY1; + const qXPromedio = (ej.qX1 + ej.qX2) / 2; + const pYPromedio = (ej.pY1 + ej.pY2) / 2; + const porcentajeQX = (deltaQX / qXPromedio) * 100; + const porcentajePY = (deltaPY / pYPromedio) * 100; + return porcentajeQX / porcentajePY; + }; + + const verificarRespuesta = () => { + const respuesta = respuestas[ejercicio.id]; + if (!respuesta) return; + + const valorCorrecto = calcularElasticidad(ejercicio); + const valorIngresado = parseFloat(respuesta.valor); + const esCorrecta = Math.abs(valorIngresado - valorCorrecto) <= 0.05; + + setRespuestas(prev => ({ + ...prev, + [ejercicio.id]: { ...respuesta, esCorrecta } + })); + + if (esCorrecta && ejercicioActual === ejercicios.length - 1 && onComplete) { + onComplete(100); + } + }; + + const handleRespuesta = (valor: string) => { + setRespuestas(prev => ({ + ...prev, + [ejercicio.id]: { valor, esCorrecta: null } + })); + }; + + const toggleSolucion = () => { + setMostrarSolucion(prev => ({ + ...prev, + [ejercicio.id]: !prev[ejercicio.id] + })); + }; + + const siguienteEjercicio = () => { + if (ejercicioActual < ejercicios.length - 1) { + setEjercicioActual(prev => prev + 1); + } + }; + + const resultado = calcularElasticidad(ejercicio); + const respuestaActual = respuestas[ejercicio.id]; + + return ( + + + +
+
+
+
+
+ +
+
+

{ejercicio.titulo}

+

{ejercicio.descripcion}

+ +
+
+

Bien X: {ejercicio.bienX}

+
+
+ QX1 +

{ejercicio.qX1}

+
+
+ QX2 +

{ejercicio.qX2}

+
+
+

{ejercicio.unidadQ}

+
+ +
+

Bien Y: {ejercicio.bienY}

+
+
+ PY1 +

${ejercicio.pY1}

+
+
+ PY2 +

${ejercicio.pY2}

+
+
+

{ejercicio.unidadP}

+
+
+
+ +
+
+

Fórmula de Elasticidad Cruzada:

+ +
+ + {mostrarFormula && ( +
+

+ Ecr = (%ΔQX) / (%ΔPY) +

+
+

Donde:

+

• %ΔQX = [(QX2 - QX1) / ((QX1 + QX2) / 2)] × 100

+

• %ΔPY = [(PY2 - PY1) / ((PY1 + PY2) / 2)] × 100

+
+
+ )} +
+ +
+

+ Calcule la elasticidad cruzada (Ecr) entre {ejercicio.bienX} y {ejercicio.bienY}: +

+ +
+ handleRespuesta(e.target.value)} + className="w-48" + placeholder="Respuesta" + /> + +
+ + {respuestaActual?.esCorrecta !== null && ( +
+ {respuestaActual.esCorrecta + ? '¡Correcto!' + : 'Incorrecto. Revisa tus cálculos.'} +
+ )} + + + + {mostrarSolucion[ejercicio.id] && ( +
+

Desarrollo:

+

ΔQX = {ejercicio.qX2} - {ejercicio.qX1} = {ejercicio.qX2 - ejercicio.qX1}

+

ΔPY = {ejercicio.pY2} - {ejercicio.pY1} = {ejercicio.pY2 - ejercicio.pY1}

+

X = ({ejercicio.qX1} + {ejercicio.qX2}) / 2 = {((ejercicio.qX1 + ejercicio.qX2) / 2).toFixed(1)}

+

Y = ({ejercicio.pY1} + {ejercicio.pY2}) / 2 = {((ejercicio.pY1 + ejercicio.pY2) / 2).toFixed(1)}

+

%ΔQX = ({ejercicio.qX2 - ejercicio.qX1} / {((ejercicio.qX1 + ejercicio.qX2) / 2).toFixed(1)}) × 100 = {(((ejercicio.qX2 - ejercicio.qX1) / ((ejercicio.qX1 + ejercicio.qX2) / 2)) * 100).toFixed(2)}%

+

%ΔPY = ({ejercicio.pY2 - ejercicio.pY1} / {((ejercicio.pY1 + ejercicio.pY2) / 2).toFixed(1)}) × 100 = {(((ejercicio.pY2 - ejercicio.pY1) / ((ejercicio.pY1 + ejercicio.pY2) / 2)) * 100).toFixed(2)}%

+

+ Ecr = {(((ejercicio.qX2 - ejercicio.qX1) / ((ejercicio.qX1 + ejercicio.qX2) / 2)) * 100).toFixed(2)} / {(((ejercicio.pY2 - ejercicio.pY1) / ((ejercicio.pY1 + ejercicio.pY2) / 2)) * 100).toFixed(2)} = {resultado.toFixed(2)} +

+
+ )} +
+ +
+ {ejercicioActual < ejercicios.length - 1 ? ( + + ) : ( +
+ {respuestaActual?.esCorrecta ? '¡Ejercicios completados!' : ''} +
+ )} +
+
+ + ); +} + +export default FormulaElasticidadCruzada; diff --git a/frontend/src/components/exercises/modulo3/FormulaElasticidadIngreso.tsx b/frontend/src/components/exercises/modulo3/FormulaElasticidadIngreso.tsx new file mode 100644 index 0000000..37188a2 --- /dev/null +++ b/frontend/src/components/exercises/modulo3/FormulaElasticidadIngreso.tsx @@ -0,0 +1,265 @@ +import { useState } from 'react'; +import { Button } from '../../ui/Button'; +import { Input } from '../../ui/Input'; +import { Card, CardHeader } from '../../ui/Card'; + +interface Ejercicio { + id: number; + titulo: string; + descripcion: string; + i1: number; + i2: number; + q1: number; + q2: number; + unidadI: string; + unidadQ: string; +} + +const ejercicios: Ejercicio[] = [ + { + id: 1, + titulo: "Cálculo de Elasticidad Ingreso", + descripcion: "Cuando el ingreso mensual de una familia aumenta de $2,000 a $2,500, su consumo de carne aumenta de 8 kg a 12 kg mensuales.", + i1: 2000, + i2: 2500, + q1: 8, + q2: 12, + unidadI: "$/mes", + unidadQ: "kg" + }, + { + id: 2, + titulo: "Elasticidad Ingreso - Producto Tecnológico", + descripcion: "El ingreso promedio de consumidores sube de $1,500 a $1,800 mensuales, y las ventas de smartphones premium aumentan de 50 a 80 unidades.", + i1: 1500, + i2: 1800, + q1: 50, + q2: 80, + unidadI: "$/mes", + unidadQ: "unidades" + }, + { + id: 3, + titulo: "Elasticidad Ingreso - Transporte", + descripcion: "Cuando el ingreso familiar aumenta de $3,000 a $4,000 mensuales, el uso de transporte público disminuye de 40 a 25 viajes mensuales.", + i1: 3000, + i2: 4000, + q1: 40, + q2: 25, + unidadI: "$/mes", + unidadQ: "viajes" + } +]; + +interface Respuesta { + valor: string; + esCorrecta: boolean | null; +} + +interface FormulaElasticidadIngresoProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +export function FormulaElasticidadIngreso({ ejercicioId: _ejercicioId, onComplete }: FormulaElasticidadIngresoProps) { + const [ejercicioActual, setEjercicioActual] = useState(0); + const [respuestas, setRespuestas] = useState>({}); + const [mostrarSolucion, setMostrarSolucion] = useState>({}); + const [mostrarFormula, setMostrarFormula] = useState(false); + + const ejercicio = ejercicios[ejercicioActual]; + + const calcularElasticidad = (ej: Ejercicio) => { + const deltaQ = ej.q2 - ej.q1; + const deltaI = ej.i2 - ej.i1; + const qPromedio = (ej.q1 + ej.q2) / 2; + const iPromedio = (ej.i1 + ej.i2) / 2; + const porcentajeQ = (deltaQ / qPromedio) * 100; + const porcentajeI = (deltaI / iPromedio) * 100; + return porcentajeQ / porcentajeI; + }; + + const verificarRespuesta = () => { + const respuesta = respuestas[ejercicio.id]; + if (!respuesta) return; + + const valorCorrecto = calcularElasticidad(ejercicio); + const valorIngresado = parseFloat(respuesta.valor); + const esCorrecta = Math.abs(valorIngresado - valorCorrecto) <= 0.05; + + setRespuestas(prev => ({ + ...prev, + [ejercicio.id]: { ...respuesta, esCorrecta } + })); + + if (esCorrecta && ejercicioActual === ejercicios.length - 1 && onComplete) { + onComplete(100); + } + }; + + const handleRespuesta = (valor: string) => { + setRespuestas(prev => ({ + ...prev, + [ejercicio.id]: { valor, esCorrecta: null } + })); + }; + + const toggleSolucion = () => { + setMostrarSolucion(prev => ({ + ...prev, + [ejercicio.id]: !prev[ejercicio.id] + })); + }; + + const siguienteEjercicio = () => { + if (ejercicioActual < ejercicios.length - 1) { + setEjercicioActual(prev => prev + 1); + } + }; + + const resultado = calcularElasticidad(ejercicio); + const respuestaActual = respuestas[ejercicio.id]; + + return ( + + + +
+
+
+
+
+ +
+
+

{ejercicio.titulo}

+

{ejercicio.descripcion}

+ +
+
+ I₁ +

{ejercicio.i1.toLocaleString()} {ejercicio.unidadI}

+
+
+ I₂ +

{ejercicio.i2.toLocaleString()} {ejercicio.unidadI}

+
+
+ Q₁ +

{ejercicio.q1} {ejercicio.unidadQ}

+
+
+ Q₂ +

{ejercicio.q2} {ejercicio.unidadQ}

+
+
+
+ +
+
+

Fórmula del método del punto medio:

+ +
+ + {mostrarFormula && ( +
+

+ Ei = (%ΔQ) / (%ΔI) +

+
+

Donde:

+

• %ΔQ = [(Q₂ - Q₁) / ((Q₁ + Q₂) / 2)] × 100

+

• %ΔI = [(I₂ - I₁) / ((I₁ + I₂) / 2)] × 100

+
+
+ )} +
+ +
+

+ Calcule la elasticidad ingreso (Ei): +

+ +
+ handleRespuesta(e.target.value)} + className="w-48" + placeholder="Respuesta" + /> + +
+ + {respuestaActual?.esCorrecta !== null && ( +
+ {respuestaActual.esCorrecta + ? '¡Correcto!' + : 'Incorrecto. Revisa tus cálculos.'} +
+ )} + + + + {mostrarSolucion[ejercicio.id] && ( +
+

Desarrollo:

+

ΔQ = {ejercicio.q2} - {ejercicio.q1} = {ejercicio.q2 - ejercicio.q1}

+

ΔI = {ejercicio.i2} - {ejercicio.i1} = {ejercicio.i2 - ejercicio.i1}

+

Q̄ = ({ejercicio.q1} + {ejercicio.q2}) / 2 = {((ejercicio.q1 + ejercicio.q2) / 2).toFixed(1)}

+

Ī = ({ejercicio.i1} + {ejercicio.i2}) / 2 = {((ejercicio.i1 + ejercicio.i2) / 2).toFixed(1)}

+

%ΔQ = ({ejercicio.q2 - ejercicio.q1} / {((ejercicio.q1 + ejercicio.q2) / 2).toFixed(1)}) × 100 = {(((ejercicio.q2 - ejercicio.q1) / ((ejercicio.q1 + ejercicio.q2) / 2)) * 100).toFixed(2)}%

+

%ΔI = ({ejercicio.i2 - ejercicio.i1} / {((ejercicio.i1 + ejercicio.i2) / 2).toFixed(1)}) × 100 = {(((ejercicio.i2 - ejercicio.i1) / ((ejercicio.i1 + ejercicio.i2) / 2)) * 100).toFixed(2)}%

+

+ Ei = {(((ejercicio.q2 - ejercicio.q1) / ((ejercicio.q1 + ejercicio.q2) / 2)) * 100).toFixed(2)} / {(((ejercicio.i2 - ejercicio.i1) / ((ejercicio.i1 + ejercicio.i2) / 2)) * 100).toFixed(2)} = {resultado.toFixed(2)} +

+
+ )} +
+ +
+ {ejercicioActual < ejercicios.length - 1 ? ( + + ) : ( +
+ {respuestaActual?.esCorrecta ? '¡Ejercicios completados!' : ''} +
+ )} +
+
+ + ); +} + +export default FormulaElasticidadIngreso; diff --git a/frontend/src/components/exercises/modulo3/GradoRelacion.tsx b/frontend/src/components/exercises/modulo3/GradoRelacion.tsx new file mode 100644 index 0000000..32fe26b --- /dev/null +++ b/frontend/src/components/exercises/modulo3/GradoRelacion.tsx @@ -0,0 +1,334 @@ +import { useState } from 'react'; +import { Button } from '../../ui/Button'; +import { Card, CardHeader } from '../../ui/Card'; + +interface ParBienes { + id: number; + bienX: string; + bienY: string; + categoria: string; + elasticidad: number; + tipoRelacion: 'sustitutos' | 'complementarios'; + interpretacion: string; +} + +const paresBienes: ParBienes[] = [ + { + id: 1, + bienX: "Coca-Cola", + bienY: "Pepsi", + categoria: "Refrescos", + elasticidad: 2.5, + tipoRelacion: 'sustitutos', + interpretacion: "Sustitutos cercanos. Elasticidad alta indica que los consumidores los ven como casi perfectamente intercambiables." + }, + { + id: 2, + bienX: "Café", + bienY: "Té", + categoria: "Bebidas calientes", + elasticidad: 0.8, + tipoRelacion: 'sustitutos', + interpretacion: "Sustitutos moderados. Elasticidad positiva pero menor indica cierta diferenciación entre los productos." + }, + { + id: 3, + bienX: "Automóviles", + bienY: "Gasolina", + categoria: "Transporte", + elasticidad: -0.3, + tipoRelacion: 'complementarios', + interpretacion: "Complementarios débiles. A corto plazo, los dueños de autos no pueden cambiar fácilmente su consumo de gasolina." + }, + { + id: 4, + bienX: "Computadoras", + bienY: "Software", + categoria: "Tecnología", + elasticidad: -2.0, + tipoRelacion: 'complementarios', + interpretacion: "Complementarios fuertes. Elasticidad negativa alta indica que se usan estrictamente juntos." + }, + { + id: 5, + bienX: "Mantequilla", + bienY: "Margarina", + categoria: "Grasas", + elasticidad: 1.8, + tipoRelacion: 'sustitutos', + interpretacion: "Sustitutos cercanos. Son productos similares que los consumidores intercambian fácilmente según el precio." + }, + { + id: 6, + bienX: "CDs de música", + bienY: "Conciertos", + categoria: "Entretenimiento", + elasticidad: 0.4, + tipoRelacion: 'sustitutos', + interpretacion: "Sustitutos débiles. Aunque ambos son música, satisfacen necesidades diferentes (hogar vs. experiencia)." + }, + { + id: 7, + bienX: "Cámaras", + bienY: "Película fotográfica", + categoria: "Fotografía", + elasticidad: -1.5, + tipoRelacion: 'complementarios', + interpretacion: "Complementarios moderados. Cámaras tradicionales requieren película para funcionar." + }, + { + id: 8, + bienX: "Hamburguesas", + bienY: "Papas fritas", + categoria: "Comida rápida", + elasticidad: -0.7, + tipoRelacion: 'complementarios', + interpretacion: "Complementarios moderados. Se consumen frecuentemente juntos en restaurantes de comida rápida." + } +]; + +type NivelRelacion = 'muy-fuerte' | 'fuerte' | 'moderado' | 'debil' | null; + +interface Respuesta { + nivel: NivelRelacion; + esCorrecta: boolean | null; +} + +interface GradoRelacionProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +export function GradoRelacion({ ejercicioId: _ejercicioId, onComplete }: GradoRelacionProps) { + const [respuestas, setRespuestas] = useState>({}); + const [mostrarResultados, setMostrarResultados] = useState(false); + + const getNivelEsperado = (elasticidad: number): NivelRelacion => { + const absE = Math.abs(elasticidad); + if (absE > 2) return 'muy-fuerte'; + if (absE > 1) return 'fuerte'; + if (absE > 0.5) return 'moderado'; + return 'debil'; + }; + + const seleccionarNivel = (parId: number, nivel: NivelRelacion) => { + if (mostrarResultados) return; + + setRespuestas(prev => ({ + ...prev, + [parId]: { nivel, esCorrecta: null } + })); + }; + + const verificarTodo = () => { + const nuevasRespuestas: Record = {}; + + paresBienes.forEach(par => { + const respuesta = respuestas[par.id]; + if (respuesta?.nivel) { + const nivelEsperado = getNivelEsperado(par.elasticidad); + nuevasRespuestas[par.id] = { + nivel: respuesta.nivel, + esCorrecta: respuesta.nivel === nivelEsperado + }; + } + }); + + setRespuestas(nuevasRespuestas); + setMostrarResultados(true); + + const correctas = Object.values(nuevasRespuestas).filter(r => r.esCorrecta).length; + if (onComplete) { + onComplete(Math.round((correctas / paresBienes.length) * 100)); + } + }; + + const reiniciar = () => { + setRespuestas({}); + setMostrarResultados(false); + }; + + const getCardStyle = (parId: number) => { + const respuesta = respuestas[parId]; + if (!mostrarResultados || !respuesta?.nivel) { + return 'bg-white border-gray-200'; + } + return respuesta.esCorrecta + ? 'bg-green-50 border-green-400' + : 'bg-red-50 border-red-400'; + }; + + const getNivelColor = (nivel: string) => { + switch (nivel) { + case 'muy-fuerte': return 'bg-purple-100 border-purple-300 text-purple-800'; + case 'fuerte': return 'bg-blue-100 border-blue-300 text-blue-800'; + case 'moderado': return 'bg-yellow-100 border-yellow-300 text-yellow-800'; + case 'debil': return 'bg-gray-100 border-gray-300 text-gray-800'; + default: return 'bg-white border-gray-200'; + } + }; + + const getNivelLabel = (nivel: string) => { + switch (nivel) { + case 'muy-fuerte': return 'Muy Fuerte'; + case 'fuerte': return 'Fuerte'; + case 'moderado': return 'Moderado'; + case 'debil': return 'Débil'; + default: return nivel; + } + }; + + const correctas = Object.values(respuestas).filter(r => r.esCorrecta).length; + const totalRespondidas = Object.keys(respuestas).length; + + return ( + + + +
+
+

Criterios de clasificación:

+
+
+

Muy Fuerte

+

|Ecr| > 2

+
+
+

Fuerte

+

1 < |Ecr| ≤ 2

+
+
+

Moderado

+

0.5 < |Ecr| ≤ 1

+
+
+

Débil

+

|Ecr| ≤ 0.5

+
+
+
+ +
+ {paresBienes.map((par) => { + const respuesta = respuestas[par.id]; + const nivelEsperado = getNivelEsperado(par.elasticidad); + + return ( +
+
+
+
+ + {par.categoria} + +
+ {par.bienX} + vs + {par.bienY} +
+
+ +
+

+ Ecr = {par.elasticidad} +

+

+ {par.tipoRelacion === 'sustitutos' ? 'Sustitutos' : 'Complementarios'} +

+
+
+ +
+ {['muy-fuerte', 'fuerte', 'moderado', 'debil'].map((nivel) => ( + + ))} +
+ + {mostrarResultados && ( +
+

+ Grado de relación:{' '} + + {getNivelLabel(nivelEsperado!)} + +

+

{par.interpretacion}

+
+ )} +
+
+ ); + })} +
+ +
+
+ Progreso: {totalRespondidas} / {paresBienes.length} +
+ + {!mostrarResultados ? ( + + ) : ( +
+
+

Puntuación

+

+ {correctas} / {paresBienes.length} +

+
+ +
+ )} +
+ + {mostrarResultados && ( +
= paresBienes.length / 2 + ? 'bg-yellow-100 border border-yellow-300' + : 'bg-red-100 border border-red-300' + }`}> +

+ {correctas === paresBienes.length + ? '¡Excelente! Has evaluado correctamente todos los grados de relación.' + : correctas >= paresBienes.length / 2 + ? '¡Buen trabajo! Algunos grados de relación necesitan más práctica.' + : 'Necesitas repasar cómo interpretar la magnitud de la elasticidad cruzada.'} +

+
+ )} +
+
+ ); +} + +export default GradoRelacion; diff --git a/frontend/src/components/exercises/modulo3/LeyUtilidadMarginalDecreciente.tsx b/frontend/src/components/exercises/modulo3/LeyUtilidadMarginalDecreciente.tsx new file mode 100644 index 0000000..2aa7ec9 --- /dev/null +++ b/frontend/src/components/exercises/modulo3/LeyUtilidadMarginalDecreciente.tsx @@ -0,0 +1,270 @@ +import { useState } from 'react'; +import { Button } from '../../ui/Button'; +import { Card, CardHeader } from '../../ui/Card'; + +interface LeyUtilidadMarginalDecrecienteProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Escenario { + id: string; + nombre: string; + descripcion: string; + datos: { unidad: number; um: number; ejemplo: string }[]; + explicacion: string; +} + +const escenarios: Escenario[] = [ + { + id: 'pizza', + nombre: 'Pizza 🍕', + descripcion: 'Utilidad marginal de comer rebanadas de pizza', + datos: [ + { unidad: 1, um: 20, ejemplo: '¡Deliciosa! Gran satisfacción' }, + { unidad: 2, um: 15, ejemplo: 'Muy buena, sigue siendo placentera' }, + { unidad: 3, um: 10, ejemplo: 'Aún rica, pero menos emocionante' }, + { unidad: 4, um: 5, ejemplo: 'Estoy llenándome...' }, + { unidad: 5, um: 0, ejemplo: 'No puedo más, estoy satisfecho' }, + { unidad: 6, um: -5, ejemplo: '¡Me siento mal! Demasiado' }, + ], + explicacion: 'Cada rebanada adicional aporta menos utilidad que la anterior. Después de la quinta, la utilidad se vuelve negativa (malestar).' + }, + { + id: 'cafe', + nombre: 'Café ☕', + descripcion: 'Utilidad marginal de tomar tazas de café', + datos: [ + { unidad: 1, um: 15, ejemplo: '¡Perfecto para empezar el día!' }, + { unidad: 2, um: 12, ejemplo: 'Aún disfruto mucho el sabor' }, + { unidad: 3, um: 8, ejemplo: 'Está bien, me mantiene despierto' }, + { unidad: 4, um: 3, ejemplo: 'Ya no sabe igual de bien' }, + { unidad: 5, um: -2, ejemplo: 'Me pone nervioso/a' }, + ], + explicacion: 'La primera taza da la mayor satisfacción. Después de la cuarta, la cafeína excesiva genera malestar.' + }, + { + id: 'netflix', + nombre: 'Series de Netflix 📺', + descripcion: 'Utilidad marginal de ver episodios seguidos', + datos: [ + { unidad: 1, um: 25, ejemplo: '¡Emocionante! Quiero saber qué pasa' }, + { unidad: 2, um: 22, ejemplo: 'La trama se pone mejor' }, + { unidad: 3, um: 18, ejemplo: 'Bien, sigue interesante' }, + { unidad: 4, um: 12, ejemplo: 'Me estoy cansando un poco' }, + { unidad: 5, um: 6, ejemplo: 'Ya quiero dormir...' }, + { unidad: 6, um: 0, ejemplo: 'Me duermo viendo la pantalla' }, + ], + explicacion: 'Aunque disfrutamos la serie, el cansancio hace que cada episodio adicional aporte menos utilidad.' + } +]; + +export function LeyUtilidadMarginalDecreciente({ ejercicioId: _ejercicioId, onComplete }: LeyUtilidadMarginalDecrecienteProps) { + const [escenarioActivo, setEscenarioActivo] = useState(escenarios[0]); + const [respuestas, setRespuestas] = useState>({}); + const [mostrarResultados, setMostrarResultados] = useState(false); + const [preguntaActiva, setPreguntaActiva] = useState(0); + + const preguntas = [ + { + id: 'p1', + texto: '¿Qué sucede con la utilidad marginal a medida que consumes más unidades de un bien?', + opciones: [ + { id: 'a', texto: 'Aumenta constantemente', correcta: false }, + { id: 'b', texto: 'Permanece igual', correcta: false }, + { id: 'c', texto: 'Disminuye (Ley de Utilidad Marginal Decreciente)', correcta: true }, + { id: 'd', texto: 'Se vuelve negativa inmediatamente', correcta: false }, + ] + }, + { + id: 'p2', + texto: 'En el ejemplo de la pizza, ¿en qué rebanada la utilidad marginal se vuelve negativa?', + opciones: [ + { id: 'a', texto: 'Segunda rebanada', correcta: false }, + { id: 'b', texto: 'Cuarta rebanada', correcta: false }, + { id: 'c', texto: 'Quinta rebanada', correcta: false }, + { id: 'd', texto: 'Sexta rebanada', correcta: true }, + ] + }, + { + id: 'p3', + texto: '¿Por qué la utilidad marginal disminuye?', + opciones: [ + { id: 'a', texto: 'Porque el bien es de mala calidad', correcta: false }, + { id: 'b', texto: 'Porque nuestras necesidades se van satisfechando', correcta: true }, + { id: 'c', texto: 'Porque aumenta el precio', correcta: false }, + { id: 'd', texto: 'Porque cambian nuestros gustos', correcta: false }, + ] + } + ]; + + const handleRespuesta = (preguntaId: string, opcionId: string, esCorrecta: boolean) => { + setRespuestas(prev => ({ ...prev, [preguntaId]: esCorrecta })); + }; + + const verificarResultados = () => { + setMostrarResultados(true); + const correctas = Object.values(respuestas).filter(Boolean).length; + const score = Math.round((correctas / preguntas.length) * 100); + if (onComplete) onComplete(score); + }; + + const maxUM = Math.max(...escenarioActivo.datos.map(d => d.um)); + const minUM = Math.min(...escenarioActivo.datos.map(d => d.um)); + + return ( + + + +
+
+

Ley de Utilidad Marginal Decreciente

+

+ A medida que un consumidor aumenta el consumo de un bien, la utilidad marginal que obtiene de cada unidad adicional tiende a disminuir. +

+
+ +
+ {escenarios.map((esc) => ( + + ))} +
+ +
+

{escenarioActivo.nombre}

+

{escenarioActivo.descripcion}

+ +
+
+ + + + + Unidades consumidas + Utilidad Marginal + + + 0 + + {escenarioActivo.datos.map((d, i) => { + const x = 80 + i * 70; + const y = d.um >= 0 + ? 110 - (d.um / maxUM) * 80 + : 110 + (Math.abs(d.um) / Math.abs(minUM)) * 40; + return ( + + = 0 ? y : 110} + width="30" + height={d.um >= 0 ? 110 - y : y - 110} + fill={d.um >= 0 ? '#3b82f6' : '#ef4444'} + opacity="0.7" + /> + = 0 ? 5 : -15)} textAnchor="middle" className="text-xs fill-gray-700 font-mono"> + {d.um} + + {d.unidad} + + ); + })} + +
+
+ +
+ {escenarioActivo.datos.map((d) => ( +
0 ? 'border-blue-200 bg-blue-50' : 'border-red-200 bg-red-50' + }`} + > +
+ Unidad {d.unidad} + = 0 ? 'text-blue-600' : 'text-red-600'}`}> + UM = {d.um} + +
+

{d.ejemplo}

+
+ ))} +
+ +
+

Análisis: {escenarioActivo.explicacion}

+
+
+ +
+

Preguntas de Comprensión

+ +
+ {preguntas.map((pregunta, idx) => ( +
+

{idx + 1}. {pregunta.texto}

+
+ {pregunta.opciones.map((opcion) => ( + + ))} +
+ ))} +
+ +
+
+ {mostrarResultados && ( + <> + Puntuación: {Object.values(respuestas).filter(Boolean).length}/{preguntas.length} + + )} +
+ +
+
+
+
+ ); +} + +export default LeyUtilidadMarginalDecreciente; diff --git a/frontend/src/components/exercises/modulo3/MaximizacionUtilidad.tsx b/frontend/src/components/exercises/modulo3/MaximizacionUtilidad.tsx new file mode 100644 index 0000000..1a0f0f3 --- /dev/null +++ b/frontend/src/components/exercises/modulo3/MaximizacionUtilidad.tsx @@ -0,0 +1,337 @@ +import { useState, useEffect, useCallback } from 'react'; +import { Button } from '../../ui/Button'; +import { Input } from '../../ui/Input'; +import { Card, CardHeader } from '../../ui/Card'; + +interface MaximizacionUtilidadProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Bien { + nombre: string; + um: number[]; + precio: number; +} + +const bienes: Record = { + pizza: { + nombre: 'Pizza', + um: [20, 15, 10, 5, 0, -5], + precio: 10 + }, + hamburguesa: { + nombre: 'Hamburguesa', + um: [18, 12, 8, 4, 0], + precio: 8 + } +}; + +export function MaximizacionUtilidad({ ejercicioId: _ejercicioId, onComplete }: MaximizacionUtilidadProps) { + const [presupuesto, setPresupuesto] = useState(50); + const [cantidadPizza, setCantidadPizza] = useState(0); + const [cantidadHamburguesa, setCantidadHamburguesa] = useState(0); + const [mostrarCalculos, setMostrarCalculos] = useState(false); + const [mostrarOptimo, setMostrarOptimo] = useState(false); + const [respuestaUsuario, setRespuestaUsuario] = useState({ pizza: '', hamburguesa: '' }); + const [verificado, setVerificado] = useState(false); + + const gastoTotal = cantidadPizza * bienes.pizza.precio + cantidadHamburguesa * bienes.hamburguesa.precio; + const dentroPresupuesto = gastoTotal <= presupuesto; + + const calcularUM = useCallback((tipo: 'pizza' | 'hamburguesa', cantidad: number) => { + const bien = bienes[tipo]; + if (cantidad === 0) return 0; + let total = 0; + for (let i = 0; i < Math.min(cantidad, bien.um.length); i++) { + total += bien.um[i]; + } + return total; + }, []); + + const calcularUMgP = useCallback((tipo: 'pizza' | 'hamburguesa', cantidad: number) => { + const bien = bienes[tipo]; + if (cantidad >= bien.um.length) return 0; + return bien.um[cantidad] / bien.precio; + }, []); + + const utilidadTotal = calcularUM('pizza', cantidadPizza) + calcularUM('hamburguesa', cantidadHamburguesa); + + const encontrarOptimo = useCallback(() => { + let mejorUT = 0; + let mejorCombo = { pizza: 0, hamburguesa: 0 }; + + for (let p = 0; p <= 5; p++) { + for (let h = 0; h <= 5; h++) { + const costo = p * bienes.pizza.precio + h * bienes.hamburguesa.precio; + if (costo <= presupuesto) { + const ut = calcularUM('pizza', p) + calcularUM('hamburguesa', h); + if (ut > mejorUT) { + mejorUT = ut; + mejorCombo = { pizza: p, hamburguesa: h }; + } + } + } + } + return mejorCombo; + }, [presupuesto, calcularUM]); + + const optimo = encontrarOptimo(); + + const verificarRespuesta = () => { + const pizzaCorrecta = parseInt(respuestaUsuario.pizza) === optimo.pizza; + const hamburguesaCorrecta = parseInt(respuestaUsuario.hamburguesa) === optimo.hamburguesa; + + setVerificado(true); + + if (pizzaCorrecta && hamburguesaCorrecta && onComplete) { + onComplete(100); + } + }; + + return ( + + + +
+
+

Regla de Maximización de Utilidad

+

+ Para maximizar la utilidad sujeto a un presupuesto, el consumidor debe igualar la utilidad marginal por peso gastado en todos los bienes: +

+
+ UMg₁/P₁ = UMg₂/P₂ = ... = UMgₙ/Pₙ +
+
+ +
+
+

🍕 Pizza

+
+
+ Precio: + ${bienes.pizza.precio} +
+
+

Utilidad Marginal por unidad:

+

{bienes.pizza.um.join(', ')}

+
+
+
+ +
+

🍔 Hamburguesa

+
+
+ Precio: + ${bienes.hamburguesa.precio} +
+
+

Utilidad Marginal por unidad:

+

{bienes.hamburguesa.um.join(', ')}

+
+
+
+
+ +
+
+ + setPresupuesto(parseInt(e.target.value) || 0)} + className="w-24" + /> +
+ +
+
+ +
+ + {cantidadPizza} + +
+
+ +
+ +
+ + {cantidadHamburguesa} + +
+
+
+ +
+

Resumen de tu selección:

+
+
+

Gasto Pizza: ${cantidadPizza * bienes.pizza.precio}

+

Gasto Hamburguesa: ${cantidadHamburguesa * bienes.hamburguesa.precio}

+

Total: ${gastoTotal}

+
+
+

UT Pizza: {calcularUM('pizza', cantidadPizza)}

+

UT Hamburguesa: {calcularUM('hamburguesa', cantidadHamburguesa)}

+

UT Total: {utilidadTotal}

+
+
+ {!dentroPresupuesto && ( +

⚠️ ¡Excedes el presupuesto!

+ )} +
+
+ +
+ + +
+ + {mostrarCalculos && ( +
+

Tabla de UMg/P (Utilidad Marginal por peso)

+
+ + + + + + + + + + + + {[0, 1, 2, 3, 4, 5].map((i) => ( + + + + + + + + ))} + +
UnidadUMg PizzaUMg/P PizzaUMg HamburguesaUMg/P Hamburguesa
{i + 1}{bienes.pizza.um[i] || '-'} + {bienes.pizza.um[i] ? (bienes.pizza.um[i] / bienes.pizza.precio).toFixed(2) : '-'} + {bienes.hamburguesa.um[i] || '-'} + {bienes.hamburguesa.um[i] ? (bienes.hamburguesa.um[i] / bienes.hamburguesa.precio).toFixed(2) : '-'} +
+
+

+ El consumidor racional comprará primero la unidad con mayor UMg/P, luego la siguiente, hasta agotar el presupuesto. +

+
+ )} + + {mostrarOptimo && ( +
+

Combinación Óptima

+

+ Con un presupuesto de ${presupuesto}, la combinación que maximiza la utilidad es: +

+
+

🍕 Pizza: {optimo.pizza} unidades

+

🍔 Hamburguesa: {optimo.hamburguesa} unidades

+

+ Utilidad Total Máxima: {calcularUM('pizza', optimo.pizza) + calcularUM('hamburguesa', optimo.hamburguesa)} +

+
+

+ En el óptimo, el consumidor gasta todo su presupuesto en la combinación que proporciona la mayor utilidad total posible. +

+
+ )} + +
+

Ejercicio: Encuentra el Óptimo

+

+ Usando un presupuesto de $50, ¿cuál es la combinación óptima de pizza y hamburguesas que maximiza la utilidad? +

+ +
+
+ + { + setRespuestaUsuario(prev => ({ ...prev, pizza: e.target.value })); + setVerificado(false); + }} + className="w-20" + /> +
+
+ + { + setRespuestaUsuario(prev => ({ ...prev, hamburguesa: e.target.value })); + setVerificado(false); + }} + className="w-20" + /> +
+ + +
+ + {verificado && ( +
+ {parseInt(respuestaUsuario.pizza) === optimo.pizza && + parseInt(respuestaUsuario.hamburguesa) === optimo.hamburguesa + ? '¡Correcto! Has encontrado la combinación óptima.' + : `Incorrecto. La combinación óptima es: ${optimo.pizza} pizzas y ${optimo.hamburguesa} hamburguesas.` + } +
+ )} +
+
+
+ ); +} + +export default MaximizacionUtilidad; diff --git a/frontend/src/components/exercises/modulo3/MetodoPuntoMedio.tsx b/frontend/src/components/exercises/modulo3/MetodoPuntoMedio.tsx new file mode 100644 index 0000000..531581c --- /dev/null +++ b/frontend/src/components/exercises/modulo3/MetodoPuntoMedio.tsx @@ -0,0 +1,247 @@ +import { useState, useCallback } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { Calculator, RotateCcw, Target } from 'lucide-react'; + +interface MetodoPuntoMedioProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface EjercicioData { + q1: number; + q2: number; + p1: number; + p2: number; + descripcion: string; +} + +const ejercicios: EjercicioData[] = [ + { + q1: 100, + q2: 120, + p1: 10, + p2: 8, + descripcion: 'Una empresa reduce el precio de su producto de $10 a $8 y las ventas aumentan de 100 a 120 unidades.', + }, + { + q1: 500, + q2: 400, + p1: 20, + p2: 25, + descripcion: 'El precio de un medicamento sube de $20 a $25 y la demanda cae de 500 a 400 unidades.', + }, + { + q1: 1000, + q2: 1050, + p1: 50, + p2: 48, + descripcion: 'Una tienda baja el precio de un artículo de $50 a $48 y las ventas suben de 1000 a 1050 unidades.', + }, +]; + +export function MetodoPuntoMedio({ ejercicioId: _ejercicioId, onComplete }: MetodoPuntoMedioProps) { + const [ejercicioIndex, setEjercicioIndex] = useState(0); + const [respuesta, setRespuesta] = useState(''); + const [validado, setValidado] = useState(false); + const [completado, setCompletado] = useState(false); + + const ejercicio = ejercicios[ejercicioIndex]; + + const calcularPuntoMedio = useCallback(() => { + const { q1, q2, p1, p2 } = ejercicio; + + // Método del punto medio (Arc Elasticity) + const qPromedio = (q1 + q2) / 2; + const pPromedio = (p1 + p2) / 2; + + const deltaQ = q2 - q1; + const deltaP = p2 - p1; + + const porcentajeQ = (deltaQ / qPromedio) * 100; + const porcentajeP = (deltaP / pPromedio) * 100; + + const elasticidad = porcentajeQ / porcentajeP; + + return { + qPromedio, + pPromedio, + deltaQ, + deltaP, + porcentajeQ, + porcentajeP, + elasticidad, + }; + }, [ejercicio]); + + const validarRespuesta = () => { + const { elasticidad } = calcularPuntoMedio(); + const respuestaNum = parseFloat(respuesta); + const tolerancia = 0.1; + + setValidado(true); + + if (Math.abs(respuestaNum - elasticidad) <= tolerancia) { + setCompletado(true); + if (onComplete) { + onComplete(100); + } + } + }; + + const siguienteEjercicio = () => { + setEjercicioIndex((prev) => (prev + 1) % ejercicios.length); + setRespuesta(''); + setValidado(false); + setCompletado(false); + }; + + const reiniciar = () => { + setRespuesta(''); + setValidado(false); + setCompletado(false); + }; + + const calculos = calcularPuntoMedio(); + + return ( +
+ + + +
+

Fórmula del Punto Medio:

+

+ Ep = (ΔQ / Qpromedio) / (ΔP / Ppromedio) +

+

+ Donde: Qpromedio = (Q1 + Q2) / 2, Ppromedio = (P1 + P2) / 2 +

+
+ +
+

+ + Ejercicio {ejercicioIndex + 1} de {ejercicios.length} +

+

{ejercicio.descripcion}

+
+ +
+
+
Punto 1
+

Q₁ = {ejercicio.q1} unidades

+

P₁ = ${ejercicio.p1}

+
+
+
Punto 2
+

Q₂ = {ejercicio.q2} unidades

+

P₂ = ${ejercicio.p2}

+
+
+ +
+

Desarrollo del Cálculo:

+
+

+ Qpromedio = ({ejercicio.q1} + {ejercicio.q2}) / 2 ={' '} + {calculos.qPromedio.toFixed(2)} +

+

+ Ppromedio = ({ejercicio.p1} + {ejercicio.p2}) / 2 ={' '} + {calculos.pPromedio.toFixed(2)} +

+

+ ΔQ = {ejercicio.q2} - {ejercicio.q1} = {calculos.deltaQ} +

+

ΔP = {ejercicio.p2} - {ejercicio.p1} = {calculos.deltaP}

+

+ %ΔQ = {calculos.deltaQ} / {calculos.qPromedio.toFixed(2)} ={' '} + {(calculos.porcentajeQ / 100).toFixed(4)} +

+

+ %ΔP = {calculos.deltaP} / {calculos.pPromedio.toFixed(2)} ={' '} + {(calculos.porcentajeP / 100).toFixed(4)} +

+

+ Ep = {(calculos.porcentajeQ / 100).toFixed(4)} / {(calculos.porcentajeP / 100).toFixed(4)} ={' '} + {calculos.elasticidad.toFixed(2)} +

+
+
+ +
+
+ + { + setRespuesta(e.target.value); + setValidado(false); + }} + placeholder="Ej: -0.82" + className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent" + /> +
+ +
+ + + {completado && ( + + )} +
+
+ + {validado && ( +
+

+ {completado + ? `¡Correcto! La elasticidad es ${calculos.elasticidad.toFixed(2)}` + : `Incorrecto. La respuesta correcta es ${calculos.elasticidad.toFixed(2)}`} +

+
+ )} +
+ + +

Ventaja del Método del Punto Medio:

+

+ El método del punto medio proporciona el mismo resultado independientemente de si el precio sube o baja, + evitando la asimetría del método tradicional. +

+

+ Ejemplo: Si el precio sube de $10 a $12 y luego baja de $12 a $10, + la elasticidad calculada es la misma en ambas direcciones. +

+
+
+ ); +} + +export default MetodoPuntoMedio; diff --git a/frontend/src/components/exercises/modulo3/ParadojaAguaDiamantes.tsx b/frontend/src/components/exercises/modulo3/ParadojaAguaDiamantes.tsx new file mode 100644 index 0000000..13cd32c --- /dev/null +++ b/frontend/src/components/exercises/modulo3/ParadojaAguaDiamantes.tsx @@ -0,0 +1,290 @@ +import { useState } from 'react'; +import { Button } from '../../ui/Button'; +import { Card, CardHeader } from '../../ui/Card'; + +interface ParadojaAguaDiamantesProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +export function ParadojaAguaDiamantes({ ejercicioId: _ejercicioId, onComplete }: ParadojaAguaDiamantesProps) { + const [respuestas, setRespuestas] = useState>({}); + const [verificadas, setVerificadas] = useState>({}); + const [mostrarExplicacion, setMostrarExplicacion] = useState(false); + + const preguntas = [ + { + id: 'p1', + pregunta: 'Según la teoría de la utilidad, ¿por qué el agua es barata a pesar de ser esencial?', + opciones: [ + { id: 'a', texto: 'Porque es fácil de producir', correcta: false }, + { id: 'b', texto: 'Porque su utilidad marginal es baja debido a la abundancia', correcta: true }, + { id: 'c', texto: 'Porque la gente no la valora', correcta: false }, + { id: 'd', texto: 'Porque el gobierno la subsidia', correcta: false }, + ] + }, + { + id: 'p2', + pregunta: '¿Por qué los diamantes son caros a pesar de no ser esenciales?', + opciones: [ + { id: 'a', texto: 'Porque son raros y escasos', correcta: false }, + { id: 'b', texto: 'Porque la gente es irracional', correcta: false }, + { id: 'c', texto: 'Porque su utilidad marginal es alta debido a la escasez', correcta: true }, + { id: 'd', texto: 'Porque cuestan mucho de extraer', correcta: false }, + ] + }, + { + id: 'p3', + pregunta: 'La paradoja se resuelve distinguiendo entre:', + opciones: [ + { id: 'a', texto: 'Utilidad total vs Utilidad marginal', correcta: true }, + { id: 'b', texto: 'Demanda y oferta', correcta: false }, + { id: 'c', texto: 'Bienes de lujo y necesarios', correcta: false }, + { id: 'd', texto: 'Precio y valor', correcta: false }, + ] + } + ]; + + const handleRespuesta = (preguntaId: string, opcionId: string, esCorrecta: boolean) => { + setRespuestas(prev => ({ ...prev, [preguntaId]: opcionId })); + setVerificadas(prev => ({ ...prev, [preguntaId]: esCorrecta })); + + const todasCorrectas = Object.values({ + ...verificadas, + [preguntaId]: esCorrecta + }).every(Boolean); + + if (todasCorrectas && onComplete) { + onComplete(100); + } + }; + + const correctas = Object.values(verificadas).filter(Boolean).length; + const total = preguntas.length; + + return ( + + + +
+
+

La Paradoja

+

+ Adam Smith planteó esta paradoja en "La Riqueza de las Naciones" (1776): + ¿Cómo puede algo tan esencial como el agua tener un valor tan bajo en el mercado, + mientras que los diamantes, que no son necesarios para la supervivencia, valen tanto? +

+
+ +
+
+
💧
+

Agua

+
+
+ Utilidad Total: + MUY ALTA +
+
+ Utilidad Marginal: + BAJA +
+
+ Disponibilidad: + Abundante +
+
+ Precio: + $2 por m³ +
+
+ +
+

+ Ejemplo: La primera botella de agua tiene utilidad infinita (supervivencia), + pero la enésima botella cuando ya estás hidratado tiene UMg cercana a cero. +

+
+
+ +
+
💎
+

Diamantes

+
+
+ Utilidad Total: + BAJA +
+
+ Utilidad Marginal: + ALTA +
+
+ Disponibilidad: + Escasa +
+
+ Precio: + $10,000 por quilate +
+
+ +
+

+ Ejemplo: El primer diamante para una joya tiene alta utilidad marginal + (exclusividad, estatus), pero tener muchos diamantes no añade tanta utilidad adicional. +

+
+
+
+ +
+

La Resolución de la Paradoja

+
+
+

🔑 El precio depende de:

+
    +
  • La utilidad marginal de la última unidad
  • +
  • La escasez del bien
  • +
  • La disposición a pagar por una unidad adicional
  • +
+
+ +
+

📊 NO del valor total:

+
    +
  • El agua tiene alta utilidad total pero baja UMg
  • +
  • Los diamantes tienen baja utilidad total pero alta UMg
  • +
  • Los precios reflejan valor marginal, no total
  • +
+
+
+
+ +
+

Gráfico Comparativo: Utilidad Marginal

+
+
+ + + + + Cantidad consumida + Utilidad Marginal + + Agua + Diamantes + + + UMg alta + + + UMg ≈ 0 (abundante) + + + UMg alta + + + Primera + + + Primero + + + Las curvas de UMg son diferentes por la abundancia vs escasez + + +
+ +

+ El precio se determina por la utilidad marginal de la última unidad. + Como el agua es abundante, su UMg en el margen es baja. Los diamantes son escasos, + manteniendo una UMg alta. +

+
+
+ +
+

Preguntas de Comprensión

+
+ {preguntas.map((pregunta, idx) => ( +
+

{idx + 1}. {pregunta.pregunta}

+
+ {pregunta.opciones.map((opcion) => ( + + ))} +
+ ))} +
+ +
+
+ Puntuación: {correctas}/{total} +
+ +
+ + {mostrarExplicacion && ( +
+
Explicación Detallada
+
+

+ 1. Valor Total vs Valor Marginal: El valor que asignamos a algo + no depende de su utilidad total, sino de lo que estaríamos dispuestos a pagar por + una unidad adicional. +

+

+ 2. El Agua: Aunque sin agua moriríamos (utilidad total infinita), + como hay mucha agua disponible, la utilidad marginal de una botella más es muy baja. + Por eso pagamos poco. +

+

+ 3. Los Diamantes: Aunque no los necesitamos para vivir, + son escasos. La utilidad marginal del primer (y único) diamante es alta porque + representa exclusividad, estatus y belleza. +

+

+ 4. Conclusión: Los precios reflejan valores marginales, + no valores totales. Esto es fundamental para entender cómo funcionan los mercados. +

+
+ )} +
+
+
+ ); +} + +export default ParadojaAguaDiamantes; diff --git a/frontend/src/components/exercises/modulo3/SustitutosComplementarios.tsx b/frontend/src/components/exercises/modulo3/SustitutosComplementarios.tsx new file mode 100644 index 0000000..dcf3b81 --- /dev/null +++ b/frontend/src/components/exercises/modulo3/SustitutosComplementarios.tsx @@ -0,0 +1,328 @@ +import { useState } from 'react'; +import { Button } from '../../ui/Button'; +import { Card, CardHeader } from '../../ui/Card'; + +interface ParBienes { + id: number; + bienX: string; + bienY: string; + descripcion: string; + elasticidad: number; + relacionCorrecta: 'sustitutos' | 'complementarios' | 'independientes'; + explicacion: string; +} + +const paresBienes: ParBienes[] = [ + { + id: 1, + bienX: "Cerveza", + bienY: "Vino", + descripcion: "Bebidas alcohólicas que los consumidores pueden intercambiar", + elasticidad: 0.8, + relacionCorrecta: 'sustitutos', + explicacion: "Ecr > 0 indica que son sustitutos. Cuando sube el precio del vino, algunos consumidores cambian a cerveza." + }, + { + id: 2, + bienX: "Tinta de impresora", + bienY: "Impresoras", + descripcion: "Productos que se usan juntos", + elasticidad: -1.2, + relacionCorrecta: 'complementarios', + explicacion: "Ecr < 0 indica que son complementarios. Si sube el precio de las impresoras, se compran menos impresoras y por tanto menos tinta." + }, + { + id: 3, + bienX: "Mantequilla", + bienY: "Margarina", + descripcion: "Grasas para cocinar/similar uso", + elasticidad: 1.5, + relacionCorrecta: 'sustitutos', + explicacion: "Ecr > 0 indica que son sustitutos cercanos. Son productos muy intercambiables para los consumidores." + }, + { + id: 4, + bienX: "Hoteles", + bienY: "Gasolina", + descripcion: "Servicio de alojamiento y combustible", + elasticidad: 0.05, + relacionCorrecta: 'independientes', + explicacion: "Ecr ≈ 0 indica que son independientes. El precio de la gasolina casi no afecta la demanda de hoteles." + }, + { + id: 5, + bienX: "Automóviles", + bienY: "Gasolina", + descripcion: "Vehículos y su combustible", + elasticidad: -0.6, + relacionCorrecta: 'complementarios', + explicacion: "Ecr < 0 indica complementariedad. Si sube el precio de la gasolina, la demanda de autos (especialmente grandes) disminuye." + }, + { + id: 6, + bienX: "Coca-Cola", + bienY: "Pepsi", + descripcion: "Bebidas gaseosas similares", + elasticidad: 2.1, + relacionCorrecta: 'sustitutos', + explicacion: "Ecr > 0 indica sustitutos. Elasticidad alta porque son productos casi perfectamente intercambiables." + }, + { + id: 7, + bienX: "Computadoras", + bienY: "Software", + descripcion: "Hardware y programas", + elasticidad: -1.8, + relacionCorrecta: 'complementarios', + explicacion: "Ecr < 0 indica fuerte complementariedad. Computadoras y software se usan juntos obligatoriamente." + }, + { + id: 8, + bienX: "Zapatos", + bienY: "Pan", + descripcion: "Calzado y alimento básico", + elasticidad: 0.01, + relacionCorrecta: 'independientes', + explicacion: "Ecr ≈ 0 indica independencia. No existe relación económica entre estos bienes." + } +]; + +interface Respuesta { + relacion: string | null; + esCorrecta: boolean | null; +} + +interface SustitutosComplementariosProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +export function SustitutosComplementarios({ ejercicioId: _ejercicioId, onComplete }: SustitutosComplementariosProps) { + const [respuestas, setRespuestas] = useState>({}); + const [mostrarResultados, setMostrarResultados] = useState(false); + + const seleccionarRelacion = (parId: number, relacion: string) => { + if (mostrarResultados) return; + + setRespuestas(prev => ({ + ...prev, + [parId]: { relacion, esCorrecta: null } + })); + }; + + const verificarTodo = () => { + const nuevasRespuestas: Record = {}; + + paresBienes.forEach(par => { + const respuesta = respuestas[par.id]; + if (respuesta?.relacion) { + nuevasRespuestas[par.id] = { + relacion: respuesta.relacion, + esCorrecta: respuesta.relacion === par.relacionCorrecta + }; + } + }); + + setRespuestas(nuevasRespuestas); + setMostrarResultados(true); + + const correctas = Object.values(nuevasRespuestas).filter(r => r.esCorrecta).length; + if (onComplete) { + onComplete(Math.round((correctas / paresBienes.length) * 100)); + } + }; + + const reiniciar = () => { + setRespuestas({}); + setMostrarResultados(false); + }; + + const getCardStyle = (parId: number) => { + const respuesta = respuestas[parId]; + if (!mostrarResultados || !respuesta?.relacion) { + return 'bg-white border-gray-200'; + } + return respuesta.esCorrecta + ? 'bg-green-50 border-green-400' + : 'bg-red-50 border-red-400'; + }; + + const getRelacionColor = (relacion: string) => { + switch (relacion) { + case 'sustitutos': return 'bg-green-100 border-green-300 text-green-800'; + case 'complementarios': return 'bg-red-100 border-red-300 text-red-800'; + case 'independientes': return 'bg-gray-100 border-gray-300 text-gray-800'; + default: return 'bg-white border-gray-200'; + } + }; + + const correctas = Object.values(respuestas).filter(r => r.esCorrecta).length; + const totalRespondidas = Object.keys(respuestas).length; + + return ( + + + +
+
+
+

Sustitutos

+

Ecr > 0

+

+ Cuando sube el precio de Y, aumenta la demanda de X. Los bienes compiten entre sí. +

+
+
+

Complementarios

+

Ecr < 0

+

+ Cuando sube el precio de Y, disminuye la demanda de X. Se consumen juntos. +

+
+
+

Independientes

+

Ecr ≈ 0

+

+ El precio de Y no afecta la demanda de X. No existe relación entre ellos. +

+
+
+ +
+ {paresBienes.map((par) => { + const respuesta = respuestas[par.id]; + + return ( +
+
+
+
+
+ {par.bienX} +
+ vs +
+ {par.bienY} +
+
+ +

{par.descripcion}

+ + {mostrarResultados && ( +
+

+ Ecr = {par.elasticidad} +

+

+ {par.relacionCorrecta === 'sustitutos' && 'Sustitutos'} + {par.relacionCorrecta === 'complementarios' && 'Complementarios'} + {par.relacionCorrecta === 'independientes' && 'Independientes'} +

+

{par.explicacion}

+
+ )} +
+ +
+ + + +
+
+
+ ); + })} +
+ +
+
+ Progreso: {totalRespondidas} / {paresBienes.length} +
+ + {!mostrarResultados ? ( + + ) : ( +
+
+

Puntuación

+

+ {correctas} / {paresBienes.length} +

+
+ +
+ )} +
+ + {mostrarResultados && ( +
= paresBienes.length / 2 + ? 'bg-yellow-100 border border-yellow-300' + : 'bg-red-100 border border-red-300' + }`}> +

+ {correctas === paresBienes.length + ? '¡Excelente! Has identificado todas las relaciones correctamente.' + : correctas >= paresBienes.length / 2 + ? '¡Buen trabajo! Algunas relaciones necesitan más atención.' + : 'Necesitas repasar la diferencia entre bienes sustitutos, complementarios e independientes.'} +

+
+ )} +
+
+ ); +} + +export default SustitutosComplementarios; diff --git a/frontend/src/components/exercises/modulo3/UtilidadTotalVsMarginal.tsx b/frontend/src/components/exercises/modulo3/UtilidadTotalVsMarginal.tsx new file mode 100644 index 0000000..8d83342 --- /dev/null +++ b/frontend/src/components/exercises/modulo3/UtilidadTotalVsMarginal.tsx @@ -0,0 +1,243 @@ +import { useState, useEffect, useCallback } from 'react'; +import { Button } from '../../ui/Button'; +import { Input } from '../../ui/Input'; +import { Card, CardHeader } from '../../ui/Card'; + +interface UtilidadTotalVsMarginalProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface FilaDatos { + cantidad: number; + utilidadTotal: number; + utilidadMarginal: number | null; +} + +const datosBase: Omit[] = [ + { cantidad: 0, utilidadTotal: 0 }, + { cantidad: 1, utilidadTotal: 10 }, + { cantidad: 2, utilidadTotal: 18 }, + { cantidad: 3, utilidadTotal: 24 }, + { cantidad: 4, utilidadTotal: 28 }, + { cantidad: 5, utilidadTotal: 30 }, + { cantidad: 6, utilidadTotal: 30 }, + { cantidad: 7, utilidadTotal: 28 }, +]; + +export function UtilidadTotalVsMarginal({ ejercicioId: _ejercicioId, onComplete }: UtilidadTotalVsMarginalProps) { + const [respuestas, setRespuestas] = useState>({}); + const [verificadas, setVerificadas] = useState>({}); + const [mostrarGrafico, setMostrarGrafico] = useState(false); + const [mostrarExplicacion, setMostrarExplicacion] = useState(false); + + const datosCompletos: FilaDatos[] = datosBase.map((fila, index) => ({ + ...fila, + utilidadMarginal: index === 0 ? null : fila.utilidadTotal - datosBase[index - 1].utilidadTotal + })); + + const calcularUMg = useCallback((q: number) => { + const fila = datosCompletos.find(d => d.cantidad === q); + return fila?.utilidadMarginal ?? 0; + }, [datosCompletos]); + + const handleRespuesta = (cantidad: number, valor: string) => { + setRespuestas(prev => ({ ...prev, [cantidad]: valor })); + setVerificadas(prev => ({ ...prev, [cantidad]: false })); + }; + + const verificarRespuesta = (cantidad: number) => { + const respuesta = parseFloat(respuestas[cantidad]); + const correcta = calcularUMg(cantidad); + const esCorrecta = Math.abs(respuesta - correcta) < 0.1; + + setVerificadas(prev => ({ ...prev, [cantidad]: esCorrecta })); + + const todasCorrectas = datosCompletos + .filter(d => d.cantidad > 0) + .every(d => { + const r = parseFloat(respuestas[d.cantidad]); + return Math.abs(r - calcularUMg(d.cantidad)) < 0.1; + }); + + if (todasCorrectas && onComplete) { + onComplete(100); + } + }; + + const puntaje = Object.values(verificadas).filter(Boolean).length; + const total = datosCompletos.length - 1; + const porcentaje = Math.round((puntaje / total) * 100); + + const maxUT = Math.max(...datosCompletos.map(d => d.utilidadTotal)); + const maxQ = Math.max(...datosCompletos.map(d => d.cantidad)); + + return ( + + + +
+
+

Conceptos Clave

+
    +
  • Utilidad Total (UT): Satisfacción total obtenida de consumir Q unidades de un bien.
  • +
  • Utilidad Marginal (UMg): Utilidad adicional obtenida de consumir una unidad más.
  • +
  • Fórmula: UMg = ΔUT / ΔQ = UT(Q) - UT(Q-1)
  • +
+
+ +
+ + + + + + + + + + + {datosCompletos.map((fila) => ( + + + + + + + ))} + +
Cantidad (Q)Utilidad Total (UT)Calcular UMgEstado
{fila.cantidad}{fila.utilidadTotal} + {fila.cantidad === 0 ? ( + N/A (punto de partida) + ) : ( +
+ handleRespuesta(fila.cantidad, e.target.value)} + className="w-24" + placeholder="UMg" + /> + +
+ )} +
+ {verificadas[fila.cantidad] === true && ( + ✓ Correcto + )} + {verificadas[fila.cantidad] === false && ( + ✗ Incorrecto + )} + {fila.cantidad > 0 && verificadas[fila.cantidad] === undefined && ( + - + )} +
+
+ +
+

+ Progreso: {puntaje}/{total} correctas ({porcentaje}%) +

+
+
+
+
+ +
+ + +
+ + {mostrarGrafico && ( +
+

Gráfico de Utilidad Total

+
+ + + + + Cantidad (Q) + Utilidad Total + + {datosCompletos.map((d, i) => { + const x = 60 + (d.cantidad / maxQ) * 300; + const y = 210 - (d.utilidadTotal / maxUT) * 180; + return ( + + + + {d.utilidadTotal} + + {d.cantidad} + + ); + })} + + { + const x = 60 + (d.cantidad / maxQ) * 300; + const y = 210 - (d.utilidadTotal / maxUT) * 180; + return `${x},${y}`; + }).join(' ')} + fill="none" + stroke="#3b82f6" + strokeWidth="2" + /> + +
+

+ Observa cómo la curva de utilidad total aumenta a tasas decrecientes hasta alcanzar su máximo en Q=5 y Q=6. +

+
+ )} + + {mostrarExplicacion && ( +
+

Cálculo paso a paso:

+
+ {datosCompletos.filter(d => d.cantidad > 0).map((fila) => ( +

+ Q={fila.cantidad}: UMg = UT({fila.cantidad}) - UT({fila.cantidad - 1}) = {fila.utilidadTotal} - {datosCompletos[fila.cantidad - 1].utilidadTotal} = {fila.utilidadMarginal} +

+ ))} +
+
+

Puntos importantes:

+
    +
  • La UMg es positiva mientras la UT esté aumentando (Q=1 a 5)
  • +
  • La UMg es cero cuando la UT es máxima (Q=6)
  • +
  • La UMg es negativa cuando la UT disminuye (Q=7)
  • +
+
+
+ )} +
+ + ); +} + +export default UtilidadTotalVsMarginal; diff --git a/frontend/src/components/exercises/modulo3/index.ts b/frontend/src/components/exercises/modulo3/index.ts new file mode 100644 index 0000000..d59e93e --- /dev/null +++ b/frontend/src/components/exercises/modulo3/index.ts @@ -0,0 +1,22 @@ +export { UtilidadTotalVsMarginal } from './UtilidadTotalVsMarginal'; +export { LeyUtilidadMarginalDecreciente } from './LeyUtilidadMarginalDecreciente'; +export { CanastaOptima } from './CanastaOptima'; +export { CurvasIndiferencia } from './CurvasIndiferencia'; +export { MaximizacionUtilidad } from './MaximizacionUtilidad'; +export { ElasticidadRectas } from './ElasticidadRectas'; +export { ElasticidadCurva } from './ElasticidadCurva'; +export { FormulaElasticidad } from './FormulaElasticidad'; +export { MetodoPuntoMedio } from './MetodoPuntoMedio'; +export { CalculadoraElasticidad } from './CalculadoraElasticidad'; +export { ClasificacionElasticidad } from './ClasificacionElasticidad'; +export { DecisionesPrecios } from './DecisionesPrecios'; +export { FormulaElasticidadCruzada } from './FormulaElasticidadCruzada'; +export { SustitutosComplementarios } from './SustitutosComplementarios'; +export { GradoRelacion } from './GradoRelacion'; +export { FormulaElasticidadIngreso } from './FormulaElasticidadIngreso'; +export { BienesNormalesInferiores } from './BienesNormalesInferiores'; +export { BienesLujoNecesarios } from './BienesLujoNecesarios'; +export { CurvaEngel } from './CurvaEngel'; +export { ParadojaAguaDiamantes } from './ParadojaAguaDiamantes'; +export { ClasificadorBienes } from './ClasificadorBienes'; +export { EjerciciosExamen } from './EjerciciosExamen'; diff --git a/frontend/src/components/exercises/modulo4/CalculadoraCostos.tsx b/frontend/src/components/exercises/modulo4/CalculadoraCostos.tsx new file mode 100644 index 0000000..ddf97b4 --- /dev/null +++ b/frontend/src/components/exercises/modulo4/CalculadoraCostos.tsx @@ -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([ + { 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([]); + + 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 ( +
+ + + +
+ + + + + + + + + + + + + + + {datosCalculados.map((fila, index) => ( + + + + + + + + + + + ))} + +
QCFCVCTCFMeCVMeCMeCMg
{fila.q}{fila.cf} + 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} + /> + {fila.ct} + {fila.q > 0 ? fila.cfme.toFixed(2) : '-'} + + {fila.q > 0 ? fila.cvme.toFixed(2) : '-'} + + {fila.q > 0 ? fila.cme.toFixed(2) : '-'} + + {fila.cmg !== null ? fila.cmg : '-'} +
+
+ +
+ + +
+ + {validado && errores.length === 0 && ( +
+
+ + ¡Todos los cálculos son correctos! +
+
+ )} + + {validado && errores.length > 0 && ( +
+

Se encontraron errores:

+
    + {errores.map((error, i) => ( +
  • {error}
  • + ))} +
+
+ )} +
+ + + + +
+
+

Costo Total (CT)

+
+ + + + Cantidad (Q) + CT + + {datosCalculados.map((d, i) => ( + + {d.q} + + ))} + + `${30 + i * 40},${140 - d.ct * escalaCT}`).join(' ')} + /> + + {datosCalculados.map((d, i) => ( + + ))} + +
+
+ +
+

Costo Medio (CMe) vs Costo Marginal (CMg)

+
+ + + + Cantidad (Q) + Costo + + {datosCalculados.filter(d => d.q > 0).map((d, i) => ( + + {d.q} + + ))} + + d.q > 0) + .map((d, i) => `${70 + i * 40},${140 - d.cme * escalaCMe}`) + .join(' ')} + /> + + d.cmg !== null) + .map((d, i) => `${70 + i * 40},${140 - (d.cmg || 0) * escalaCMg}`) + .join(' ')} + /> + + {datosCalculados.filter(d => d.q > 0).map((d, i) => ( + + ))} + + {datosCalculados.filter(d => d.cmg !== null).map((d, i) => ( + + ))} + + + + CMe + + CMg + + +
+
+
+
+ + +

Fórmulas utilizadas:

+
    +
  • CT = CF + CV (Costo Total)
  • +
  • CFMe = CF / Q (Costo Fijo Medio)
  • +
  • CVMe = CV / Q (Costo Variable Medio)
  • +
  • CMe = CT / Q (Costo Medio)
  • +
  • CMg = ΔCT / ΔQ (Costo Marginal)
  • +
+
+
+ ); +} + +export default CalculadoraCostos; diff --git a/frontend/src/components/exercises/modulo4/CortoVsLargoPlazo.tsx b/frontend/src/components/exercises/modulo4/CortoVsLargoPlazo.tsx new file mode 100644 index 0000000..d6303ce --- /dev/null +++ b/frontend/src/components/exercises/modulo4/CortoVsLargoPlazo.tsx @@ -0,0 +1,213 @@ +import { useState } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { CheckCircle, Clock, Calendar, AlertCircle } from 'lucide-react'; +import { QuizExercise, QuizOption } from '../common/QuizExercise'; + +interface CortoVsLargoPlazoProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface FactorItem { + id: string; + nombre: string; + tipo: 'fijo' | 'variable'; + descripcion: string; +} + +const factores: FactorItem[] = [ + { id: '1', nombre: 'Edificio de fábrica', tipo: 'fijo', descripcion: 'No se puede cambiar en el corto plazo' }, + { id: '2', nombre: 'Maquinaria especializada', tipo: 'fijo', descripcion: 'Requiere tiempo para adquirir o vender' }, + { id: '3', nombre: 'Trabajadores temporales', tipo: 'variable', descripcion: 'Se pueden contratar/despedir rápidamente' }, + { id: '4', nombre: 'Materias primas', tipo: 'variable', descripcion: 'Se ajustan según la producción' }, + { id: '5', nombre: 'Contrato de arrendamiento', tipo: 'fijo', descripcion: 'Compromiso a largo plazo' }, + { id: '6', nombre: 'Horas extras', tipo: 'variable', descripcion: 'Se pueden aumentar o disminuir' }, +]; + +export function CortoVsLargoPlazo({ ejercicioId: _ejercicioId, onComplete }: CortoVsLargoPlazoProps) { + const [asignaciones, setAsignaciones] = useState>({}); + const [showResults, setShowResults] = useState(false); + const [puntuacion, setPuntuacion] = useState(0); + + const handleAsignar = (id: string, tipo: 'fijo' | 'variable') => { + if (showResults) return; + setAsignaciones(prev => ({ ...prev, [id]: tipo })); + }; + + const handleVerificar = () => { + let correctas = 0; + factores.forEach(factor => { + if (asignaciones[factor.id] === factor.tipo) { + correctas++; + } + }); + const puntaje = Math.round((correctas / factores.length) * 100); + setPuntuacion(puntaje); + setShowResults(true); + + if (onComplete && puntaje >= 70) { + onComplete(puntaje); + } + }; + + const handleReiniciar = () => { + setAsignaciones({}); + setShowResults(false); + setPuntuacion(0); + }; + + const todasAsignadas = factores.every(f => asignaciones[f.id] !== undefined); + + const quizOptions: QuizOption[] = [ + { id: 'a', text: 'En el corto plazo todos los factores son variables', isCorrect: false }, + { id: 'b', text: 'En el corto plazo al menos un factor es fijo', isCorrect: true }, + { id: 'c', text: 'En el largo plazo no hay factores variables', isCorrect: false }, + { id: 'd', text: 'El tiempo determina si un factor es fijo o variable', isCorrect: false }, + ]; + + return ( +
+ + + +
+
+
+ +

Corto Plazo

+
+

+ Periodo en el que al menos un factor de producción es fijo. + No se puede cambiar la cantidad de todos los factores. +

+
+ +
+
+ +

Largo Plazo

+
+

+ Periodo en el que todos los factores son variables. + La empresa puede ajustar todas sus capacidades productivas. +

+
+
+ +
+

+ Clasifica cada factor como Fijo o Variable en el corto plazo: +

+ +
+ {factores.map((factor) => ( +
+
+
+

{factor.nombre}

+

{factor.descripcion}

+
+
+ + +
+
+ {showResults && ( +

+ {asignaciones[factor.id] === factor.tipo + ? '✓ Correcto' + : `✗ Incorrecto. Es un factor ${factor.tipo}`} +

+ )} +
+ ))} +
+
+ + {!showResults ? ( +
+ +
+ ) : ( +
+
+
+

+ Puntuación: {puntuacion}% +

+

+ {puntuacion >= 70 + ? '¡Buen trabajo! Has comprendido la diferencia entre factores fijos y variables.' + : 'Repasa los conceptos e intenta de nuevo.'} +

+
+ +
+
+ )} +
+ + { + if (result.correct && onComplete && puntuacion >= 70) { + onComplete(Math.max(puntuacion, result.score)); + } + }} + exerciseId="corto-largo-plazo-quiz" + /> + +
+ +
+
+ ); +} + +export default CortoVsLargoPlazo; diff --git a/frontend/src/components/exercises/modulo4/CostoTotalMedioMarginal.tsx b/frontend/src/components/exercises/modulo4/CostoTotalMedioMarginal.tsx new file mode 100644 index 0000000..368cc74 --- /dev/null +++ b/frontend/src/components/exercises/modulo4/CostoTotalMedioMarginal.tsx @@ -0,0 +1,212 @@ +import { useState } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { Calculator, CheckCircle, XCircle } from 'lucide-react'; + +export function CostoTotalMedioMarginal() { + const [respuestas, setRespuestas] = useState<{[key: string]: string}>({ + cme_q2: '', + cme_q4: '', + cmg_q3: '', + cmg_q5: '', + }); + const [mostrarResultados, setMostrarResultados] = useState(false); + + const datos = [ + { q: 0, ct: 100 }, + { q: 1, ct: 150 }, + { q: 2, ct: 180 }, + { q: 3, ct: 220 }, + { q: 4, ct: 300 }, + { q: 5, ct: 450 }, + ]; + + // Cálculos correctos + const respuestasCorrectas: { [key: string]: string } = { + cme_q2: '90', // 180/2 + cme_q4: '75', // 300/4 + cmg_q3: '40', // 220-180 + cmg_q5: '150', // 450-300 + }; + + const handleInputChange = (campo: string, valor: string) => { + setRespuestas(prev => ({ ...prev, [campo]: valor })); + setMostrarResultados(false); + }; + + const validar = () => { + setMostrarResultados(true); + }; + + const todasCompletadas = Object.values(respuestas).every(r => r !== ''); + + const esCorrecto = (campo: string) => { + return respuestas[campo] === respuestasCorrectas[campo]; + }; + + const correctas = Object.keys(respuestasCorrectas).filter(esCorrecto).length; + + return ( +
+ + + +
+ {/* Datos base */} +
+ + + + + + + + + + + {datos.map((fila) => ( + + + + + + + ))} + +
Cantidad (Q)Costo Total (CT)CF (100)CV
{fila.q}${fila.ct}$100${fila.ct - 100}
+
+ + {/* Preguntas */} +
+

+ + Calcula los siguientes valores: +

+ +
+
+

1. CMe cuando Q = 2

+

Fórmula: CT / Q = 180 / 2

+
+ $ + handleInputChange('cme_q2', e.target.value)} + className={`w-20 px-2 py-1 border rounded ${ + mostrarResultados && esCorrecto('cme_q2') + ? 'border-green-500 bg-green-50' + : mostrarResultados && !esCorrecto('cme_q2') + ? 'border-red-500 bg-red-50' + : 'border-gray-300' + }`} + disabled={mostrarResultados} + /> + {mostrarResultados && esCorrecto('cme_q2') && } + {mostrarResultados && !esCorrecto('cme_q2') && } +
+
+ +
+

2. CMe cuando Q = 4

+

Fórmula: CT / Q = 300 / 4

+
+ $ + handleInputChange('cme_q4', e.target.value)} + className={`w-20 px-2 py-1 border rounded ${ + mostrarResultados && esCorrecto('cme_q4') + ? 'border-green-500 bg-green-50' + : mostrarResultados && !esCorrecto('cme_q4') + ? 'border-red-500 bg-red-50' + : 'border-gray-300' + }`} + disabled={mostrarResultados} + /> + {mostrarResultados && esCorrecto('cme_q4') && } + {mostrarResultados && !esCorrecto('cme_q4') && } +
+
+ +
+

3. CMg del 2do al 3er trabajador

+

Fórmula: CT₃ - CT₂ = 220 - 180

+
+ $ + handleInputChange('cmg_q3', e.target.value)} + className={`w-20 px-2 py-1 border rounded ${ + mostrarResultados && esCorrecto('cmg_q3') + ? 'border-green-500 bg-green-50' + : mostrarResultados && !esCorrecto('cmg_q3') + ? 'border-red-500 bg-red-50' + : 'border-gray-300' + }`} + disabled={mostrarResultados} + /> + {mostrarResultados && esCorrecto('cmg_q3') && } + {mostrarResultados && !esCorrecto('cmg_q3') && } +
+
+ +
+

4. CMg del 4to al 5to trabajador

+

Fórmula: CT₅ - CT₄ = 450 - 300

+
+ $ + handleInputChange('cmg_q5', e.target.value)} + className={`w-20 px-2 py-1 border rounded ${ + mostrarResultados && esCorrecto('cmg_q5') + ? 'border-green-500 bg-green-50' + : mostrarResultados && !esCorrecto('cmg_q5') + ? 'border-red-500 bg-red-50' + : 'border-gray-300' + }`} + disabled={mostrarResultados} + /> + {mostrarResultados && esCorrecto('cmg_q5') && } + {mostrarResultados && !esCorrecto('cmg_q5') && } +
+
+
+
+ + + + {mostrarResultados && ( +
+

Resultado: {correctas}/4 correctas

+ {correctas < 4 && ( +

Las respuestas correctas son: CMe(Q=2)=$90, CMe(Q=4)=$75, CMg(2→3)=$40, CMg(4→5)=$150

+ )} +
+ )} +
+
+ + +

Fórmulas Importantes

+
+

Costo Medio (CMe): CMe = CT / Q

+

Costo Marginal (CMg): CMg = ΔCT / ΔQ = CTₙ - CTₙ₋₁

+

Observa cómo el CMg aumenta significativamente del 4to al 5to trabajador ($150 vs $40), + mostrando los rendimientos decrecientes.

+
+
+
+ ); +} + +export default CostoTotalMedioMarginal; diff --git a/frontend/src/components/exercises/modulo4/CostosFijosVsVariables.tsx b/frontend/src/components/exercises/modulo4/CostosFijosVsVariables.tsx new file mode 100644 index 0000000..6189924 --- /dev/null +++ b/frontend/src/components/exercises/modulo4/CostosFijosVsVariables.tsx @@ -0,0 +1,180 @@ +import { useState } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { CheckCircle, XCircle, DollarSign } from 'lucide-react'; + +export function CostosFijosVsVariables() { + const [clasificaciones, setClasificaciones] = useState<{[key: string]: 'fijo' | 'variable' | null}>({ + alquiler: null, + materias: null, + salarios: null, + luz: null, + depreciacion: null, + publicidad: null, + }); + const [mostrarResultados, setMostrarResultados] = useState(false); + + const conceptos = [ + { id: 'alquiler', nombre: 'Alquiler del local', tipo: 'fijo' as const, explicacion: 'El alquiler se paga mensualmente independientemente de cuánto produzcas.' }, + { id: 'materias', nombre: 'Materias primas', tipo: 'variable' as const, explicacion: 'A más producción, más materias primas necesitas.' }, + { id: 'salarios', nombre: 'Salarios de obreros temporales', tipo: 'variable' as const, explicacion: 'Los obreros temporales se contratan según la demanda de producción.' }, + { id: 'luz', nombre: 'Electricidad de máquinas', tipo: 'variable' as const, explicacion: 'Más horas de producción = más consumo eléctrico.' }, + { id: 'depreciacion', nombre: 'Depreciación de maquinaria', tipo: 'fijo' as const, explicacion: 'La depreciación ocurre con el paso del tiempo, no con la cantidad producida.' }, + { id: 'publicidad', nombre: 'Publicidad (contrato anual)', tipo: 'fijo' as const, explicacion: 'El contrato de publicidad es un costo fijo por período.' }, + ]; + + const clasificar = (id: string, tipo: 'fijo' | 'variable') => { + setClasificaciones(prev => ({ ...prev, [id]: tipo })); + setMostrarResultados(false); + }; + + const validar = () => { + setMostrarResultados(true); + }; + + const todasClasificadas = Object.values(clasificaciones).every(c => c !== null); + const correctas = conceptos.filter(c => clasificaciones[c.id] === c.tipo).length; + + return ( +
+ + + +
+ {/* Gráfico comparativo */} +
+ + {/* Título */} + Comportamiento de Costos Fijos y Variables + + {/* Gráfico CF */} + Costo Fijo (CF) + + + Q + $ + {/* Línea horizontal CF */} + + CF = 1000 + + {/* Gráfico CV */} + Costo Variable (CV) + + + Q + $ + {/* Línea creciente CV */} + + + {/* Gráfico CT */} + Costo Total (CT) + + + Q + $ + {/* Línea CT = CF + CV */} + + {/* Línea punteada CF */} + + CF + +
+ + {/* Ejercicio de clasificación */} +
+ {conceptos.map((concepto) => { + const esCorrecto = mostrarResultados && clasificaciones[concepto.id] === concepto.tipo; + const esIncorrecto = mostrarResultados && clasificaciones[concepto.id] !== concepto.tipo && clasificaciones[concepto.id] !== null; + + return ( +
+
+
+ + {concepto.nombre} +
+
+ + +
+
+ + {mostrarResultados && ( +
+ {concepto.tipo === 'fijo' ? 'FIJO' : 'VARIABLE'}: {concepto.explicacion} +
+ )} +
+ ); + })} +
+ + + + {mostrarResultados && ( +
+
+ {correctas === 6 ? ( + + ) : ( + + )} + Resultado: {correctas}/6 correctas +
+
+ )} +
+
+ + +

Definiciones Clave

+
+

Costo Fijo (CF): No depende del nivel de producción. Se incurren aunque Q = 0.

+

Costo Variable (CV): Varía directamente con la cantidad producida. CV = 0 cuando Q = 0.

+

Costo Total (CT): CT = CF + CV

+
+
+
+ ); +} + +export default CostosFijosVsVariables; diff --git a/frontend/src/components/exercises/modulo4/CostosMedios.tsx b/frontend/src/components/exercises/modulo4/CostosMedios.tsx new file mode 100644 index 0000000..08254c0 --- /dev/null +++ b/frontend/src/components/exercises/modulo4/CostosMedios.tsx @@ -0,0 +1,204 @@ +import { useState } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { CheckCircle, XCircle, PieChart } from 'lucide-react'; + +export function CostosMedios() { + const [respuesta, setRespuesta] = useState(null); + const [mostrarResultado, setMostrarResultado] = useState(false); + + const pregunta = { + texto: 'Según la gráfica, ¿cuál es la relación entre CFMe, CVMe y CMe en Q=4?', + opciones: [ + { id: 'a', texto: 'CFMe > CVMe > CMe', correcta: false }, + { id: 'b', texto: 'CMe = CFMe + CVMe', correcta: true }, + { id: 'c', texto: 'CVMe = CFMe + CMe', correcta: false }, + { id: 'd', texto: 'CFMe = CVMe = CMe', correcta: false }, + ], + explicacion: 'Correcto. El Costo Medio (CMe) es la suma del Costo Fijo Medio (CFMe) y el Costo Variable Medio (CVMe): CMe = CFMe + CVMe' + }; + + // Datos para la gráfica + const datos = [ + { q: 1, cfme: 100, cvme: 50, cme: 150 }, + { q: 2, cfme: 50, cvme: 40, cme: 90 }, + { q: 3, cfme: 33.33, cvme: 35, cme: 68.33 }, + { q: 4, cfme: 25, cvme: 32.5, cme: 57.5 }, + { q: 5, cfme: 20, cvme: 35, cme: 55 }, + { q: 6, cfme: 16.67, cvme: 42.5, cme: 59.17 }, + ]; + + const validar = () => { + setMostrarResultado(true); + }; + + const esCorrecta = respuesta === 'b'; + + return ( +
+ + + +
+ {/* Gráfico de barras apiladas */} +
+

Descomposición del Costo Medio

+ + {/* Ejes */} + + + + {/* Etiquetas */} + Cantidad (Q) + Costo Medio ($) + + {/* Barras apiladas */} + {datos.map((d, i) => { + const x = 90 + i * 80; + const alturaCVMe = (d.cvme / 160) * 180; + const alturaCFMe = (d.cfme / 160) * 180; + const alturaTotal = alturaCVMe + alturaCFMe; + + return ( + + {/* CFMe (parte superior) */} + + + {/* CVMe (parte inferior) */} + + + {/* Etiqueta Q */} + {d.q} + + {/* Valor CMe */} + + ${d.cme.toFixed(1)} + + + ); + })} + + {/* Línea de CMe */} + { + const x = 90 + i * 80; + const alturaTotal = ((d.cvme + d.cfme) / 160) * 180; + return `${x},${220 - alturaTotal}`; + }).join(' ')} + /> + + {/* Leyenda */} + + + CFMe + + CVMe + + CMe = CFMe + CVMe + + +
+ + {/* Observaciones */} +
+
+
CFMe (Costo Fijo Medio)
+

CFMe = CF / Q

+

Siempre decreciente. A mayor producción, el costo fijo se "reparte" entre más unidades.

+
+ +
+
CVMe (Costo Variable Medio)
+

CVMe = CV / Q

+

Tiene forma de U. Primero baja por eficiencias, luego sube por rendimientos decrecientes.

+
+
+ + {/* Pregunta */} +
+

{pregunta.texto}

+
+ {pregunta.opciones.map((opcion) => ( + + ))} +
+ + + + {mostrarResultado && ( +
+
+ {esCorrecta ? : } + + {esCorrecta ? '¡Correcto!' : 'Incorrecto'} + +
+

{pregunta.explicacion}

+
+ )} +
+
+
+ + +
+ +

Resumen de Fórmulas

+
+
+

CFMe = CF / Q (siempre decreciente)

+

CVMe = CV / Q (forma de U)

+

CMe = CFMe + CVMe = CT / Q (forma de U)

+

Observa cómo CFMe se vuelve insignificante a altos niveles de producción, + mientras que CVMe domina el costo medio.

+
+
+
+ ); +} + +export default CostosMedios; diff --git a/frontend/src/components/exercises/modulo4/CurvaCostoLargoPlazo.tsx b/frontend/src/components/exercises/modulo4/CurvaCostoLargoPlazo.tsx new file mode 100644 index 0000000..d2d968c --- /dev/null +++ b/frontend/src/components/exercises/modulo4/CurvaCostoLargoPlazo.tsx @@ -0,0 +1,274 @@ +import { useState, useMemo } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { Input } from '../../ui/Input'; +import { CheckCircle, TrendingUp, RotateCcw, Calculator } from 'lucide-react'; + +interface CurvaCostoLargoPlazoProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface DatosEscala { + q: number; + cme: number; +} + +export function CurvaCostoLargoPlazo({ ejercicioId: _ejercicioId, onComplete }: CurvaCostoLargoPlazoProps) { + const datosBase: DatosEscala[] = [ + { q: 1, cme: 120 }, + { q: 2, cme: 85 }, + { q: 3, cme: 70 }, + { q: 4, cme: 65 }, + { q: 5, cme: 62 }, + { q: 6, cme: 60 }, + { q: 7, cme: 61 }, + { q: 8, cme: 64 }, + { q: 9, cme: 69 }, + { q: 10, cme: 75 }, + ]; + + const [respuestas, setRespuestas] = useState<{[key: string]: string}>({ + cmeMinimo: '', + cantidadOptima: '', + ctQ5: '', + }); + const [validado, setValidado] = useState(false); + const [errores, setErrores] = useState([]); + + const datosCalculados = useMemo(() => { + return datosBase.map(d => ({ + ...d, + ct: d.q * d.cme, + })); + }, []); + + const cmeMinimo = useMemo(() => { + return Math.min(...datosBase.map(d => d.cme)); + }, []); + + const cantidadOptima = useMemo(() => { + const minCME = Math.min(...datosBase.map(d => d.cme)); + return datosBase.find(d => d.cme === minCME)?.q || 0; + }, []); + + const handleRespuestaChange = (campo: string, valor: string) => { + setRespuestas(prev => ({ ...prev, [campo]: valor })); + setValidado(false); + }; + + const validarRespuestas = () => { + const nuevosErrores: string[] = []; + + if (parseFloat(respuestas.cmeMinimo) !== cmeMinimo) { + nuevosErrores.push('El CMe mínimo no es correcto. Observa la curva U.'); + } + if (parseFloat(respuestas.cantidadOptima) !== cantidadOptima) { + nuevosErrores.push('La cantidad óptima no es correcta. Es donde el CMe es mínimo.'); + } + if (parseFloat(respuestas.ctQ5) !== 310) { + nuevosErrores.push('El CT a Q=5 es incorrecto. Recuerda: CT = CMe × Q'); + } + + setErrores(nuevosErrores); + setValidado(true); + + if (nuevosErrores.length === 0 && onComplete) { + onComplete(100); + } + }; + + const reiniciar = () => { + setRespuestas({ cmeMinimo: '', cantidadOptima: '', ctQ5: '' }); + setValidado(false); + setErrores([]); + }; + + const maxCMe = Math.max(...datosBase.map(d => d.cme)); + const escalaY = 120 / maxCMe; + + return ( +
+ + + +
+
+ + Concepto +
+

+ A largo plazo todos los factores son variables. La curva CMeLP tiene forma de U + debido a las economías y deseconomías de escala. El punto mínimo representa la + escala eficiente de producción. +

+
+ +
+ + + + Cantidad (Q) + CMe ($) + + {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((q, i) => ( + + + {q} + + ))} + + {[20, 40, 60, 80, 100, 120].map((val, i) => ( + + + {val} + + ))} + + `L ${60 + (i + 1) * 30},${180 - d.cme * escalaY}`).join(' ')}`} + fill="none" + stroke="#7c3aed" + strokeWidth="3" + /> + + {datosBase.map((d, i) => ( + + ))} + + + + Mínimo CMe = $60 + + +
+ +
+ + + + + + + + + + {datosCalculados.map((d, i) => ( + + + + + + ))} + +
QCMe ($)CT ($)
{d.q}{d.cme}{d.ct}
+
+ +
+

+ + Responde las siguientes preguntas: +

+
+
+ + handleRespuestaChange('cmeMinimo', e.target.value)} + className="w-full" + placeholder="Ej: 60" + /> +
+
+ + handleRespuestaChange('cantidadOptima', e.target.value)} + className="w-full" + placeholder="Ej: 6" + /> +
+
+ + handleRespuestaChange('ctQ5', e.target.value)} + className="w-full" + placeholder="CMe × Q" + /> +
+
+
+ +
+ + +
+ + {validado && errores.length === 0 && ( +
+
+ + ¡Correcto! La escala eficiente es Q = 6 con CMe = $60 +
+
+ )} + + {validado && errores.length > 0 && ( +
+

Revisa tus respuestas:

+
    + {errores.map((error, i) => ( +
  • {error}
  • + ))} +
+
+ )} +
+ + +

Fórmulas importantes:

+
    +
  • CT = CMe × Q (Costo Total)
  • +
  • CMe LP = Costo medio a largo plazo (todas las plantas posibles)
  • +
  • Escala eficiente: Cantidad donde CMe es mínimo
  • +
+
+
+ ); +} + +export default CurvaCostoLargoPlazo; diff --git a/frontend/src/components/exercises/modulo4/CurvasCosto.tsx b/frontend/src/components/exercises/modulo4/CurvasCosto.tsx new file mode 100644 index 0000000..215f5b3 --- /dev/null +++ b/frontend/src/components/exercises/modulo4/CurvasCosto.tsx @@ -0,0 +1,218 @@ +import { useState } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { TrendingUp, CheckCircle, DollarSign } from 'lucide-react'; + +export function CurvasCosto() { + const [etapaActiva, setEtapaActiva] = useState(null); + + // Datos para las curvas + const datosCT = [ + { q: 0, ct: 100 }, + { q: 1, ct: 140 }, + { q: 2, ct: 170 }, + { q: 3, ct: 190 }, + { q: 4, ct: 220 }, + { q: 5, ct: 270 }, + { q: 6, ct: 350 }, + { q: 7, ct: 460 }, + { q: 8, ct: 600 }, + ]; + + const datosCMe = [ + { q: 1, cme: 140 }, + { q: 2, cme: 85 }, + { q: 3, cme: 63.33 }, + { q: 4, cme: 55 }, + { q: 5, cme: 54 }, + { q: 6, cme: 58.33 }, + { q: 7, cme: 65.71 }, + { q: 8, cme: 75 }, + ]; + + const datosCMg = [ + { q: 1, cmg: 40 }, + { q: 2, cmg: 30 }, + { q: 3, cmg: 20 }, + { q: 4, cmg: 30 }, + { q: 5, cmg: 50 }, + { q: 6, cmg: 80 }, + { q: 7, cmg: 110 }, + { q: 8, cmg: 140 }, + ]; + + const puntosCorte = [ + { q: 4, desc: 'CMg corta a CMe en su mínimo' }, + { q: 5, desc: 'CMe mínimo (producción eficiente)' }, + ]; + + return ( +
+ + + +
+ {/* Gráfico de Costo Total */} +
+
+

Curva de Costo Total (CT)

+ CT = CF + CV +
+ + {/* Ejes */} + + + + {/* Etiquetas */} + Cantidad (Q) + Costo Total ($) + + {/* CF horizontal */} + + CF = 100 + + {/* Curva CT */} + `${50 + i * 45},${170 - (d.ct / 700) * 150}`).join(' ')} + /> + + {/* Puntos */} + {datosCT.map((d, i) => ( + + ))} + + {/* Etiquetas de Q */} + {datosCT.map((d, i) => ( + + {d.q} + + ))} + +
+ + {/* Gráfico de CMe y CMg */} +
+
+

Curvas de CMe y CMg

+
+ + CMe + + + CMg + +
+
+ + + {/* Ejes */} + + + + {/* Etiquetas */} + Cantidad (Q) + Costo ($) + + {/* Curva CMe */} + `${95 + i * 45},${170 - (d.cme / 160) * 150}`).join(' ')} + /> + + {/* Curva CMg */} + `${95 + i * 45},${170 - (d.cmg / 160) * 150}`).join(' ')} + /> + + {/* Puntos de corte */} + + Mínimo CMe + + {/* Etiquetas de Q */} + {datosCMe.map((d, i) => ( + + {d.q} + + ))} + + {/* Leyenda */} + + + CMe + + CMg + + +
+ + {/* Puntos clave */} +
+ {puntosCorte.map((punto, index) => ( + + ))} +
+ + {etapaActiva && ( +
+
+ + Análisis +
+

+ En Q=5 se alcanza el CMe mínimo ($54), que es el punto donde CMg = CMe. + Este es el nivel de producción más eficiente en términos de costos medios. +

+
+ )} +
+
+ + +
+ +

Interpretación Económica

+
+
    +
  • Costo Total (CT): Siempre crece porque producir más cuesta más
  • +
  • Costo Medio (CMe): Tiene forma de U debido a los rendimientos decrecientes
  • +
  • Costo Marginal (CMg): Corta a CMe en su punto mínimo
  • +
  • Regla: Si CMg {'<'} CMe, el costo medio baja; si CMg {'>'} CMe, el costo medio sube
  • +
+
+
+ ); +} + +export default CurvasCosto; diff --git a/frontend/src/components/exercises/modulo4/DiseconomiasEscala.tsx b/frontend/src/components/exercises/modulo4/DiseconomiasEscala.tsx new file mode 100644 index 0000000..4fd7afb --- /dev/null +++ b/frontend/src/components/exercises/modulo4/DiseconomiasEscala.tsx @@ -0,0 +1,309 @@ +import { useState, useMemo } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { Input } from '../../ui/Input'; +import { CheckCircle, ArrowUp, RotateCcw, AlertTriangle } from 'lucide-react'; + +interface DiseconomiasEscalaProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface RangoEscala { + min: number; + max: number; + tipo: 'economias' | 'constante' | 'diseconomias'; + descripcion: string; +} + +export function DiseconomiasEscala({ ejercicioId: _ejercicioId, onComplete }: DiseconomiasEscalaProps) { + const rangos: RangoEscala[] = [ + { min: 0, max: 500, tipo: 'economias', descripcion: 'Economías de escala' }, + { min: 500, max: 1000, tipo: 'constante', descripcion: 'Rendimientos constantes a escala' }, + { min: 1000, max: 2000, tipo: 'diseconomias', descripcion: 'Diseconomías de escala' }, + ]; + + const calcularCMe = (q: number): number => { + if (q <= 500) { + return 50 - (q / 500) * 20; + } else if (q <= 1000) { + return 30; + } else { + return 30 + ((q - 1000) / 1000) * 25; + } + }; + + const [cantidad, setCantidad] = useState(600); + const [respuestas, setRespuestas] = useState({ + cme: '', + ct: '', + rango: '', + }); + const [validado, setValidado] = useState(false); + const [errores, setErrores] = useState([]); + + const cmeActual = useMemo(() => calcularCMe(cantidad), [cantidad]); + const ctActual = useMemo(() => cmeActual * cantidad, [cmeActual, cantidad]); + const rangoActual = useMemo(() => { + return rangos.find(r => cantidad >= r.min && cantidad < r.max) || rangos[2]; + }, [cantidad]); + + const datosGrafico = useMemo(() => { + const puntos = []; + for (let q = 100; q <= 2000; q += 100) { + puntos.push({ q, cme: calcularCMe(q) }); + } + return puntos; + }, []); + + const handleRespuestaChange = (campo: string, valor: string) => { + setRespuestas(prev => ({ ...prev, [campo]: valor })); + setValidado(false); + }; + + const validarRespuestas = () => { + const nuevosErrores: string[] = []; + + if (Math.abs(parseFloat(respuestas.cme) - cmeActual) > 0.5) { + nuevosErrores.push(`El CMe no es correcto. Debería ser aproximadamente $${cmeActual.toFixed(2)}`); + } + if (Math.abs(parseFloat(respuestas.ct) - ctActual) > 50) { + nuevosErrores.push(`El CT no es correcto. Recuerda: CT = CMe × Q`); + } + if (respuestas.rango.toLowerCase() !== rangoActual.tipo.toLowerCase()) { + nuevosErrores.push(`El rango no es correcto. Estás en la zona de ${rangoActual.descripcion}`); + } + + setErrores(nuevosErrores); + setValidado(true); + + if (nuevosErrores.length === 0 && onComplete) { + onComplete(100); + } + }; + + const reiniciar = () => { + setCantidad(600); + setRespuestas({ cme: '', ct: '', rango: '' }); + setValidado(false); + setErrores([]); + }; + + const maxCMe = Math.max(...datosGrafico.map(d => d.cme)); + const escalaY = 100 / maxCMe; + + return ( +
+ + + +
+
+ + Concepto +
+

+ Las deseconomías de escala ocurren cuando la empresa crece tanto que los costos de + coordinación, supervisión y comunición aumentan. El CMe comienza a subir después + de alcanzar el punto óptimo de escala. +

+
+ +
+ +
+ { + setCantidad(parseInt(e.target.value)); + setValidado(false); + }} + className="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" + /> + + {cantidad} + +
+
+ +
+ + + + Cantidad (Q) + CMe ($) + + {[500, 1000, 1500, 2000].map((q) => ( + + + {q} + + ))} + + {[10, 20, 30, 40, 50].map((val) => ( + + + {val} + + ))} + + + + + + Economías + Constantes + Diseconomías + + `${40 + (d.q / 2000) * 300},${160 - d.cme * escalaY * 2}`).join(' L ')}`} + fill="none" + stroke="#7c3aed" + strokeWidth="3" + /> + + + + + +
+ +
+
+

Costo Medio

+

${cmeActual.toFixed(2)}

+
+
+

Costo Total

+

${ctActual.toLocaleString()}

+
+
+

Zona

+

+ {rangoActual.descripcion} +

+
+
+ +
+

+ + Responde para Q = {cantidad}: +

+
+
+ + handleRespuestaChange('cme', e.target.value)} + className="w-full" + placeholder="Ej: 30.00" + /> +
+
+ + handleRespuestaChange('ct', e.target.value)} + className="w-full" + placeholder="CMe × Q" + /> +
+
+ + handleRespuestaChange('rango', e.target.value)} + className="w-full" + placeholder="economias" + /> +
+
+
+ +
+ + +
+ + {validado && errores.length === 0 && ( +
+
+ + ¡Correcto! Observa cómo el CMe cambia según la escala +
+
+ )} + + {validado && errores.length > 0 && ( +
+

Revisa tus respuestas:

+
    + {errores.map((error, i) => ( +
  • {error}
  • + ))} +
+
+ )} +
+ + +

Causas de las Diseconomías de Escala:

+
    +
  • Problemas de coordinación: Más difícil coordinar muchos departamentos
  • +
  • Burocracia: Decisiones lentas y procesos administrativos complejos
  • +
  • Problemas de comunicación: Información se distorsiona en cadenas largas
  • +
  • Desmotivación: Trabajadores se sienten insignificantes en empresas grandes
  • +
+
+
+ ); +} + +export default DiseconomiasEscala; diff --git a/frontend/src/components/exercises/modulo4/EconomiasEscala.tsx b/frontend/src/components/exercises/modulo4/EconomiasEscala.tsx new file mode 100644 index 0000000..577140c --- /dev/null +++ b/frontend/src/components/exercises/modulo4/EconomiasEscala.tsx @@ -0,0 +1,217 @@ +import { useState, useMemo } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { CheckCircle, ArrowDown, RotateCcw, Factory } from 'lucide-react'; + +interface EconomiasEscalaProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface EscalaData { + planta: string; + capacidad: number; + cf: number; + cvUnitario: number; +} + +export function EconomiasEscala({ ejercicioId: _ejercicioId, onComplete }: EconomiasEscalaProps) { + const datosPlantas: EscalaData[] = [ + { planta: 'Pequeña', capacidad: 100, cf: 1000, cvUnitario: 10 }, + { planta: 'Mediana', capacidad: 500, cf: 3000, cvUnitario: 8 }, + { planta: 'Grande', capacidad: 1000, cf: 5000, cvUnitario: 6 }, + { planta: 'Muy Grande', capacidad: 2000, cf: 8000, cvUnitario: 5 }, + ]; + + const [produccion, setProduccion] = useState(500); + const [seleccion, setSeleccion] = useState(null); + const [validado, setValidado] = useState(false); + + const calculos = useMemo(() => { + return datosPlantas.map(p => { + const q = Math.min(produccion, p.capacidad); + const cv = q * p.cvUnitario; + const ct = p.cf + cv; + const cme = q > 0 ? ct / q : 0; + const puedeProducir = produccion <= p.capacidad; + return { ...p, q, cv, ct, cme, puedeProducir }; + }); + }, [produccion]); + + const plantaOptima = useMemo(() => { + const plantasFactibles = calculos.filter(c => c.puedeProducir); + if (plantasFactibles.length === 0) return null; + return plantasFactibles.reduce((min, curr) => curr.cme < min.cme ? curr : min); + }, [calculos]); + + const handleValidar = () => { + setValidado(true); + if (seleccion === plantaOptima?.planta && onComplete) { + onComplete(100); + } + }; + + const reiniciar = () => { + setProduccion(500); + setSeleccion(null); + setValidado(false); + }; + + return ( +
+ + + +
+
+ + Concepto +
+

+ Las economías de escala ocurren cuando el costo medio disminuye a medida que + aumenta la producción. Esto puede deberse a: especialización, tecnología eficiente, + descuentos por volumen en compras, y distribución de costos fijos. +

+
+ +
+ +
+ { + setProduccion(parseInt(e.target.value)); + setValidado(false); + setSeleccion(null); + }} + className="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" + /> + + {produccion} + +
+
+ +
+ {calculos.map((calc) => ( +
calc.puedeProducir && setSeleccion(calc.planta)} + className={` + p-4 rounded-lg border-2 cursor-pointer transition-all + ${!calc.puedeProducir ? 'bg-gray-100 border-gray-200 opacity-50 cursor-not-allowed' : ''} + ${seleccion === calc.planta ? 'border-primary bg-primary/5' : 'border-gray-200 hover:border-gray-300'} + ${validado && calc.planta === plantaOptima?.planta ? 'border-green-500 bg-green-50' : ''} + `} + > +
+
+ + {calc.planta} +
+ {!calc.puedeProducir && ( + + Insuficiente + + )} + {validado && calc.planta === plantaOptima?.planta && ( + + Óptima + + )} +
+
+
+ Capacidad máxima: + {calc.capacidad} unidades +
+
+ Costo Fijo: + ${calc.cf.toLocaleString()} +
+
+ CV unitario: + ${calc.cvUnitario} +
+ {calc.puedeProducir && ( + <> +
+ Costo Total: + ${calc.ct.toLocaleString()} +
+
+ Costo Medio: + c.puedeProducir).map(c => c.cme)) ? 'text-green-600' : 'text-gray-900'}`}> + ${calc.cme.toFixed(2)} + +
+ + )} +
+
+ ))} +
+ +
+

+ Selecciona la planta óptima para producir {produccion} unidades: +

+

+ Tip: Elige la planta con el menor costo medio (CMe) que pueda producir la cantidad deseada. +

+
+ +
+ + +
+ + {validado && seleccion === plantaOptima?.planta && ( +
+
+ + + ¡Correcto! La planta {plantaOptima.planta} tiene el menor CMe (${plantaOptima.cme.toFixed(2)}) + +
+
+ )} + + {validado && seleccion !== plantaOptima?.planta && ( +
+

+ La planta {seleccion} no es la óptima. La planta {plantaOptima?.planta} tiene un CMe menor (${plantaOptima?.cme.toFixed(2)} vs ${calculos.find(c => c.planta === seleccion)?.cme.toFixed(2)}). +

+
+ )} +
+ + +

Causas de las Economías de Escala:

+
    +
  • Especialización del trabajo: Tareas más específicas = mayor eficiencia
  • +
  • Tecnología especializada: Maquinaria más eficiente a gran escala
  • +
  • Descuentos por volumen: Comprar insumos al por mayor es más barato
  • +
  • División de costos fijos: Se reparten entre más unidades
  • +
+
+
+ ); +} + +export default EconomiasEscala; diff --git a/frontend/src/components/exercises/modulo4/EtapasProduccion.tsx b/frontend/src/components/exercises/modulo4/EtapasProduccion.tsx new file mode 100644 index 0000000..2ab219d --- /dev/null +++ b/frontend/src/components/exercises/modulo4/EtapasProduccion.tsx @@ -0,0 +1,231 @@ +import { useState } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { CheckCircle, XCircle, Layers } from 'lucide-react'; + +interface Etapa { + id: string; + nombre: string; + descripcion: string; + color: string; + rango: string; +} + +const ETAPAS: Etapa[] = [ + { + id: 'i', + nombre: 'Etapa I', + descripcion: 'PMg creciente - Rendimientos crecientes a escala', + color: '#22c55e', + rango: '0 a 3 trabajadores' + }, + { + id: 'ii', + nombre: 'Etapa II', + descripcion: 'PMg decreciente pero positivo - Rendimientos decrecientes', + color: '#3b82f6', + rango: '3 a 6 trabajadores' + }, + { + id: 'iii', + nombre: 'Etapa III', + descripcion: 'PMg negativo - Producción total disminuye', + color: '#ef4444', + rango: 'Más de 6 trabajadores' + } +]; + +export function EtapasProduccion() { + const [respuestas, setRespuestas] = useState<{[key: number]: string}>({}); + const [mostrarResultados, setMostrarResultados] = useState(false); + + const preguntas = [ + { + id: 1, + texto: '¿En qué etapa un productor racional NUNCA producirá?', + respuestaCorrecta: 'iii', + explicacion: 'En la Etapa III el producto marginal es negativo, lo que significa que agregar más trabajadores disminuye la producción total. Un productor racional evitará esta etapa.' + }, + { + id: 2, + texto: '¿En qué etapa los rendimientos marginales son crecientes?', + respuestaCorrecta: 'i', + explicacion: 'En la Etapa I, cada trabajador adicional aporta más que el anterior debido a la especialización y división del trabajo.' + }, + { + id: 3, + texto: '¿En qué etapa se encuentra la mayoría de la producción eficiente?', + respuestaCorrecta: 'ii', + explicacion: 'La Etapa II es donde opera un productor racional. Aunque los rendimientos marginales decrecen, siguen siendo positivos hasta cierto punto.' + } + ]; + + const seleccionarRespuesta = (preguntaId: number, etapaId: string) => { + setRespuestas(prev => ({ ...prev, [preguntaId]: etapaId })); + setMostrarResultados(false); + }; + + const validarTodas = () => { + setMostrarResultados(true); + }; + + const todasRespondidas = preguntas.every(p => respuestas[p.id]); + + return ( +
+ + + +
+ {/* Gráfico de etapas */} +
+

Producto Total y sus Etapas

+ + {/* Ejes */} + + + + {/* Etiquetas eje X */} + L1 + L2 + L3 + Cantidad de Trabajo (L) + + {/* Etiquetas eje Y */} + 0 + Q1 + Q2 + Q3 + Producto Total (PT) + + {/* Líneas verticales separadoras de etapas */} + + + + {/* Zonas de etapas */} + + + + + {/* Etiquetas de etapas */} + ETAPA I + PMg creciente + + ETAPA II + PMg decreciente + + ETAPA III + PMg negativo + + {/* Curva de producto total */} + + + {/* Punto de inflexión */} + + Punto de Inflexión + + {/* Punto máximo */} + + PT Máximo + + {/* Flecha mostrando declive */} + + + + + + + +
+ + {/* Leyenda de etapas */} +
+ {ETAPAS.map(etapa => ( +
+
{etapa.nombre}
+

{etapa.descripcion}

+

{etapa.rango}

+
+ ))} +
+ + {/* Preguntas */} +
+ {preguntas.map(pregunta => ( +
+

{pregunta.id}. {pregunta.texto}

+
+ {ETAPAS.map(etapa => { + const esCorrecta = mostrarResultados && respuestas[pregunta.id] === pregunta.respuestaCorrecta; + const esIncorrecta = mostrarResultados && respuestas[pregunta.id] === etapa.id && respuestas[pregunta.id] !== pregunta.respuestaCorrecta; + const esLaCorrecta = mostrarResultados && etapa.id === pregunta.respuestaCorrecta; + + return ( + + ); + })} +
+ {mostrarResultados && ( +
+ {pregunta.explicacion} +
+ )} +
+ ))} +
+ + + + {mostrarResultados && ( +
+
+ + Conclusión +
+

+ Un productor racional opera principalmente en la Etapa II, + donde aunque los rendimientos marginales decrecen, siguen siendo positivos. + La Etapa I es muy corta y la Etapa III es irracional desde el punto de vista económico. +

+
+ )} +
+
+
+ ); +} + +export default EtapasProduccion; diff --git a/frontend/src/components/exercises/modulo4/FuncionProduccion.tsx b/frontend/src/components/exercises/modulo4/FuncionProduccion.tsx new file mode 100644 index 0000000..168ac4b --- /dev/null +++ b/frontend/src/components/exercises/modulo4/FuncionProduccion.tsx @@ -0,0 +1,184 @@ +import { useState, useMemo } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { Input } from '../../ui/Input'; +import { CheckCircle, Factory, Calculator } from 'lucide-react'; + +interface FuncionProduccionProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +export function FuncionProduccion({ ejercicioId: _ejercicioId, onComplete }: FuncionProduccionProps) { + const [capital, setCapital] = useState(4); + const [trabajo, setTrabajo] = useState(5); + + const tablaProduccion = [ + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 8, 12, 17, 20, 22, 23, 23], + [0, 12, 20, 28, 35, 40, 43, 44], + [0, 17, 28, 40, 50, 58, 63, 65], + [0, 20, 35, 50, 65, 75, 83, 87], + [0, 22, 40, 58, 75, 88, 98, 104], + [0, 23, 43, 63, 83, 98, 110, 118], + [0, 23, 44, 65, 87, 104, 118, 128], + ]; + + const output = useMemo(() => { + if (trabajo >= 0 && trabajo <= 7 && capital >= 0 && capital <= 7) { + return tablaProduccion[capital][trabajo]; + } + return 0; + }, [capital, trabajo]); + + const handleCompletar = () => { + if (onComplete) { + onComplete(100); + } + }; + + return ( +
+ + + +
+
+ + Concepto +
+

+ La función de producción muestra la relación técnica entre los factores productivos + (Capital K y Trabajo L) y la cantidad máxima de output (Q) que puede producirse. +

+
+ +
+
+ +
+ setCapital(parseInt(e.target.value))} + className="flex-1" + /> + + {capital} + +
+
+ +
+ +
+ setTrabajo(parseInt(e.target.value))} + className="flex-1" + /> + + {trabajo} + +
+
+
+ +
+
+ +
+

Output Total (Q)

+

+ Q = f({capital}, {trabajo}) = {output} +

+

+ unidades producidas +

+
+
+
+ +
+ + + + + {[0, 1, 2, 3, 4, 5, 6, 7].map(l => ( + + ))} + + + + {tablaProduccion.map((fila, k) => ( + + + {fila.map((q, l) => ( + + ))} + + ))} + +
K \ L{l}
{k} + {q} +
+
+ +
+

Nota: La celda resaltada en verde muestra el output actual. + Las filas representan niveles de Capital (K) y las columnas niveles de Trabajo (L).

+
+
+ + +

Ejercicio de Comprensión

+
+

+ Si una empresa tiene 3 unidades de capital y contrata 4 trabajadores, + ¿cuál es el nivel de producción máximo alcanzable según la tabla? +

+
+ + + Respuesta correcta: {tablaProduccion[3][4]} unidades + +
+
+
+ +
+ +
+
+ ); +} + +export default FuncionProduccion; diff --git a/frontend/src/components/exercises/modulo4/IngresoCompetenciaPerfecta.tsx b/frontend/src/components/exercises/modulo4/IngresoCompetenciaPerfecta.tsx new file mode 100644 index 0000000..c194c7b --- /dev/null +++ b/frontend/src/components/exercises/modulo4/IngresoCompetenciaPerfecta.tsx @@ -0,0 +1,278 @@ +import { useState, useMemo } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { Input } from '../../ui/Input'; +import { CheckCircle, Scale, RotateCcw, TrendingUp } from 'lucide-react'; + +interface IngresoCompetenciaPerfectaProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +export function IngresoCompetenciaPerfecta({ ejercicioId: _ejercicioId, onComplete }: IngresoCompetenciaPerfectaProps) { + const PRECIO_MERCADO = 50; + + const [cantidad, setCantidad] = useState(100); + const [respuestas, setRespuestas] = useState({ + it: '', + img: '', + relacion: '', + }); + const [validado, setValidado] = useState(false); + const [errores, setErrores] = useState([]); + + const ingresoTotal = useMemo(() => PRECIO_MERCADO * cantidad, [cantidad]); + const ingresoMarginal = PRECIO_MERCADO; + const ingresoPromedio = PRECIO_MERCADO; + + const datosTabla = useMemo(() => { + const datos = []; + for (let q = 0; q <= 200; q += 25) { + datos.push({ + q, + p: PRECIO_MERCADO, + it: PRECIO_MERCADO * q, + img: PRECIO_MERCADO, + ip: PRECIO_MERCADO, + }); + } + return datos; + }, []); + + const handleRespuestaChange = (campo: string, valor: string) => { + setRespuestas(prev => ({ ...prev, [campo]: valor })); + setValidado(false); + }; + + const validarRespuestas = () => { + const nuevosErrores: string[] = []; + + if (parseFloat(respuestas.it) !== ingresoTotal) { + nuevosErrores.push(`IT incorrecto. IT = P × Q = ${PRECIO_MERCADO} × ${cantidad}`); + } + if (parseFloat(respuestas.img) !== ingresoMarginal) { + nuevosErrores.push(`IMg incorrecto. En competencia perfecta, IMg = P`); + } + if (!['igual', 'igual a', 'es igual', 'son iguales'].some(r => respuestas.relacion.toLowerCase().includes(r))) { + nuevosErrores.push('En competencia perfecta, P = IMg = IPMe'); + } + + setErrores(nuevosErrores); + setValidado(true); + + if (nuevosErrores.length === 0 && onComplete) { + onComplete(100); + } + }; + + const reiniciar = () => { + setCantidad(100); + setRespuestas({ it: '', img: '', relacion: '' }); + setValidado(false); + setErrores([]); + }; + + return ( +
+ + + +
+
+ + Características +
+

+ En competencia perfecta, la empresa es tomadora de precios. El precio de mercado + es constante e independiente de la cantidad que produzca la empresa. Por eso: + P = IMg = IPMe. La curva de demanda es horizontal (perfectamente elástica). +

+
+ +
+
+

Precio de Mercado (P)

+

${PRECIO_MERCADO}

+

Constante

+
+
+

Ingreso Marginal (IMg)

+

${ingresoMarginal}

+

=P

+
+
+

Ingreso Promedio (IPMe)

+

${ingresoPromedio}

+

=P

+
+
+ +
+ +
+ { + setCantidad(parseInt(e.target.value)); + setValidado(false); + }} + className="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" + /> + + {cantidad} + +
+
+ +
+
+

Ingreso Total con Q = {cantidad}

+

+ IT = ${ingresoTotal.toLocaleString()} +

+

+ {PRECIO_MERCADO} × {cantidad} = ${ingresoTotal.toLocaleString()} +

+
+
+ +
+ + + + + + + + + + + + {datosTabla.filter((_, i) => i % 2 === 0 || i === datosTabla.length - 1).map((d, i) => ( + + + + + + + + ))} + +
QP ($)IT ($)IMg ($)IPMe ($)
{d.q}${d.p}${d.it.toLocaleString()}${d.img}${d.ip}
+
+ +
+

+ + Responde para Q = {cantidad}: +

+
+
+ + handleRespuestaChange('it', e.target.value)} + className="w-full" + placeholder={String(PRECIO_MERCADO * cantidad)} + /> +
+
+ + handleRespuestaChange('img', e.target.value)} + className="w-full" + placeholder="?" + /> +
+
+ + handleRespuestaChange('relacion', e.target.value)} + className="w-full" + placeholder="Son iguales / Diferentes" + /> +
+
+
+ +
+ + +
+ + {validado && errores.length === 0 && ( +
+
+ + ¡Correcto! En competencia perfecta: P = IMg = IPMe = ${PRECIO_MERCADO} +
+
+ )} + + {validado && errores.length > 0 && ( +
+

Revisa tus respuestas:

+
    + {errores.map((error, i) => ( +
  • {error}
  • + ))} +
+
+ )} +
+ + +

Resumen - Competencia Perfecta:

+
+
+

Fórmulas:

+
    +
  • • IT = P × Q
  • +
  • • IMg = P (constante)
  • +
  • • IPMe = P (constante)
  • +
+
+
+

Características:

+
    +
  • • La empresa es tomadora de precios
  • +
  • • Demanda horizontal (perfectamente elástica)
  • +
  • • P = IMg = IPMe
  • +
+
+
+
+
+ ); +} + +export default IngresoCompetenciaPerfecta; diff --git a/frontend/src/components/exercises/modulo4/IngresoMarginal.tsx b/frontend/src/components/exercises/modulo4/IngresoMarginal.tsx new file mode 100644 index 0000000..dd39b4d --- /dev/null +++ b/frontend/src/components/exercises/modulo4/IngresoMarginal.tsx @@ -0,0 +1,234 @@ +import { useState, useMemo } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { Input } from '../../ui/Input'; +import { CheckCircle, Activity, RotateCcw, Calculator } from 'lucide-react'; + +interface IngresoMarginalProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface FilaIngreso { + q: number; + p: number; +} + +export function IngresoMarginal({ ejercicioId: _ejercicioId, onComplete }: IngresoMarginalProps) { + const datosBase: FilaIngreso[] = [ + { q: 0, p: 100 }, + { q: 1, p: 90 }, + { q: 2, p: 80 }, + { q: 3, p: 70 }, + { q: 4, p: 60 }, + { q: 5, p: 50 }, + { q: 6, p: 40 }, + { q: 7, p: 30 }, + { q: 8, p: 20 }, + ]; + + const [respuestas, setRespuestas] = useState<{[key: string]: string}>({}); + const [validado, setValidado] = useState(false); + const [errores, setErrores] = useState([]); + + const datosCalculados = useMemo(() => { + return datosBase.map((fila, index) => { + const it = fila.p * fila.q; + const itAnterior = index > 0 ? datosBase[index - 1].p * datosBase[index - 1].q : 0; + const img = index > 0 ? it - itAnterior : null; + return { ...fila, it, img }; + }); + }, []); + + const handleRespuestaChange = (q: number, valor: string) => { + setRespuestas(prev => ({ ...prev, [`img_${q}`]: valor })); + setValidado(false); + }; + + const validarRespuestas = () => { + const nuevosErrores: string[] = []; + + datosCalculados.forEach((fila) => { + if (fila.img !== null) { + const respuesta = parseFloat(respuestas[`img_${fila.q}`] || '0'); + if (Math.abs(respuesta - fila.img) > 1) { + nuevosErrores.push(`Q=${fila.q}: El IMg debería ser $${fila.img}`); + } + } + }); + + setErrores(nuevosErrores); + setValidado(true); + + if (nuevosErrores.length === 0 && onComplete) { + onComplete(100); + } + }; + + const reiniciar = () => { + setRespuestas({}); + setValidado(false); + setErrores([]); + }; + + const maxIT = Math.max(...datosCalculados.map(d => d.it)); + const maxIMG = Math.max(...datosCalculados.filter(d => d.img !== null).map(d => Math.abs(d.img || 0))); + const escalaIT = maxIT > 0 ? 120 / maxIT : 1; + const escalaIMG = maxIMG > 0 ? 60 / maxIMG : 1; + + return ( +
+ + + +
+
+ + Concepto +
+

+ El Ingreso Marginal es el cambio en el ingreso total resultante de vender + una unidad adicional. Se calcula como: IMg = ΔIT / ΔQ. + Cuando el precio debe bajar para vender más, el IMg {'<'} IT. +

+
+ +
+ + + + Cantidad (Q) + $ (×100) + + {datosBase.map((d, i) => ( + + + {d.q} + + ))} + + `${60 + i * 35},${160 - d.it * escalaIT}`).join(' ')} + /> + + d.img !== null) + .map((d, i) => `${95 + i * 35},${160 - (d.img || 0) * escalaIMG - 50}`) + .join(' ')} + /> + + + + IT + + IMg + + +
+ +
+ + + + + + + + + + + {datosCalculados.map((fila) => ( + + + + + + + ))} + +
QP ($)IT ($)IMg ($)
{fila.q}{fila.p}{fila.it} + {fila.img !== null ? ( + handleRespuestaChange(fila.q, e.target.value)} + className="w-24" + placeholder="IMg" + /> + ) : ( + - + )} +
+
+ +
+

+ + Cálculo del Ingreso Marginal: +

+

+ IMg = IT(Q) - IT(Q-1) +

+

+ Ejemplo: Cuando Q aumenta de 2 a 3 unidades, el IT pasa de $160 a $210. + El IMg de la 3ra unidad es $210 - $160 = $50. +

+
+ +
+ + +
+ + {validado && errores.length === 0 && ( +
+
+ + ¡Todos los cálculos son correctos! +
+
+ )} + + {validado && errores.length > 0 && ( +
+

Errores encontrados:

+
    + {errores.map((error, i) => ( +
  • {error}
  • + ))} +
+
+ )} +
+ + +

Importancia del Ingreso Marginal:

+
    +
  • Regla de maximización: La empresa maximiza beneficios cuando IMg = CMg
  • +
  • IMg {'<'} P: Cuando debe bajar el precio para vender más, el IMg es menor que el precio
  • +
  • IMg positivo: Mientras IMg {'>'} 0, el ingreso total aumenta
  • +
  • IMg negativo: Si IMg {'<'} 0, vender más reduce el ingreso total
  • +
+
+
+ ); +} + +export default IngresoMarginal; diff --git a/frontend/src/components/exercises/modulo4/IngresoTotal.tsx b/frontend/src/components/exercises/modulo4/IngresoTotal.tsx new file mode 100644 index 0000000..bceeb50 --- /dev/null +++ b/frontend/src/components/exercises/modulo4/IngresoTotal.tsx @@ -0,0 +1,273 @@ +import { useState, useMemo } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { Input } from '../../ui/Input'; +import { CheckCircle, DollarSign, RotateCcw, TrendingUp } from 'lucide-react'; + +interface IngresoTotalProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Producto { + nombre: string; + precio: number; +} + +export function IngresoTotal({ ejercicioId: _ejercicioId, onComplete }: IngresoTotalProps) { + const productos: Producto[] = [ + { nombre: 'Libros', precio: 25 }, + { nombre: 'Electrónicos', precio: 150 }, + { nombre: 'Ropa', precio: 45 }, + ]; + + const [productoSeleccionado, setProductoSeleccionado] = useState(0); + const [cantidad, setCantidad] = useState(100); + const [respuestaIT, setRespuestaIT] = useState(''); + const [validado, setValidado] = useState(false); + const [error, setError] = useState(''); + + const precio = productos[productoSeleccionado].precio; + const ingresoTotal = useMemo(() => precio * cantidad, [precio, cantidad]); + + const datosTabla = useMemo(() => { + const datos = []; + for (let q = 0; q <= 200; q += 20) { + datos.push({ q, it: precio * q }); + } + return datos; + }, [precio]); + + const handleValidar = () => { + const respuesta = parseFloat(respuestaIT); + if (Math.abs(respuesta - ingresoTotal) < 1) { + setError(''); + setValidado(true); + if (onComplete) { + onComplete(100); + } + } else { + setError(`Incorrecto. IT = P × Q = $${precio} × ${cantidad} = $${ingresoTotal.toLocaleString()}`); + setValidado(true); + } + }; + + const reiniciar = () => { + setCantidad(100); + setRespuestaIT(''); + setValidado(false); + setError(''); + }; + + const maxIT = Math.max(...datosTabla.map(d => d.it)); + const escalaY = maxIT > 0 ? 120 / maxIT : 1; + + return ( +
+ + + +
+
+ + Fórmula Fundamental +
+

+ El Ingreso Total representa el dinero total que recibe una empresa por la venta + de sus productos. Se calcula multiplicando el precio de venta por la cantidad + vendida: IT = P × Q +

+
+ +
+
+ + +
+ +
+ + { + setCantidad(parseInt(e.target.value)); + setValidado(false); + setRespuestaIT(''); + }} + className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" + /> +
+
+ +
+
+

Precio (P)

+

${precio}

+
+
+

Cantidad (Q)

+

{cantidad}

+
+
+

Ingreso Total (IT)

+

${ingresoTotal.toLocaleString()}

+
+
+ +
+ + + + Cantidad (Q) + IT ($) + + {[0, 50, 100, 150, 200].map((q) => ( + + + {q} + + ))} + + + + + + + (${ingresoTotal.toLocaleString()}) + + +
+ +
+ + + + + + + + + + {datosTabla.filter((_, i) => i % 2 === 0).map((d, i) => ( + + + + + + ))} + +
QP ($)IT ($)
{d.q}${precio}${d.it.toLocaleString()}
+
+ +
+

+ + Calcula el Ingreso Total: +

+
+
+ + { + setRespuestaIT(e.target.value); + setValidado(false); + }} + className="w-full" + placeholder="Ingresa el IT" + /> +
+
+
+ +
+ + +
+ + {validado && !error && ( +
+
+ + ¡Correcto! IT = ${ingresoTotal.toLocaleString()} +
+
+ )} + + {validado && error && ( +
+

{error}

+
+ )} +
+ + +

Fórmula del Ingreso Total:

+
+

IT = P × Q

+

+ Donde: IT = Ingreso Total, P = Precio, Q = Cantidad vendida +

+
+
+
+ ); +} + +export default IngresoTotal; diff --git a/frontend/src/components/exercises/modulo4/LeyRendimientosDecrecientes.tsx b/frontend/src/components/exercises/modulo4/LeyRendimientosDecrecientes.tsx new file mode 100644 index 0000000..df06e9b --- /dev/null +++ b/frontend/src/components/exercises/modulo4/LeyRendimientosDecrecientes.tsx @@ -0,0 +1,173 @@ +import { useState } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { CheckCircle, XCircle, TrendingDown } from 'lucide-react'; + +export function LeyRendimientosDecrecientes() { + const [respuesta, setRespuesta] = useState(null); + const [mostrarExplicacion, setMostrarExplicacion] = useState(false); + + const validarRespuesta = () => { + setMostrarExplicacion(true); + }; + + return ( +
+ + + +
+
+

+ Escenario: Un granjero tiene 100 hectáreas de tierra fijas. + Puede contratar más trabajadores, pero la cantidad de tierra no cambia. +

+
+ +
+

Producción de Trigo (toneladas)

+ + {/* Ejes */} + + + + {/* Etiquetas eje X - Trabajadores */} + 1 + 2 + 3 + 4 + 5 + Número de Trabajadores + + {/* Etiquetas eje Y - Producción */} + 0 + 50 + 100 + 150 + 200 + Producción (Tn) + + {/* Líneas de cuadrícula */} + + + + + {/* Curva de producción total */} + + + {/* Puntos de datos */} + + + + + + + {/* Etiquetas de puntos */} + 50Tn + 100Tn + 135Tn + 155Tn + 160Tn + + {/* Flecha indicando decrecimiento */} + + + + + + + +
+ +
+

+ ¿Qué observas en el punto del 5to trabajador? +

+
+ + + +
+
+ + + + {mostrarExplicacion && ( +
+
+ {respuesta === 'b' ? ( + + ) : ( + + )} + + {respuesta === 'b' ? '¡Correcto!' : 'Incorrecto'} + +
+

+ La respuesta correcta es b). Con el 5to trabajador, la producción + solo aumenta de 155Tn a 160Tn (5Tn adicionales), mientras que el 2do trabajador + aportó 50Tn adicionales. Esto demuestra la Ley de Rendimientos Decrecientes: + a medida que aumentamos una variable productiva (trabajo) manteniendo fijas las demás + (tierra), el producto marginal disminuye. +

+
+ )} +
+
+ + +

+ + Fórmula del Producto Marginal +

+

+ PMg = ΔProducción Total / ΔTrabajadores +

+

+ PMg (1→2) = (100-50)/(2-1) = 50 Tn
+ PMg (4→5) = (160-155)/(5-4) = 5 Tn +

+
+
+ ); +} + +export default LeyRendimientosDecrecientes; diff --git a/frontend/src/components/exercises/modulo4/ProductoMarginal.tsx b/frontend/src/components/exercises/modulo4/ProductoMarginal.tsx new file mode 100644 index 0000000..702a7c1 --- /dev/null +++ b/frontend/src/components/exercises/modulo4/ProductoMarginal.tsx @@ -0,0 +1,233 @@ +import { useState, useMemo } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { Input } from '../../ui/Input'; +import { CheckCircle, Calculator, TrendingDown, TrendingUp } from 'lucide-react'; + +interface ProductoMarginalProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface FilaDatos { + L: number; + PT: number; + PMg: number | null; +} + +export function ProductoMarginal({ ejercicioId: _ejercicioId, onComplete }: ProductoMarginalProps) { + const [respuestas, setRespuestas] = useState>({}); + const [verificado, setVerificado] = useState(false); + + const datosBase = [ + { L: 0, PT: 0 }, + { L: 1, PT: 10 }, + { L: 2, PT: 25 }, + { L: 3, PT: 45 }, + { L: 4, PT: 60 }, + { L: 5, PT: 70 }, + { L: 6, PT: 75 }, + { L: 7, PT: 75 }, + { L: 8, PT: 70 }, + ]; + + const datosCompletos: FilaDatos[] = useMemo(() => { + return datosBase.map((fila, index) => ({ + L: fila.L, + PT: fila.PT, + PMg: index > 0 ? fila.PT - datosBase[index - 1].PT : null, + })); + }, []); + + const handleInputChange = (L: number, value: string) => { + setRespuestas(prev => ({ ...prev, [L]: value })); + }; + + const handleVerificar = () => { + setVerificado(true); + + let correctas = 0; + let total = 0; + + datosCompletos.forEach(fila => { + if (fila.PMg !== null) { + total++; + if (parseInt(respuestas[fila.L]) === fila.PMg) { + correctas++; + } + } + }); + + if (correctas === total && onComplete) { + onComplete(100); + } + }; + + const handleReiniciar = () => { + setRespuestas({}); + setVerificado(false); + }; + + const todasRespondidas = datosCompletos + .filter(f => f.PMg !== null) + .every(f => respuestas[f.L] !== undefined && respuestas[f.L] !== ''); + + return ( +
+ + + +
+
+ + Fórmula +
+
+

+ PMg = ΔPT / ΔL = (PT₁ - PT₀) / (L₁ - L₀) +

+
+

+ El Producto Marginal mide la producción adicional generada + al emplear una unidad más de trabajo, manteniendo constante el capital. +

+
+ +
+ + + + + + + + + + + {datosCompletos.map((fila) => ( + + + + + + + ))} + +
Trabajo (L)Producto Total (PT)Producto Marginal (PMg)Estado
{fila.L}{fila.PT} + {fila.PMg === null ? ( + + ) : ( +
+ handleInputChange(fila.L, e.target.value)} + disabled={verificado} + className={`w-24 ${ + verificado + ? parseInt(respuestas[fila.L]) === fila.PMg + ? 'border-success bg-success/5' + : 'border-error bg-error/5' + : '' + }`} + placeholder="?" + /> + {verificado && ( + + {parseInt(respuestas[fila.L]) === fila.PMg ? '✓' : `✗ ${fila.PMg}`} + + )} +
+ )} +
+ {fila.PMg !== null && ( + <> + {fila.PMg > (datosCompletos[fila.L - 1]?.PMg || 0) ? ( + + + Creciente + + ) : fila.PMg > 0 ? ( + + Decreciente + + ) : ( + + + Negativo + + )} + + )} +
+
+ +
+

Ley de los Rendimientos Marginales Decrecientes

+

+ A medida que se agregan más unidades de un factor variable (trabajo) a un factor + fijo (capital), el producto marginal eventualmente disminuirá. +

+
+
+

Fase 1: PMg creciente

+

Especialización y eficiencia

+
+
+

Fase 2: PMg decreciente

+

Ley de rendimientos decrecientes

+
+
+

Fase 3: PMg negativo

+

Hacinamiento/sobrepoblación

+
+
+
+
+ +
+
+ {!verificado ? ( + Completa todos los campos para verificar + ) : ( + + Correctos: {datosCompletos.filter(f => + f.PMg !== null && parseInt(respuestas[f.L]) === f.PMg + ).length} / {datosCompletos.filter(f => f.PMg !== null).length} + + )} +
+
+ {!verificado ? ( + + ) : ( + <> + + {datosCompletos.filter(f => + f.PMg !== null && parseInt(respuestas[f.L]) === f.PMg + ).length === datosCompletos.filter(f => f.PMg !== null).length && ( + + )} + + )} +
+
+
+ ); +} + +export default ProductoMarginal; diff --git a/frontend/src/components/exercises/modulo4/ProductoMedio.tsx b/frontend/src/components/exercises/modulo4/ProductoMedio.tsx new file mode 100644 index 0000000..973f0c1 --- /dev/null +++ b/frontend/src/components/exercises/modulo4/ProductoMedio.tsx @@ -0,0 +1,247 @@ +import { useState, useMemo } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { Input } from '../../ui/Input'; +import { CheckCircle, Divide, ArrowRight } from 'lucide-react'; + +interface ProductoMedioProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface FilaDatos { + L: number; + PT: number; + PMe: number | null; +} + +export function ProductoMedio({ ejercicioId: _ejercicioId, onComplete }: ProductoMedioProps) { + const [respuestas, setRespuestas] = useState>({}); + const [verificado, setVerificado] = useState(false); + + const datosBase = [ + { L: 1, PT: 10 }, + { L: 2, PT: 24 }, + { L: 3, PT: 39 }, + { L: 4, PT: 52 }, + { L: 5, PT: 60 }, + { L: 6, PT: 66 }, + { L: 7, PT: 70 }, + { L: 8, PT: 72 }, + ]; + + const datosCompletos: FilaDatos[] = useMemo(() => { + return datosBase.map(fila => ({ + L: fila.L, + PT: fila.PT, + PMe: fila.L > 0 ? parseFloat((fila.PT / fila.L).toFixed(2)) : null, + })); + }, []); + + const maxPMe = Math.max(...datosCompletos.map(d => d.PMe || 0)); + const maxPMeL = datosCompletos.find(d => d.PMe === maxPMe)?.L; + + const handleInputChange = (L: number, value: string) => { + setRespuestas(prev => ({ ...prev, [L]: value })); + }; + + const handleVerificar = () => { + setVerificado(true); + + let correctas = 0; + datosCompletos.forEach(fila => { + const respuesta = parseFloat(respuestas[fila.L]); + if (Math.abs(respuesta - (fila.PMe || 0)) < 0.1) { + correctas++; + } + }); + + if (correctas === datosCompletos.length && onComplete) { + onComplete(100); + } + }; + + const handleReiniciar = () => { + setRespuestas({}); + setVerificado(false); + }; + + const todasRespondidas = datosCompletos.every(f => + respuestas[f.L] !== undefined && respuestas[f.L] !== '' + ); + + return ( +
+ + + +
+
+ + Fórmula +
+
+

+ PMe = PT / L = Q / L +

+
+

+ El Producto Medio representa la producción por trabajador. + Mide la eficiencia promedio del factor trabajo. +

+
+ +
+ + + + + + + + + + + {datosCompletos.map((fila) => ( + + + + + + + ))} + +
Trabajo (L)Producto Total (PT)Producto Medio (PMe)Estado
{fila.L}{fila.PT} +
+ handleInputChange(fila.L, e.target.value)} + disabled={verificado} + className={`w-24 ${ + verificado + ? Math.abs(parseFloat(respuestas[fila.L]) - (fila.PMe || 0)) < 0.1 + ? 'border-success bg-success/5' + : 'border-error bg-error/5' + : '' + }`} + placeholder="?" + /> + {verificado && ( + + {Math.abs(parseFloat(respuestas[fila.L]) - (fila.PMe || 0)) < 0.1 + ? '✓' + : `✗ ${fila.PMe}`} + + )} +
+
+ {fila.PMe === maxPMe && ( + + Máximo + + )} +
+
+ +
+

Relación entre PMg y PMe

+
+
+ +

Cuando PMg {'>'} PMe, el producto medio está aumentando

+
+
+ +

Cuando PMg {'<'} PMe, el producto medio está disminuyendo

+
+
+ +

Cuando PMg = PMe, el producto medio está en su máximo

+
+
+
+
+ + + + +
+

+ Pregunta: ¿En qué nivel de trabajo (L) se alcanza el Producto Medio máximo + y cuál es su valor? +

+ +
+
+
+

Nivel de trabajo (L):

+

{maxPMeL} trabajadores

+
+
+

Producto Medio máximo:

+

{maxPMe} unidades/trabajador

+
+
+
+ +

+ Interpretación: Cada trabajador produce en promedio {maxPMe} unidades + cuando hay {maxPMeL} trabajadores. Este es el punto de máxima eficiencia por trabajador. +

+
+
+ +
+
+ {!verificado ? ( + Completa todos los cálculos con 2 decimales + ) : ( + + Correctos: {datosCompletos.filter(f => + Math.abs(parseFloat(respuestas[f.L]) - (f.PMe || 0)) < 0.1 + ).length} / {datosCompletos.length} + + )} +
+
+ {!verificado ? ( + + ) : ( + <> + + {datosCompletos.filter(f => + Math.abs(parseFloat(respuestas[f.L]) - (f.PMe || 0)) < 0.1 + ).length === datosCompletos.length && ( + + )} + + )} +
+
+
+ ); +} + +export default ProductoMedio; diff --git a/frontend/src/components/exercises/modulo4/ProductoTotal.tsx b/frontend/src/components/exercises/modulo4/ProductoTotal.tsx new file mode 100644 index 0000000..8a317c5 --- /dev/null +++ b/frontend/src/components/exercises/modulo4/ProductoTotal.tsx @@ -0,0 +1,223 @@ +import { useState } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { Input } from '../../ui/Input'; +import { CheckCircle, TrendingUp, AlertCircle } from 'lucide-react'; + +interface ProductoTotalProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface FilaProduccion { + L: number; + Q: number; +} + +const datosProduccion: FilaProduccion[] = [ + { L: 0, Q: 0 }, + { L: 1, Q: 8 }, + { L: 2, Q: 20 }, + { L: 3, Q: 36 }, + { L: 4, Q: 52 }, + { L: 5, Q: 64 }, + { L: 6, Q: 72 }, + { L: 7, Q: 76 }, + { L: 8, Q: 76 }, + { L: 9, Q: 72 }, +]; + +export function ProductoTotal({ ejercicioId: _ejercicioId, onComplete }: ProductoTotalProps) { + const [respuestaMax, setRespuestaMax] = useState(''); + const [respuestaL, setRespuestaL] = useState(''); + const [verificado, setVerificado] = useState(false); + const [correcto, setCorrecto] = useState({ max: false, l: false }); + + const maxQ = Math.max(...datosProduccion.map(d => d.Q)); + const maxL = datosProduccion.find(d => d.Q === maxQ)?.L; + + const handleVerificar = () => { + const esCorrectoMax = parseInt(respuestaMax) === maxQ; + const esCorrectoL = parseInt(respuestaL) === maxL; + + setCorrecto({ max: esCorrectoMax, l: esCorrectoL }); + setVerificado(true); + + if (esCorrectoMax && esCorrectoL && onComplete) { + onComplete(100); + } + }; + + const handleReiniciar = () => { + setRespuestaMax(''); + setRespuestaL(''); + setVerificado(false); + setCorrecto({ max: false, l: false }); + }; + + return ( +
+ + + +
+
+ + Definición +
+

+ El Producto Total (PT o Q) es la cantidad total de output producida + utilizando una cierta cantidad de un factor variable (generalmente trabajo L), + manteniendo fijos los demás factores. +

+

+ Fórmula: PT = Q = f(L) cuando K es constante +

+
+ +
+ + + + + + + + + + {datosProduccion.map((fila, index) => ( + + + + + + ))} + +
Trabajo (L)Producto Total (Q)Estado
{fila.L}{fila.Q} + {fila.Q === maxQ && ( + + Máximo + + )} + {fila.L > 0 && fila.Q < datosProduccion[index - 1].Q && ( + + Rendimientos negativos + + )} +
+
+ +
+
+ + Análisis +
+
    +
  • La producción aumenta hasta cierto punto (L = 7 u 8)
  • +
  • Beyond that point, los rendimientos son decrecientes
  • +
  • Con L = 9, el producto total disminuye (rendimientos negativos)
  • +
+
+
+ + + + +
+
+
+ + setRespuestaMax(e.target.value)} + placeholder="Valor de Q máximo" + disabled={verificado} + className={verificado + ? correcto.max + ? 'border-success bg-success/5' + : 'border-error bg-error/5' + : '' + } + /> + {verificado && ( +

+ {correcto.max ? '✓ Correcto' : `✗ Incorrecto. La respuesta es ${maxQ}`} +

+ )} +
+ +
+ + setRespuestaL(e.target.value)} + placeholder="Valor de L" + disabled={verificado} + className={verificado + ? correcto.l + ? 'border-success bg-success/5' + : 'border-error bg-error/5' + : '' + } + /> + {verificado && ( +

+ {correcto.l ? '✓ Correcto' : `✗ Incorrecto. La respuesta es ${maxL}`} +

+ )} +
+
+ +
+ {!verificado ? ( + + ) : ( + <> + + {(correcto.max && correcto.l) && ( + + )} + + )} +
+
+
+
+ ); +} + +export default ProductoTotal; diff --git a/frontend/src/components/exercises/modulo4/ProductorRacional.tsx b/frontend/src/components/exercises/modulo4/ProductorRacional.tsx new file mode 100644 index 0000000..a083a70 --- /dev/null +++ b/frontend/src/components/exercises/modulo4/ProductorRacional.tsx @@ -0,0 +1,199 @@ +import { useState } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { CheckCircle, XCircle, Brain } from 'lucide-react'; + +export function ProductorRacional() { + const [respuestas, setRespuestas] = useState<{[key: string]: boolean | null}>({ + afirmacion1: null, + afirmacion2: null, + afirmacion3: null, + afirmacion4: null, + }); + const [mostrarResultados, setMostrarResultados] = useState(false); + + const afirmaciones = [ + { + id: 'afirmacion1', + texto: 'Un productor racional siempre busca minimizar costos para un nivel dado de producción.', + esCorrecta: true, + explicacion: 'Correcto. La racionalidad económica implica optimizar recursos, lo que incluye minimizar costos para producir una cantidad determinada.' + }, + { + id: 'afirmacion2', + texto: 'Producir en la Etapa III es racional si los precios son muy altos.', + esCorrecta: false, + explicacion: 'Incorrecto. En la Etapa III el producto marginal es negativo, por lo que producir más disminuye el output total. Nunca es racional operar aquí.' + }, + { + id: 'afirmacion3', + texto: 'El productor racional equilibra el ingreso marginal con el costo marginal.', + esCorrecta: true, + explicacion: 'Correcto. La condición de maximización de beneficios es IMg = CMg. Producir donde el ingreso adicional iguala al costo adicional.' + }, + { + id: 'afirmacion4', + texto: 'Producir en la Etapa I es óptimo porque los rendimientos son crecientes.', + esCorrecta: false, + explicacion: 'Incorrecto. Aunque los rendimientos son crecientes en la Etapa I, el productor puede aumentar la producción y los beneficios moviéndose a la Etapa II.' + } + ]; + + const seleccionarRespuesta = (id: string, valor: boolean) => { + setRespuestas(prev => ({ ...prev, [id]: valor })); + setMostrarResultados(false); + }; + + const validar = () => { + setMostrarResultados(true); + }; + + const todasRespondidas = Object.values(respuestas).every(r => r !== null); + const correctas = afirmaciones.filter(a => respuestas[a.id] === a.esCorrecta).length; + + return ( +
+ + + +
+ {/* Diagrama de decisión */} +
+

Zona de Decisión del Productor

+ + {/* Ejes */} + + + + {/* Etiquetas */} + Cantidad de Trabajo + PT + + {/* Curva PT */} + + + {/* Zona I */} + + ZONA I + No óptima + + {/* Zona II - ZONA RACIONAL */} + + ZONA RACIONAL + ETAPA II + Donde opera el + productor eficiente + + {/* Zona III */} + + ZONA III + Irracional + + {/* Límites */} + + + +
+ + {/* Afirmaciones */} +
+ {afirmaciones.map((afirmacion, index) => ( +
+
+ + {index + 1} + +

{afirmacion.texto}

+
+ +
+ + +
+ + {mostrarResultados && ( +
+ {afirmacion.explicacion} +
+ )} +
+ ))} +
+ + + + {mostrarResultados && ( +
+
+ + Resultado: {correctas}/4 correctas +
+ {correctas === 4 && ( +

+ ¡Excelente! Comprendes perfectamente qué hace racional a un productor. +

+ )} + {correctas < 4 && ( +

+ Revisa las explicaciones para entender mejor el comportamiento del productor racional. +

+ )} +
+ )} +
+
+
+ ); +} + +export default ProductorRacional; diff --git a/frontend/src/components/exercises/modulo4/PuntoCierreEquilibrio.tsx b/frontend/src/components/exercises/modulo4/PuntoCierreEquilibrio.tsx new file mode 100644 index 0000000..81b5613 --- /dev/null +++ b/frontend/src/components/exercises/modulo4/PuntoCierreEquilibrio.tsx @@ -0,0 +1,310 @@ +import { useState, useMemo } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { Input } from '../../ui/Input'; +import { CheckCircle, Power, RotateCcw, AlertTriangle, Calculator } from 'lucide-react'; + +interface PuntoCierreEquilibrioProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface Escenario { + nombre: string; + precio: number; + q: number; + cf: number; + cv: number; + descripcion: string; +} + +export function PuntoCierreEquilibrio({ ejercicioId: _ejercicioId, onComplete }: PuntoCierreEquilibrioProps) { + const escenarios: Escenario[] = [ + { nombre: 'Beneficios', precio: 60, q: 100, cf: 2000, cv: 3000, descripcion: 'P > CMe: La empresa gana dinero' }, + { nombre: 'Equilibrio', precio: 50, q: 100, cf: 2000, cv: 3000, descripcion: 'P = CMe: Beneficio = 0 (normal)' }, + { nombre: 'Pérdida pero opera', precio: 35, q: 100, cf: 2000, cv: 3000, descripcion: 'CVMe < P < CMe: Cubre CV, parte de CF' }, + { nombre: 'Punto de cierre', precio: 30, q: 100, cf: 2000, cv: 3000, descripcion: 'P = CVMe: Debe cerrar a largo plazo' }, + { nombre: 'Cierre inmediato', precio: 25, q: 100, cf: 2000, cv: 3000, descripcion: 'P < CVMe: Debe cerrar inmediatamente' }, + ]; + + const [escenarioSeleccionado, setEscenarioSeleccionado] = useState(0); + const [respuestas, setRespuestas] = useState({ + ingresoTotal: '', + costoTotal: '', + costoVariable: '', + beneficio: '', + decision: '', + }); + const [validado, setValidado] = useState(false); + const [errores, setErrores] = useState([]); + + const escenario = escenarios[escenarioSeleccionado]; + + const calculos = useMemo(() => { + const it = escenario.precio * escenario.q; + const ct = escenario.cf + escenario.cv; + const cvme = escenario.cv / escenario.q; + const cme = ct / escenario.q; + const beneficio = it - ct; + return { it, ct, cvme, cme, beneficio }; + }, [escenario]); + + const decisionCorrecta = useMemo(() => { + if (calculos.beneficio >= 0) return 'producir'; + if (escenario.precio > calculos.cvme) return 'producir_perdida'; + return 'cerrar'; + }, [calculos, escenario.precio]); + + const handleRespuestaChange = (campo: string, valor: string) => { + setRespuestas(prev => ({ ...prev, [campo]: valor })); + setValidado(false); + }; + + const validarRespuestas = () => { + const nuevosErrores: string[] = []; + + if (parseFloat(respuestas.ingresoTotal) !== calculos.it) { + nuevosErrores.push(`IT incorrecto. IT = P × Q = ${escenario.precio} × ${escenario.q}`); + } + if (parseFloat(respuestas.costoTotal) !== calculos.ct) { + nuevosErrores.push(`CT incorrecto. CT = CF + CV = ${escenario.cf} + ${escenario.cv}`); + } + if (parseFloat(respuestas.costoVariable) !== escenario.cv) { + nuevosErrores.push(`CV incorrecto. El CV es ${escenario.cv}`); + } + if (parseFloat(respuestas.beneficio) !== calculos.beneficio) { + nuevosErrores.push(`Beneficio incorrecto. Beneficio = IT - CT`); + } + + const respDecision = respuestas.decision.toLowerCase().trim(); + const esCorrecto = + (decisionCorrecta === 'producir' && (respDecision.includes('producir') || respDecision.includes('continuar'))) || + (decisionCorrecta === 'producir_perdida' && (respDecision.includes('producir') || respDecision.includes('operar'))) || + (decisionCorrecta === 'cerrar' && (respDecision.includes('cerrar') || respDecision.includes('parar'))); + + if (!esCorrecto) { + if (decisionCorrecta === 'producir') { + nuevosErrores.push('La empresa debe seguir produciendo porque obtiene beneficios.'); + } else if (decisionCorrecta === 'producir_perdida') { + nuevosErrores.push('La empresa debe seguir produciendo en el corto plazo porque P > CVMe (cubre los costos variables).'); + } else { + nuevosErrores.push('La empresa debe cerrar porque P < CVMe (no cubre los costos variables).'); + } + } + + setErrores(nuevosErrores); + setValidado(true); + + if (nuevosErrores.length === 0 && onComplete) { + onComplete(100); + } + }; + + const reiniciar = () => { + setRespuestas({ ingresoTotal: '', costoTotal: '', costoVariable: '', beneficio: '', decision: '' }); + setValidado(false); + setErrores([]); + }; + + return ( +
+ + + +
+
+ + Reglas de Decisión +
+

+ Punto de cierre: Si P {'<'} CVMe, la empresa debe cerrar inmediatamente + porque ni siquiera cubre los costos variables. Equilibrio: Si P = CMe, + la empresa obtiene beneficio cero (beneficio normal). +

+
+ +
+ + +
+ +
+
+ + + {escenario.nombre} + +
+

{escenario.descripcion}

+
+ +
+
+

Precio (P)

+

${escenario.precio}

+
+
+

Cantidad (Q)

+

{escenario.q}

+
+
+

Costo Fijo (CF)

+

${escenario.cf}

+
+
+

Costo Variable (CV)

+

${escenario.cv}

+
+
= 0 ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200' + }`}> +

CMe ($)

+

{calculos.cme.toFixed(2)}

+
+
+ +
+

+ + Completa los cálculos: +

+
+
+ + handleRespuestaChange('ingresoTotal', e.target.value)} + className="w-full" + placeholder="P × Q" + /> +
+
+ + handleRespuestaChange('costoTotal', e.target.value)} + className="w-full" + placeholder="CF + CV" + /> +
+
+ + handleRespuestaChange('costoVariable', e.target.value)} + className="w-full" + placeholder="CV" + /> +
+
+ + handleRespuestaChange('beneficio', e.target.value)} + className="w-full" + placeholder="IT - CT" + /> +
+
+ + handleRespuestaChange('decision', e.target.value)} + className="w-full" + placeholder="Producir / Cerrar" + /> +
+
+
+ +
+ + +
+ + {validado && errores.length === 0 && ( +
+
+ + ¡Correcto! Respuestas validadas +
+
+ )} + + {validado && errores.length > 0 && ( +
+

Revisa tus respuestas:

+
    + {errores.map((error, i) => ( +
  • {error}
  • + ))} +
+
+ )} +
+ +
+ +

Punto de Equilibrio:

+
    +
  • Definición: Cuando P = CMe (Beneficio = 0)
  • +
  • Significado: La empresa cubre todos sus costos
  • +
  • Beneficio: Es el beneficio "normal" del empresario
  • +
  • Decisión: Continuar operando
  • +
+
+ + +

Punto de Cierre:

+
    +
  • Definición: Cuando P = CVMe mínimo
  • +
  • Si P {'>'} CVMe: Cubre CV, ayuda con CF → Seguir produciendo
  • +
  • Si P = CVMe: Indiferente entre producir o cerrar
  • +
  • Si P {'<'} CVMe: Ni siquiera cubre CV → Cerrar inmediatamente
  • +
+
+
+
+ ); +} + +export default PuntoCierreEquilibrio; diff --git a/frontend/src/components/exercises/modulo4/ReglaImgCmg.tsx b/frontend/src/components/exercises/modulo4/ReglaImgCmg.tsx new file mode 100644 index 0000000..82a5d06 --- /dev/null +++ b/frontend/src/components/exercises/modulo4/ReglaImgCmg.tsx @@ -0,0 +1,309 @@ +import { useState, useMemo } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { Input } from '../../ui/Input'; +import { CheckCircle, Target, RotateCcw, Calculator } from 'lucide-react'; + +interface ReglaImgCmgProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +interface DatoMercado { + q: number; + cmg: number; + img: number; + ct: number; + it: number; +} + +export function ReglaImgCmg({ ejercicioId: _ejercicioId, onComplete }: ReglaImgCmgProps) { + const datosMercado: DatoMercado[] = [ + { q: 0, cmg: 0, img: 100, ct: 50, it: 0 }, + { q: 1, cmg: 30, img: 90, ct: 80, it: 90 }, + { q: 2, cmg: 40, img: 80, ct: 120, it: 160 }, + { q: 3, cmg: 50, img: 70, ct: 170, it: 210 }, + { q: 4, cmg: 60, img: 60, ct: 230, it: 240 }, + { q: 5, cmg: 70, img: 50, ct: 300, it: 250 }, + { q: 6, cmg: 80, img: 40, ct: 380, it: 240 }, + { q: 7, cmg: 90, img: 30, ct: 470, it: 210 }, + { q: 8, cmg: 100, img: 20, ct: 570, it: 160 }, + ]; + + const [respuestas, setRespuestas] = useState({ + qOptima: '', + beneficio: '', + condicion: '', + }); + const [validado, setValidado] = useState(false); + const [errores, setErrores] = useState([]); + + const qOptima = useMemo(() => { + const datoOptimo = datosMercado + .filter(d => d.cmg <= d.img && d.q > 0) + .pop(); + return datoOptimo?.q || 0; + }, []); + + const beneficioMaximo = useMemo(() => { + const datoOptimo = datosMercado.find(d => d.q === qOptima); + return datoOptimo ? datoOptimo.it - datoOptimo.ct : 0; + }, [qOptima]); + + const handleRespuestaChange = (campo: string, valor: string) => { + setRespuestas(prev => ({ ...prev, [campo]: valor })); + setValidado(false); + }; + + const validarRespuestas = () => { + const nuevosErrores: string[] = []; + + if (parseInt(respuestas.qOptima) !== qOptima) { + nuevosErrores.push(`La cantidad óptima no es correcta. Busca donde IMg = CMg (o IMg >= CMg más cercano)`); + } + if (parseFloat(respuestas.beneficio) !== beneficioMaximo) { + nuevosErrores.push(`El beneficio máximo es incorrecto. Beneficio = IT - CT`); + } + if (!respuestas.condicion.toLowerCase().includes('img = cmg') && + !respuestas.condicion.toLowerCase().includes('img igual a cmg') && + !respuestas.condicion.toLowerCase().includes('ingreso marginal igual a costo marginal')) { + nuevosErrores.push('La condición de maximización es IMg = CMg'); + } + + setErrores(nuevosErrores); + setValidado(true); + + if (nuevosErrores.length === 0 && onComplete) { + onComplete(100); + } + }; + + const reiniciar = () => { + setRespuestas({ qOptima: '', beneficio: '', condicion: '' }); + setValidado(false); + setErrores([]); + }; + + const maxValor = Math.max(...datosMercado.map(d => Math.max(d.cmg, d.img))); + const escalaY = 100 / maxValor; + + return ( +
+ + + +
+
+ + Regla Fundamental +
+

+ Una empresa maximiza su beneficio cuando produce la cantidad donde el Ingreso Marginal (IMg) + es igual al Costo Marginal (CMg). Si IMg {'>'} CMg, debe producir más. Si IMg {'<'} CMg, + debe producir menos. +

+
+ +
+ + + + Cantidad (Q) + Costo/Ingreso ($) + + {datosMercado.map((d, i) => ( + + + {d.q} + + ))} + + d.q > 0) + .map((d, i) => `${85 + i * 35},${160 - d.cmg * escalaY}`) + .join(' ')} + /> + + d.q > 0) + .map((d, i) => `${85 + i * 35},${160 - d.img * escalaY}`) + .join(' ')} + /> + + + + + + CMg + + IMg + + + + Q* = {qOptima} + + +
+ +
+ + + + + + + + + + + + + + {datosMercado.map((d) => { + const beneficio = d.it - d.ct; + const esOptimo = d.q === qOptima; + const debeExpandir = d.img > d.cmg && d.q > 0; + const debeReducir = d.img < d.cmg; + + return ( + + + + + + + + + + ); + })} + +
QCMg ($)IMg ($)CT ($)IT ($)Beneficio ($)Decisión
{d.q}{d.cmg || '-'}{d.img}{d.ct}{d.it}= 0 ? 'text-green-600' : 'text-red-600'}`}> + {d.q === 0 ? '-' : beneficio} + + {d.q === 0 ? '-' : ( + + {esOptimo ? 'ÓPTIMO ✓' : debeExpandir ? 'Expandir ↑' : 'Reducir ↓'} + + )} +
+
+ +
+

+ + Responde: +

+
+
+ + handleRespuestaChange('qOptima', e.target.value)} + className="w-full" + placeholder="Busca IMg = CMg" + /> +
+
+ + handleRespuestaChange('beneficio', e.target.value)} + className="w-full" + placeholder="IT - CT" + /> +
+
+ + handleRespuestaChange('condicion', e.target.value)} + className="w-full" + placeholder="IMg = CMg" + /> +
+
+
+ +
+ + +
+ + {validado && errores.length === 0 && ( +
+
+ + + ¡Correcto! La empresa maximiza beneficios con Q* = {qOptima}, obteniendo un beneficio de ${beneficioMaximo} + +
+
+ )} + + {validado && errores.length > 0 && ( +
+

Revisa tus respuestas:

+
    + {errores.map((error, i) => ( +
  • {error}
  • + ))} +
+
+ )} +
+ + +

Reglas de Maximización:

+
    +
  • IMg {'>'} CMg: La empresa debe aumentar la producción
  • +
  • IMg {'<'} CMg: La empresa debe reducir la producción
  • +
  • IMg = CMg: La empresa está maximizando beneficios
  • +
  • Beneficio = IT - CT (o también: (P - CMe) × Q)
  • +
+
+
+ ); +} + +export default ReglaImgCmg; diff --git a/frontend/src/components/exercises/modulo4/RelacionCMgCMe.tsx b/frontend/src/components/exercises/modulo4/RelacionCMgCMe.tsx new file mode 100644 index 0000000..00efc9e --- /dev/null +++ b/frontend/src/components/exercises/modulo4/RelacionCMgCMe.tsx @@ -0,0 +1,235 @@ +import { useState } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { CheckCircle, XCircle, GitCompare } from 'lucide-react'; + +export function RelacionCMgCMe() { + const [respuestas, setRespuestas] = useState<{[key: string]: string}>({ + pregunta1: '', + pregunta2: '', + pregunta3: '', + }); + const [mostrarResultados, setMostrarResultados] = useState(false); + + const preguntas = [ + { + id: 'pregunta1', + texto: 'Cuando CMg < CMe, el costo medio:', + opciones: [ + { id: 'a', texto: 'Aumenta', correcta: false }, + { id: 'b', texto: 'Disminuye', correcta: true }, + { id: 'c', texto: 'Se mantiene constante', correcta: false }, + ], + explicacion: 'Si el costo marginal es menor que el costo medio, "arrastra" el promedio hacia abajo, haciendo que CMe disminuya.' + }, + { + id: 'pregunta2', + texto: 'El CMe alcanza su mínimo cuando:', + opciones: [ + { id: 'a', texto: 'CMg = 0', correcta: false }, + { id: 'b', texto: 'CMg es máximo', correcta: false }, + { id: 'c', texto: 'CMg = CMe', correcta: true }, + ], + explicacion: 'El CMg corta a CMe en su punto mínimo. Cuando se igualan, CMe deja de caer y empieza a subir.' + }, + { + id: 'pregunta3', + texto: 'Cuando CMg > CMe, el costo medio:', + opciones: [ + { id: 'a', texto: 'Aumenta', correcta: true }, + { id: 'b', texto: 'Disminuye', correcta: false }, + { id: 'c', texto: 'Es cero', correcta: false }, + ], + explicacion: 'Si el costo marginal es mayor que el costo medio, "empuja" el promedio hacia arriba, haciendo que CMe aumente.' + } + ]; + + const seleccionarRespuesta = (preguntaId: string, opcionId: string) => { + setRespuestas(prev => ({ ...prev, [preguntaId]: opcionId })); + setMostrarResultados(false); + }; + + const validar = () => { + setMostrarResultados(true); + }; + + const todasRespondidas = Object.values(respuestas).every(r => r !== ''); + + const esCorrecta = (preguntaId: string) => { + const pregunta = preguntas.find(p => p.id === preguntaId); + return pregunta?.opciones.find(o => o.id === respuestas[preguntaId])?.correcta || false; + }; + + const correctas = preguntas.filter(p => esCorrecta(p.id)).length; + + return ( +
+ + + +
+ {/* Gráfico animado */} +
+

CMg "jalona" al CMe

+ + + {/* Ejes */} + + + + {/* Etiquetas */} + Cantidad (Q) + Costo ($) + + {/* Curva CMe */} + + + {/* Curva CMg */} + + + {/* Punto de corte */} + + Mínimo CMe + + CMg = CMe + + {/* Zona 1: CMg < CMe */} + + ZONA 1 + CMg {'<'} CMe + CMe ↓ decrece + + {/* Zona 2: CMg > CMe */} + + ZONA 2 + CMg {'>'} CMe + CMe ↑ aumenta + + {/* Flechas indicadoras */} + + + + + + + + + + + + + {/* Leyenda */} + + + CMe + + CMg + + +
+ + {/* Analogía */} +
+

Analogía del Promedio y la Nueva Nota

+

+ Imagina tu promedio académico (CMe) y tu próxima nota (CMg): +

+
    +
  • Si tu nueva nota (CMg) es menor que tu promedio (CMe) → tu promedio baja
  • +
  • Si tu nueva nota (CMg) es igual a tu promedio (CMe) → tu promedio se mantiene (mínimo)
  • +
  • Si tu nueva nota (CMg) es mayor que tu promedio (CMe) → tu promedio sube
  • +
+
+ + {/* Preguntas */} +
+ {preguntas.map((pregunta) => { + const preguntaCorrecta = esCorrecta(pregunta.id); + + return ( +
+

{pregunta.texto}

+
+ {pregunta.opciones.map((opcion) => { + const esSeleccionada = respuestas[pregunta.id] === opcion.id; + const mostrarCorrecta = mostrarResultados && opcion.correcta; + const mostrarIncorrecta = mostrarResultados && esSeleccionada && !opcion.correcta; + + return ( + + ); + })} +
+ + {mostrarResultados && ( +
+ {pregunta.explicacion} +
+ )} +
+ ); + })} +
+ + + + {mostrarResultados && ( +
+
+ + Resultado: {correctas}/3 correctas +
+ {correctas === 3 && ( +

¡Excelente! Dominas la relación entre CMg y CMe.

+ )} +
+ )} +
+
+ + +

Regla de Oro

+
+

CMg {'<'} CMe → CMe decrece (costo marginal menor que el promedio)

+

CMg = CMe → CMe mínimo (punto de eficiencia)

+

CMg {'>'} CMe → CMe crece (costo marginal mayor que el promedio)

+
+
+
+ ); +} + +export default RelacionCMgCMe; diff --git a/frontend/src/components/exercises/modulo4/SimuladorProduccion.tsx b/frontend/src/components/exercises/modulo4/SimuladorProduccion.tsx new file mode 100644 index 0000000..4b16807 --- /dev/null +++ b/frontend/src/components/exercises/modulo4/SimuladorProduccion.tsx @@ -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 ( +
+ + + +
+ +
+ + setPrecio(parseFloat(e.target.value) || 0)} + className="w-32" + min="0" + /> + + Ajusta el precio para ver cómo cambia la decisión óptima + +
+
+ +
+ + + + + + + + + + + + + + {datosCalculados.map((fila) => ( + + + + + + + + + + ))} + +
QPrecio (P)IT = P × QCTBT = IT - CTIMgCMg
{fila.q}{fila.precio}{fila.it}{fila.ct}= 0 ? 'text-success' : 'text-error'}`}> + {fila.bt} + + {fila.img !== null ? fila.img : '-'} + + {fila.cmg !== null ? fila.cmg : '-'} +
+
+ +
+
+ +
+

+ Cantidad Óptima: Q = {qOptima} +

+

+ Beneficio Máximo: BT = {datosCalculados.find(d => d.q === qOptima)?.bt} + {' '}(${precio} × {qOptima} - {datosCalculados.find(d => d.q === qOptima)?.ct}) +

+
+
+
+ + {verificacionIMgCMg && ( +
+
+ {verificacionIMgCMg.cumple ? ( + + ) : ( + + )} + + Verificación IMg ≈ CMg: + +
+

+ IMg = {verificacionIMgCMg.img}, CMg = {verificacionIMgCMg.cmg} + {' '}(Diferencia: {verificacionIMgCMg.diferencia.toFixed(1)}) +

+

+ {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} +

+
+ )} +
+ + + + +
+ + + + Cantidad (Q) + $ + + {datosCalculados.map((d, i) => ( + + {d.q} + + ))} + + `${80 + i * 45},${190 - d.it * escala}`).join(' ')} + /> + + `${80 + i * 45},${190 - d.ct * escala}`).join(' ')} + /> + + {datosCalculados.map((d, i) => ( + + + + + ))} + + d.q === qOptima) * 45}, ${ + 190 - (datosCalculados.find(d => d.q === qOptima)?.it || 0) * escala - 20 + })`}> + + + Óptimo Q={qOptima} + + + + + + IT (Ingreso Total) + + CT (Costo Total) + + +
+
+ + +

+ + Conceptos Clave +

+
+
+

Ingreso Total (IT)

+

IT = P × Q

+
+
+

Beneficio Total (BT)

+

BT = IT - CT

+
+
+

Ingreso Marginal (IMg)

+

IMg = ΔIT / ΔQ = P (en competencia perfecta)

+
+
+

Condición de Optimalidad

+

IMg = CMg (producir hasta que el ingreso marginal iguale al costo marginal)

+
+
+
+ +
+ +
+
+ ); +} + +export default SimuladorProduccion; diff --git a/frontend/src/components/exercises/modulo4/TablaCostos.tsx b/frontend/src/components/exercises/modulo4/TablaCostos.tsx new file mode 100644 index 0000000..f76bca4 --- /dev/null +++ b/frontend/src/components/exercises/modulo4/TablaCostos.tsx @@ -0,0 +1,200 @@ +import { useState } from 'react'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { CheckCircle, XCircle, Table } from 'lucide-react'; + +interface FilaCostos { + q: number; + cf: number; + cv: number; + ct: number | null; + cme: number | null; + cmg: number | null; +} + +export function TablaCostos() { + const CF_BASE = 200; + + const [filas, setFilas] = useState([ + { q: 0, cf: CF_BASE, cv: 0, ct: null, cme: null, cmg: null }, + { q: 1, cf: CF_BASE, cv: 50, ct: null, cme: null, cmg: null }, + { q: 2, cf: CF_BASE, cv: 90, ct: null, cme: null, cmg: null }, + { q: 3, cf: CF_BASE, cv: 120, ct: null, cme: null, cmg: null }, + { q: 4, cf: CF_BASE, cv: 160, ct: null, cme: null, cmg: null }, + { q: 5, cf: CF_BASE, cv: 220, ct: null, cme: null, cmg: null }, + { q: 6, cf: CF_BASE, cv: 300, ct: null, cme: null, cmg: null }, + { q: 7, cf: CF_BASE, cv: 400, ct: null, cme: null, cmg: null }, + { q: 8, cf: CF_BASE, cv: 520, ct: null, cme: null, cmg: null }, + ]); + + const [mostrarResultados, setMostrarResultados] = useState(false); + + const handleInputChange = (index: number, campo: 'ct' | 'cme' | 'cmg', valor: string) => { + const numValor = valor === '' ? null : parseFloat(valor); + const nuevasFilas = [...filas]; + nuevasFilas[index] = { ...nuevasFilas[index], [campo]: numValor }; + setFilas(nuevasFilas); + setMostrarResultados(false); + }; + + // Valores correctos + const valoresCorrectos = [ + { ct: 200, cme: null, cmg: null }, + { ct: 250, cme: 250, cmg: 50 }, + { ct: 290, cme: 145, cmg: 40 }, + { ct: 320, cme: 106.67, cmg: 30 }, + { ct: 360, cme: 90, cmg: 40 }, + { ct: 420, cme: 84, cmg: 60 }, + { ct: 500, cme: 83.33, cmg: 80 }, + { ct: 600, cme: 85.71, cmg: 100 }, + { ct: 720, cme: 90, cmg: 120 }, + ]; + + const validar = () => { + setMostrarResultados(true); + }; + + const esCorrecto = (index: number, campo: 'ct' | 'cme' | 'cmg', valor: number | null) => { + if (valor === null) return false; + const correcto = valoresCorrectos[index][campo]; + if (correcto === null) return true; + if (campo === 'cme' && index > 0) { + return Math.abs(valor - correcto) < 1; + } + return valor === correcto; + }; + + const todasCompletadas = filas.every((fila, index) => { + if (index === 0) return fila.ct !== null; + return fila.ct !== null && fila.cme !== null && fila.cmg !== null; + }); + + const calcularCorrectas = () => { + let correctas = 0; + filas.forEach((fila, index) => { + if (esCorrecto(index, 'ct', fila.ct)) correctas++; + if (index > 0) { + if (esCorrecto(index, 'cme', fila.cme)) correctas++; + if (esCorrecto(index, 'cmg', fila.cmg)) correctas++; + } + }); + return correctas; + }; + + const totalCampos = 1 + (filas.length - 1) * 3; + + return ( +
+ + + +
+
+ + + + + + + + + + + + + {filas.map((fila, index) => ( + + + + + + + + + ))} + +
QCFCVCTCMeCMg
{fila.q}${fila.cf}${fila.cv} +
+ $ + handleInputChange(index, 'ct', e.target.value)} + className="w-16 px-1 py-1 border rounded text-sm" + disabled={mostrarResultados} + /> + {mostrarResultados && esCorrecto(index, 'ct', fila.ct) && } + {mostrarResultados && !esCorrecto(index, 'ct', fila.ct) && } +
+
0 ? (esCorrecto(index, 'cme', fila.cme) ? 'bg-green-100' : 'bg-red-100') : ''}`}> + {index === 0 ? ( + - + ) : ( +
+ $ + handleInputChange(index, 'cme', e.target.value)} + className="w-16 px-1 py-1 border rounded text-sm" + disabled={mostrarResultados} + step="0.01" + /> + {mostrarResultados && esCorrecto(index, 'cme', fila.cme) && } + {mostrarResultados && !esCorrecto(index, 'cme', fila.cme) && } +
+ )} +
0 ? (esCorrecto(index, 'cmg', fila.cmg) ? 'bg-green-100' : 'bg-red-100') : ''}`}> + {index === 0 ? ( + - + ) : ( +
+ $ + handleInputChange(index, 'cmg', e.target.value)} + className="w-16 px-1 py-1 border rounded text-sm" + disabled={mostrarResultados} + /> + {mostrarResultados && esCorrecto(index, 'cmg', fila.cmg) && } + {mostrarResultados && !esCorrecto(index, 'cmg', fila.cmg) && } +
+ )} +
+
+ + + + {mostrarResultados && ( +
+
+ + Resultado: {calcularCorrectas()}/{totalCampos} campos correctos + + {calcularCorrectas() < totalCampos && ( +

Revisa tus cálculos. Recuerda: CT = CF + CV, CMe = CT/Q, CMg = CT actual - CT anterior.

+ )} + + )} + + + + +

Fórmulas

+
+

CT = CF + CV

+

CMe = CT / Q (solo cuando Q {'>'} 0)

+

CMg = CTₙ - CTₙ₋₁ (costo del último trabajador)

+
+
+ + ); +} + +export default TablaCostos; diff --git a/frontend/src/components/exercises/modulo4/VisualizadorExcedentes.tsx b/frontend/src/components/exercises/modulo4/VisualizadorExcedentes.tsx new file mode 100644 index 0000000..d38145f --- /dev/null +++ b/frontend/src/components/exercises/modulo4/VisualizadorExcedentes.tsx @@ -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 ( +
+ + + +
+ +
+ setPrecio(parseFloat(e.target.value))} + className="flex-1" + /> + ${precio} +
+
+ $20 + Precio de equilibrio: ${puntoEquilibrio.pEq.toFixed(1)} + $90 +
+
+ +
+ + + + + Cantidad (Q) + Precio (P) + + {[0, 25, 50, 75, 100].map((q) => ( + + + + {q} + + + ))} + + {[0, 25, 50, 75, 100].map((p) => ( + + + + {p} + + + ))} + + + + P = {precio} + + + {precio > puntoEquilibrio.pEq && ( + + )} + + {precio < puntoEquilibrio.pEq && ( + + )} + + {Math.abs(precio - puntoEquilibrio.pEq) < 2 && ( + <> + + + + )} + + `${50 + d.q * escalaX},${200 - d.pDemanda * escalaY}`).join(' ')} + /> + + d.pOferta >= 0).map(d => `${50 + d.q * escalaX},${200 - d.pOferta * escalaY}`).join(' ')} + /> + + + + E + + + + + Demanda + + Oferta + + EC + + EP + + +
+
+ +
+ +
+
+

Excedente del Consumidor

+
+

${excedentes.ec.toFixed(0)}

+

+ Área bajo la curva de demanda y sobre el precio +

+ + + +
+
+

Excedente del Productor

+
+

${excedentes.ep.toFixed(0)}

+

+ Área sobre la curva de oferta y bajo el precio +

+ + + +
+ +

Excedente Total

+
+

${excedentes.total.toFixed(0)}

+

+ EC + EP = Bienestar social total +

+
+
+ + +
+ +
+

En el Equilibrio de Mercado:

+
+
+ Precio: + ${puntoEquilibrio.pEq.toFixed(1)} +
+
+ Cantidad: + {puntoEquilibrio.qEq.toFixed(1)} +
+
+ Excedente Total: + ${excedentesEquilibrio.total.toFixed(0)} +
+
+

+ El equilibrio de mercado maximiza el bienestar social (excedente total). + Cualquier desviación del precio de equilibrio genera pérdida de eficiencia. +

+
+
+
+ +
+ +
+
+ ); +} + +export default VisualizadorExcedentes; diff --git a/frontend/src/components/exercises/modulo4/index.ts b/frontend/src/components/exercises/modulo4/index.ts new file mode 100644 index 0000000..cff8fd5 --- /dev/null +++ b/frontend/src/components/exercises/modulo4/index.ts @@ -0,0 +1,25 @@ +export { FuncionProduccion } from './FuncionProduccion'; +export { ProductoTotal } from './ProductoTotal'; +export { ProductoMarginal } from './ProductoMarginal'; +export { ProductoMedio } from './ProductoMedio'; +export { LeyRendimientosDecrecientes } from './LeyRendimientosDecrecientes'; +export { EtapasProduccion } from './EtapasProduccion'; +export { ProductorRacional } from './ProductorRacional'; +export { CortoVsLargoPlazo } from './CortoVsLargoPlazo'; +export { CostosFijosVsVariables } from './CostosFijosVsVariables'; +export { CostoTotalMedioMarginal } from './CostoTotalMedioMarginal'; +export { TablaCostos } from './TablaCostos'; +export { CurvasCosto } from './CurvasCosto'; +export { CostosMedios } from './CostosMedios'; +export { RelacionCMgCMe } from './RelacionCMgCMe'; +export { EconomiasEscala } from './EconomiasEscala'; +export { DiseconomiasEscala } from './DiseconomiasEscala'; +export { CurvaCostoLargoPlazo } from './CurvaCostoLargoPlazo'; +export { IngresoTotal } from './IngresoTotal'; +export { IngresoMarginal } from './IngresoMarginal'; +export { IngresoCompetenciaPerfecta } from './IngresoCompetenciaPerfecta'; +export { PuntoCierreEquilibrio } from './PuntoCierreEquilibrio'; +export { ReglaImgCmg } from './ReglaImgCmg'; +export { CalculadoraCostos } from './CalculadoraCostos'; +export { SimuladorProduccion } from './SimuladorProduccion'; +export { VisualizadorExcedentes } from './VisualizadorExcedentes'; diff --git a/frontend/src/components/progress/Badges.tsx b/frontend/src/components/progress/Badges.tsx new file mode 100644 index 0000000..1e6e709 --- /dev/null +++ b/frontend/src/components/progress/Badges.tsx @@ -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> = { + 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 ( + +
+
+ + + + + + +
+ +

+ {badge.titulo} +

+

+ {badge.descripcion} +

+ + {badge.fechaDesbloqueo && ( +

+ Desbloqueado: {new Date(badge.fechaDesbloqueo).toLocaleDateString()} +

+ )} +
+
+ ); + } + + return ( +
+
+
+
+ +
+
+ +
+
+ +

+ {badge.titulo} +

+ +

+ {badge.descripcion} +

+
+
+ ); +} + +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 ( +
+ {badges.map((badge, index) => ( + + + + ))} +
+ ); +} + +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 ( +
+ {/* Resumen */} +
+
+
+
+ +
+
+

Logros

+

+ {badgesDesbloqueados.length} de {totalBadges} desbloqueados +

+
+
+ + {porcentaje}% + +
+
+ +
+
+ + {/* Badges Desbloqueados */} + {badgesDesbloqueados.length > 0 && ( +
+

+ + Desbloqueados ({badgesDesbloqueados.length}) +

+ +
+ )} + + {/* Badges Bloqueados */} + {badgesBloqueados.length > 0 && ( +
+

+ + Por desbloquear ({badgesBloqueados.length}) +

+ +
+ )} +
+ ); +} + +export default BadgesGrid; diff --git a/frontend/src/components/progress/ProgressBar.tsx b/frontend/src/components/progress/ProgressBar.tsx new file mode 100644 index 0000000..165f706 --- /dev/null +++ b/frontend/src/components/progress/ProgressBar.tsx @@ -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 ( +
+ {showLabel && ( +
+ + Módulo {moduloNumero} + + + {porcentaje}% + +
+ )} + +
+ +
+ + {porcentaje === 100 && ( + + ¡Módulo completado! + + )} +
+ ); +} + +export default ProgressBar; diff --git a/frontend/src/components/progress/ScoreDisplay.tsx b/frontend/src/components/progress/ScoreDisplay.tsx new file mode 100644 index 0000000..d2aa08e --- /dev/null +++ b/frontend/src/components/progress/ScoreDisplay.tsx @@ -0,0 +1,220 @@ +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 = { + Novato: { + color: 'text-gray-600', + bgColor: 'bg-gray-100', + icon: '🌱' + }, + Estudiante: { + color: 'text-amber-700', + bgColor: 'bg-amber-100', + icon: '📚' + }, + Avanzado: { + color: 'text-gray-400', + bgColor: 'bg-gray-200', + icon: '⭐' + }, + Experto: { + color: 'text-yellow-600', + bgColor: 'bg-yellow-100', + icon: '🏆' + }, + Maestro: { + color: 'text-cyan-500', + bgColor: 'bg-cyan-100', + icon: '💎' + }, +}; + +function calcularNivel(puntuacion: number): NivelUsuario { + if (puntuacion >= 10000) return 'Maestro'; + if (puntuacion >= 6000) return 'Experto'; + if (puntuacion >= 3000) return 'Avanzado'; + if (puntuacion >= 1000) return 'Estudiante'; + return 'Novato'; +} + +function calcularProgresoNivel(puntuacion: number): { actual: number; siguiente: number; porcentaje: number } { + if (puntuacion >= 10000) { + return { actual: 10000, siguiente: 10000, porcentaje: 100 }; + } else if (puntuacion >= 6000) { + return { actual: puntuacion, siguiente: 10000, porcentaje: ((puntuacion - 6000) / 4000) * 100 }; + } else if (puntuacion >= 3000) { + return { actual: puntuacion, siguiente: 6000, porcentaje: ((puntuacion - 3000) / 3000) * 100 }; + } else if (puntuacion >= 1000) { + return { actual: puntuacion, siguiente: 3000, porcentaje: ((puntuacion - 1000) / 2000) * 100 }; + } else { + return { actual: puntuacion, siguiente: 1000, porcentaje: (puntuacion / 1000) * 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 ( +
+
+
+
+ +
+
+

Puntuación Total

+
+ + {puntosAnimados.toLocaleString()} + + pts + + + {cambioReciente !== 0 && ( + 0 ? 'text-green-600' : 'text-red-600'}`} + > + {cambioReciente > 0 ? '+' : ''}{cambioReciente} + + )} + +
+
+
+ + {showNivel && ( +
+ + {configNivel.icon} + {nivel} + +
+ )} +
+ + {showNivel && progresoNivel.siguiente > progresoNivel.actual && ( +
+
+ Progreso hacia {calcularNivel(progresoNivel.siguiente)} + {Math.round(progresoNivel.porcentaje)}% +
+
+ +
+

+ {progresoNivel.siguiente - progresoNivel.actual} puntos para el siguiente nivel +

+
+ )} + + {progresoNivel.porcentaje === 100 && nivel === 'Maestro' && ( + +

+ 🎉 ¡Has alcanzado el nivel máximo! +

+
+ )} +
+ ); +} + +export default ScoreDisplay; diff --git a/frontend/src/components/progress/index.ts b/frontend/src/components/progress/index.ts new file mode 100644 index 0000000..6e3a0b2 --- /dev/null +++ b/frontend/src/components/progress/index.ts @@ -0,0 +1,3 @@ +export { ProgressBar } from './ProgressBar'; +export { ScoreDisplay } from './ScoreDisplay'; +export { BadgesGrid, BadgesSection, BadgeCard } from './Badges'; diff --git a/frontend/src/components/ui/Button.tsx b/frontend/src/components/ui/Button.tsx new file mode 100644 index 0000000..d6af066 --- /dev/null +++ b/frontend/src/components/ui/Button.tsx @@ -0,0 +1,45 @@ +import { ButtonHTMLAttributes, forwardRef } from 'react'; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: 'primary' | 'secondary' | 'outline' | 'ghost'; + size?: 'sm' | 'md' | 'lg'; + isLoading?: boolean; +} + +export const Button = forwardRef( + ({ className = '', variant = 'primary', size = 'md', isLoading, children, disabled, ...props }, ref) => { + const baseStyles = 'inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed'; + + const variants = { + primary: 'bg-primary text-white hover:bg-blue-700 focus:ring-blue-500', + secondary: 'bg-secondary text-white hover:bg-violet-700 focus:ring-violet-500', + outline: 'border-2 border-primary text-primary hover:bg-primary hover:text-white focus:ring-primary', + ghost: 'text-gray-600 hover:bg-gray-100 focus:ring-gray-500', + }; + + const sizes = { + sm: 'px-3 py-1.5 text-sm', + md: 'px-4 py-2 text-base', + lg: 'px-6 py-3 text-lg', + }; + + return ( + + ); + } +); + +Button.displayName = 'Button'; diff --git a/frontend/src/components/ui/Card.tsx b/frontend/src/components/ui/Card.tsx new file mode 100644 index 0000000..73773dd --- /dev/null +++ b/frontend/src/components/ui/Card.tsx @@ -0,0 +1,38 @@ +import { ReactNode } from 'react'; + +interface CardProps { + children: ReactNode; + className?: string; + onClick?: () => void; +} + +export function Card({ children, className = '', onClick }: CardProps) { + return ( +
+ {children} +
+ ); +} + +interface CardHeaderProps { + title: string; + subtitle?: string; + action?: ReactNode; +} + +export function CardHeader({ title, subtitle, action }: CardHeaderProps) { + return ( +
+
+

{title}

+ {subtitle &&

{subtitle}

} +
+ {action &&
{action}
} +
+ ); +} diff --git a/frontend/src/components/ui/Input.tsx b/frontend/src/components/ui/Input.tsx new file mode 100644 index 0000000..9b8058a --- /dev/null +++ b/frontend/src/components/ui/Input.tsx @@ -0,0 +1,48 @@ +import { InputHTMLAttributes, forwardRef, ReactNode } from 'react'; + +interface InputProps extends InputHTMLAttributes { + label?: string; + error?: string; + icon?: ReactNode; +} + +export const Input = forwardRef( + ({ className = '', label, error, icon, id, ...props }, ref) => { + const inputId = id || label?.toLowerCase().replace(/\s+/g, '-'); + + return ( +
+ {label && ( + + )} +
+ {icon && ( +
+ {icon} +
+ )} + +
+ {error && ( +

{error}

+ )} +
+ ); + } +); + +Input.displayName = 'Input'; diff --git a/frontend/src/components/ui/Loader.tsx b/frontend/src/components/ui/Loader.tsx new file mode 100644 index 0000000..c0d9683 --- /dev/null +++ b/frontend/src/components/ui/Loader.tsx @@ -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 ( + + ); +} diff --git a/frontend/src/content/modulo1/agentes.ts b/frontend/src/content/modulo1/agentes.ts new file mode 100644 index 0000000..1e23b49 --- /dev/null +++ b/frontend/src/content/modulo1/agentes.ts @@ -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; diff --git a/frontend/src/content/modulo1/ejercicios.ts b/frontend/src/content/modulo1/ejercicios.ts new file mode 100644 index 0000000..174c3dd --- /dev/null +++ b/frontend/src/content/modulo1/ejercicios.ts @@ -0,0 +1,1372 @@ +import type { Ejercicio } from './introduccion'; + +export interface EjercicioDetallado extends Omit { + instrucciones: string; + pistas?: string[]; + solucion?: string; + dificultad: 'facil' | 'medio' | 'dificil'; + duracionEstimada: number; // en minutos + objetivosAprendizaje: string[]; + // Configuraciones específicas por tipo + config?: Record; +} + +// ============================================ +// CONFIGURACIONES ESPECÍFICAS POR TIPO +// ============================================ + +export interface QuizConfig { + preguntas: QuizPregunta[]; + modo: 'seleccion-unica' | 'multiple' | 'clasificacion' | 'identificacion' | 'clasificacion-multiple'; + opciones?: string[]; + configuracionVisual?: { + mostrarBarraProgreso?: boolean; + mostrarPuntaje?: boolean; + retroalimentacionInmediata?: boolean; + tiempoLimite?: number; + permitirReintentar?: boolean; + }; + nivelesDificultad?: Record; +} + +export interface QuizPregunta { + id: string; + pregunta: string; + opciones?: string[]; + respuestaCorrecta: string | string[]; + explicacion?: string; + imagen?: string; + expresion?: string; + categoriaElasticidad?: string; + bien?: string; + descripcion?: string; + explicacionDetallada?: string; +} + +export interface SliderConfig { + escenario: { + titulo: string; + descripcion: string; + bienA: { nombre: string; unidad: string; maxProduccion: number; color: string }; + bienB: { nombre: string; unidad: string; maxProduccion: number; color: string }; + }; + parametros: { + mostrarFPP?: boolean; + mostrarCostoOportunidad?: boolean; + mostrarPuntoActual?: boolean; + tipoCurva?: 'lineal' | 'concava'; + totalRecursos?: number; + puntosDesplazamiento?: number; + }; +} + +export interface JuegoConfig { + tipoJuego: 'drag-and-drop' | 'memoria' | 'ordenar'; + elementosArrastrables?: Array<{ id: string; texto: string; tipo: string; categoria?: string }>; + opcionesParejas?: Array<{ id: string; elementoA: string; elementoB: string }>; + correcta?: boolean; +} + +export interface CalculadoraConfig { + formula: string; + variables: Array<{ nombre: string; simbolo: string; unidad: string; valorDefecto?: number }>; + resultadoEsperado?: number; + pasos?: string[]; + permiteDecimales?: boolean; +} + +export interface MatchingConfig { + columnas: Array<{ titulo: string; elementos: string[] }>; + parejasCorrectas: Array<{ izquierda: string; derecha: string }>; + modo: 'arrastrar' | 'seleccionar'; +} + +export interface InteractiveConfig { + tipo: 'fpp' | 'grafico' | 'diagrama' | 'clasificacion'; + puntosInteractivos?: Array<{ x: number; y: number;movible: boolean; etiqueta?: string }>; + restricciones?: { xMin: number; xMax: number; yMin: number; yMax: number }; + diagrama?: Record; + configuracion?: Record; +} + +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` + }, + +// ============================================ +// EJERCICIO 4: Quiz - Microeconomía vs Macroeconomía +// ============================================ +{ + id: 'quiz-micro-macro', + tipo: 'quiz', + titulo: 'Quiz: Microeconomía vs Macroeconomía', + descripcion: 'Identifica si los siguientes temas pertenecen al ámbito de la microeconomía o la macroeconomía', + instrucciones: `En este quiz debes clasificar cada enunciado o tema según corresponda a microeconomía o macroeconomía: + +- **Microeconomía**: Estudia decisiones individuales de hogares, empresas y mercados específicos +- **Macroeconomía**: Estudia la economía en su conjunto (PIB, inflación, desempleo, políticas económicas) + +Lee cada pregunta cuidadosamente y selecciona la respuesta correcta.`, + dificultad: 'facil', + duracionEstimada: 10, + objetivosAprendizaje: [ + 'Distinguir entre microeconomía y macroeconomía', + 'Comprender el nivel de análisis de cada rama económica', + 'Identificar ejemplos de decisiones individuales vs agregadas' + ], + config: { + preguntas: [ + { + id: 'p1', + pregunta: '¿Por qué bajan los precios de un teléfono específico?', + opciones: ['Microeconomía', 'Macroeconomía'], + respuestaCorrecta: 'Microeconomía', + explicacion: 'El estudio de cómo se determina el precio de un bien específico es un tema microeconómico' + }, + { + id: 'p2', + pregunta: '¿Por qué aumenta el desempleo en el país?', + opciones: ['Microeconomía', 'Macroeconomía'], + respuestaCorrecta: 'Macroeconomía', + explicacion: 'El desempleo es una variable agregada que estudia la macroeconomía' + }, + { + id: 'p3', + pregunta: '¿Cuánto debería producir una empresa para maximizar ganancias?', + opciones: ['Microeconomía', 'Macroeconomía'], + respuestaCorrecta: 'Microeconomía', + explicacion: 'Las decisiones de producción de una empresa individual son tema microeconómico' + }, + { + id: 'p4', + pregunta: '¿Qué causa la inflación en la economía?', + opciones: ['Microeconomía', 'Macroeconomía'], + respuestaCorrecta: 'Macroeconomía', + explicacion: 'La inflación es un fenómeno macroeconómico que afecta el nivel general de precios' + }, + { + id: 'p5', + pregunta: '¿Cómo afecta un impuesto al consumo de un bien específico?', + opciones: ['Microeconomía', 'Macroeconomía'], + respuestaCorrecta: 'Microeconomía', + explicacion: 'El efecto de impuestos en mercados específicos se estudia en microeconomía' + }, + { + id: 'p6', + pregunta: '¿Cómo se calcula el Producto Interno Bruto (PIB)?', + opciones: ['Microeconomía', 'Macroeconomía'], + respuestaCorrecta: 'Macroeconomía', + explicacion: 'El PIB es una variable macroeconómica que mide la producción agregada' + }, + { + id: 'p7', + pregunta: '¿Por qué algunas personas ganan más que otras?', + opciones: ['Microeconomía', 'Macroeconomía'], + respuestaCorrecta: 'Microeconomía', + explicacion: 'La distribución del ingreso a nivel individual es tema microeconómico' + }, + { + id: 'p8', + pregunta: '¿Qué políticas puede usar el gobierno para estimular la economía?', + opciones: ['Microeconomía', 'Macroeconomía'], + respuestaCorrecta: 'Macroeconomía', + explicacion: 'Las políticas fiscal y monetaria son herramientas macroeconómicas' + } + ], + modo: 'seleccion-unica', + configuracionVisual: { + mostrarBarraProgreso: true, + mostrarPuntaje: true, + retroalimentacionInmediata: true, + tiempoLimite: 600, + permitirReintentar: true + } + }, + pistas: [ + 'La microeconomía estudia el "árbol" (individual), la macroeconomía estudia el "bosque" (conjunto)', + 'Si la pregunta menciona "un bien", "una empresa" o "un consumidor", probablemente sea microeconomía', + 'Si la pregunta menciona "país", "inflación", "desempleo" o "PIB", es macroeconomía' + ], + solucion: `Recordatorio: +- **Microeconomía**: Decisiones individuales (empresas, consumidores, mercados específicos) +- **Macroeconomía**: Fenómenos agregados (PIB, inflación, desempleo, políticas económicas)` +}, + +// ============================================ +// EJERCICIO 5: Quiz - Problema Económico Fundamental +// ============================================ +{ + id: 'quiz-problema-economico', + tipo: 'quiz', + titulo: 'Quiz: Las Tres Preguntas Económicas Fundamentales', + descripcion: 'Identifica cuál de las tres preguntas económicas fundamentales responde cada situación', + instrucciones: `Toda sociedad debe responder tres preguntas económicas básicas: + +1. **¿Qué producir?**: Qué bienes y servicios se fabricarán +2. **¿Cómo producir?**: Qué combinación de recursos y tecnología usar +3. **¿Para quién producir?**: Cómo se distribuirán los bienes producidos + +Identifica qué pregunta responde cada situación económica.`, + dificultad: 'facil', + duracionEstimada: 8, + objetivosAprendizaje: [ + 'Identificar las tres preguntas fundamentales de la economía', + 'Relacionar decisiones económicas con las preguntas fundamentales', + 'Comprender por qué toda sociedad debe resolver estas preguntas' + ], + config: { + preguntas: [ + { + id: 'p1', + pregunta: 'Una empresa decide usar máquinas en lugar de trabajadores para producir autos. ¿Qué pregunta responde?', + opciones: ['¿Qué producir?', '¿Cómo producir?', '¿Para quién producir?'], + respuestaCorrecta: '¿Cómo producir?', + explicacion: 'Esta decisión se refiere a la tecnología y métodos de producción a utilizar' + }, + { + id: 'p2', + pregunta: 'El gobierno decide construir más hospitales que escuelas. ¿Qué pregunta responde?', + opciones: ['¿Qué producir?', '¿Cómo producir?', '¿Para quién producir?'], + respuestaCorrecta: '¿Qué producir?', + explicacion: 'Esta decisión determina qué bienes y servicios se producirán con los recursos disponibles' + }, + { + id: 'p3', + pregunta: 'Se decide que los médicos y enfermeras reciban los servicios de salud antes que otros grupos. ¿Qué pregunta responde?', + opciones: ['¿Qué producir?', '¿Cómo producir?', '¿Para quién producir?'], + respuestaCorrecta: '¿Para quién producir?', + explicacion: 'Esta decisión determina cómo se distribuyen los bienes y servicios producidos' + }, + { + id: 'p4', + pregunta: 'Una fábrica decide automatizar su producción de textiles. ¿Qué pregunta responde?', + opciones: ['¿Qué producir?', '¿Cómo producir?', '¿Para quién producir?'], + respuestaCorrecta: '¿Cómo producir?', + explicacion: 'La elección entre trabajo manual o automatizado responde a cómo producir' + }, + { + id: 'p5', + pregunta: 'La economía debe decidir entre producir alimentos o armamento. ¿Qué pregunta responde?', + opciones: ['¿Qué producir?', '¿Cómo producir?', '¿Para quién producir?'], + respuestaCorrecta: '¿Qué producir?', + explicacion: 'Elegir qué bienes producir (alimentos vs armamento) es la pregunta qué producir' + }, + { + id: 'p6', + pregunta: 'Se establece que los ancianos reciban pensiones garantizadas. ¿Qué pregunta responde?', + opciones: ['¿Qué producir?', '¿Cómo producir?', '¿Para quién producir?'], + respuestaCorrecta: '¿Para quién producir?', + explicacion: 'Las políticas de distribución responden a la pregunta para quién producir' + } + ], + modo: 'seleccion-unica', + configuracionVisual: { + mostrarBarraProgreso: true, + mostrarPuntaje: true, + retroalimentacionInmediata: true, + tiempoLimite: 480, + permitirReintentar: true + } + }, + pistas: [ + '¿Qué producir? → Selección de bienes/servicios a fabricar', + '¿Cómo producir? → Selección de tecnología y métodos de producción', + '¿Para quién producir? → Selección de criterios de distribución' + ], + solucion: `Las tres preguntas fundamentales: +1. **¿Qué producir?** → Bienes y servicios a fabricar +2. **¿Cómo producir?** → Tecnología y métodos (trabajo vs capital) +3. **¿Para quién producir?** → Distribución de la producción` +}, + +// ============================================ +// EJERCICIO 6: Slider - Escasez y Distribución de Recursos +// ============================================ +{ + id: 'simulador-escases-recursos', + tipo: 'slider', + titulo: 'Simulador: Escasez y Distribución de Recursos', + descripcion: 'Distribuye 100 puntos de recursos entre diferentes necesidades básicas', + instrucciones: `Tienes 100 puntos de recursos limitados para distribuir entre 5 necesidades básicas de una sociedad: + +1. **Alimentación**: Básica para la supervivencia +2. **Salud**: Servicios médicos y medicamentos +3. **Educación**: Formación y escuelas +4. **Vivienda**: Construcción y mantenimiento de hogares +5. **Infraestructura**: Caminos, puentes, servicios públicos + +Instrucciones: +1. Usa los sliders para asignar recursos a cada necesidad +2. El total debe sumar exactamente 100 puntos +3. Observa las consecuencias de tu distribución +4. Reflexiona sobre qué significa la escasez`, + dificultad: 'medio', + duracionEstimada: 12, + objetivosAprendizaje: [ + 'Comprender el concepto de escasez', + 'Visualizar la necesidad de elegir entre usos alternativos', + 'Entender el trade-off en la asignación de recursos', + 'Relacionar la escasez con la toma de decisiones' + ], + config: { + escenario: { + titulo: 'Asignación de Recursos Societales', + descripcion: 'Una sociedad debe distribuir recursos limitados entre múltiples necesidades ilimitadas', + bienA: { nombre: 'Total Asignado', unidad: 'puntos', maxProduccion: 100, color: '#4CAF50' }, + bienB: { nombre: 'Restante', unidad: 'puntos', maxProduccion: 100, color: '#f44336' } + }, + parametros: { + mostrarFPP: false, + mostrarCostoOportunidad: true, + mostrarPuntoActual: true, + tipoCurva: 'lineal', + totalRecursos: 100 + } + }, + pistas: [ + 'La escasez existe porque los recursos son limitados pero las necesidades son ilimitadas', + 'No puedes satisfacer todas las necesidades completamente con recursos limitados', + 'Cada punto que das a una necesidad es un punto que no tiene otra' + ], + solucion: `Este ejercicio ilustra el problema fundamental de la escasez: +1. Con recursos limitados (100 puntos), debes elegir entre necesidades ilimitadas +2. No es posible maximizar todas las necesidades simultáneamente +3. La decisión de asignación refleja prioridades sociales y valores +4. Siempre habrá necesidades insatisfechas debido a la escasez` +}, + +// ============================================ +// EJERCICIO 7: Quiz - Economía Positiva vs Normativa +// ============================================ +{ + id: 'quiz-economia-positiva-normativa', + tipo: 'quiz', + titulo: 'Quiz: Economía Positiva vs Economía Normativa', + descripcion: 'Identifica si los enunciados económicos son positivos (descriptivos) o normativos (prescriptivos)', + instrucciones: `Distingue entre los dos tipos de enunciados económicos: + +- **Economía Positiva**: Describe cómo es la economía actualmente (hechos, datos). Se puede verificar empíricamente. +- **Economía Normativa**: Describe cómo debería ser la economía (juicios de valor, opiniones). No se puede verificar empíricamente. + +Identifica cada enunciado como positivo o normativo.`, + dificultad: 'facil', + duracionEstimada: 8, + objetivosAprendizaje: [ + 'Diferenciar entre enunciados positivos y normativos', + 'Comprender el método científico en economía', + 'Identificar juicios de valor en análisis económicos' + ], + config: { + preguntas: [ + { + id: 'p1', + pregunta: '"El desempleo en España es del 12%"', + opciones: ['Econ. Positiva', 'Econ. Normativa'], + respuestaCorrecta: 'Econ. Positiva', + explicacion: 'Este es un enunciado verificable con datos reales' + }, + { + id: 'p2', + pregunta: '"El gobierno debería aumentar los impuestos a los ricos"', + opciones: ['Econ. Positiva', 'Econ. Normativa'], + respuestaCorrecta: 'Econ. Normativa', + explicacion: 'Este enunciado incluye un juicio de valor ("debería")' + }, + { + id: 'p3', + pregunta: '"Cuando sube el precio de un bien, la cantidad demandada baja"', + opciones: ['Econ. Positiva', 'Econ. Normativa'], + respuestaCorrecta: 'Econ. Positiva', + explicacion: 'Es una ley económica verificable empíricamente (Ley de la demanda)' + }, + { + id: 'p4', + pregunta: '"Es injusto que algunos ganen tanto mientras otros viven en pobreza"', + opciones: ['Econ. Positiva', 'Econ. Normativa'], + respuestaCorrecta: 'Econ. Normativa', + expresion: 'Expresa un juicio de valor sobre lo que es "injusto"' + }, + { + id: 'p5', + pregunta: '"La inflación ha disminuido del 8% al 3% en el último año"', + opciones: ['Econ. Positiva', 'Econ. Normativa'], + respuestaCorrecta: 'Econ. Positiva', + explicacion: 'Es un enunciado verificable con datos económicos' + }, + { + id: 'p6', + pregunta: '"El tipo mínimo debería ser del 15%"', + opciones: ['Econ. Positiva', 'Econ. Normativa'], + respuestaCorrecta: 'Econ. Normativa', + explicacion: 'Incluye un juicio de valor sobre lo que "debería ser"' + }, + { + id: 'p7', + pregunta: '"Un aumento del salario mínimo reduce el empleo juvenil"', + opciones: ['Econ. Positiva', 'Econ. Normativa'], + respuestaCorrecta: 'Econ. Positiva', + explicacion: 'Es una proposición que podría verificarse empíricamente' + }, + { + id: 'p8', + pregunta: '"Es preferable priorizar el crecimiento económico sobre el medio ambiente"', + opciones: ['Econ. Positiva', 'Econ. Normativa'], + respuestaCorrecta: 'Econ. Normativa', + explicacion: 'Expresa una preferencia o juicio de valor' + } + ], + modo: 'seleccion-unica', + configuracionVisual: { + mostrarBarraProgreso: true, + mostrarPuntaje: true, + retroalimentacionInmediata: true, + tiempoLimite: 480, + permitirReintentar: true + } + }, + pistas: [ + 'Busca palabras como "debería", "es preferible", "es justo" → son normativos', + 'Los positivos describen hechos: "es", "ha sido", "aumentó"', + 'Los normativos incluyen opiniones o juicios de valor' + ], + solucion: `Diferencia clave: +- **Positivo**: "Cómo ES" - verificable con datos +- **Normativo**: "Cómo DEBERÍA SER" - juicio de valor + +La economía positiva busca explicar; la normativa busca recomendar.` +}, + +// ============================================ +// EJERCICIO 8: Quiz - Sistemas Económicos +// ============================================ +{ + id: 'quiz-sistemas-economicos', + tipo: 'quiz', + titulo: 'Quiz: Sistemas Económicos', + descripcion: 'Identifica las características de los principales sistemas económicos', + instrucciones: `Los tres sistemas económicos principales son: + +1. **Sistema de Mercado**: Las decisiones las toman compradores y vendedores. Los precios se determinan por oferta y demanda. + +2. **Sistema de Planificación Centralizada**: El Estado decide qué, cómo y para quién producir. No hay propiedad privada de los medios de producción. + +3. **Sistema Mixto**: Combinación de elementos de mercado y planificación. El Estado y el mercado comparten las decisiones económicas. + +Identifica el sistema al que corresponde cada característica.`, + dificultad: 'medio', + duracionEstimada: 10, + objetivosAprendizaje: [ + 'Conocer los tres sistemas económicos principales', + 'Diferenciar las características de cada sistema', + 'Comprender cómo se toman las decisiones económicas en cada sistema' + ], + config: { + preguntas: [ + { + id: 'p1', + pregunta: 'En este sistema, los precios se determinan por la oferta y la demanda', + opciones: ['Mercado', 'Planificación Centralizada', 'Mixto'], + respuestaCorrecta: 'Mercado', + explicacion: 'En el sistema de mercado, los precios emergen de la interacción de oferta y demanda' + }, + { + id: 'p2', + pregunta: 'El Estado decide qué se producirá y en qué cantidad', + opciones: ['Mercado', 'Planificación Centralizada', 'Mixto'], + respuestaCorrecta: 'Planificación Centralizada', + explicacion: 'En la planificación centralizada, el Estado es el único decisor económico' + }, + { + id: 'p3', + pregunta: 'Combina elementos del mercado con intervención del Estado', + opciones: ['Mercado', 'Planificación Centralizada', 'Mixto'], + respuestaCorrecta: 'Mixto', + explicacion: 'Los sistemas mixtos usan el mercado pero con participación estatal' + }, + { + id: 'p4', + pregunta: 'La propiedad privada de los medios de producción es fundamental', + opciones: ['Mercado', 'Planificación Centralizada', 'Mixto'], + respuestaCorrecta: 'Mercado', + explicacion: 'El sistema de mercado se basa en la propiedad privada' + }, + { + id: 'p5', + pregunta: 'El Estado distribuye los bienes según un plan', + opciones: ['Mercado', 'Planificación Centralizada', 'Mixto'], + respuestaCorrecta: 'Planificación Centralizada', + explicacion: 'La planificación central implica distribución estatal según planes' + }, + { + id: 'p6', + pregunta: 'Ejemplo actual: La mayoría de los países europeos', + opciones: ['Mercado', 'Planificación Centralizada', 'Mixto'], + respuestaCorrecta: 'Mixto', + explicacion: 'Los países europeos tienen economía de mercado con fuerte intervención estatal' + }, + { + id: 'p7', + pregunta: 'La competencia entre empresas impulsa la eficiencia', + opciones: ['Mercado', 'Planificación Centralizada', 'Mixto'], + respuestaCorrecta: 'Mercado', + explicacion: 'La competencia es un elemento central del sistema de mercado' + }, + { + id: 'p8', + pregunta: 'El Estado puede nacionalizar industrias estratégicas', + opciones: ['Mercado', 'Planificación Centralizada', 'Mixto'], + respuestaCorrecta: 'Mixto', + explicacion: 'Los sistemas mixtos permiten nacionalizaciones en sectores clave' + } + ], + modo: 'seleccion-unica', + configuracionVisual: { + mostrarBarraProgreso: true, + mostrarPuntaje: true, + retroalimentacionInmediata: true, + tiempoLimite: 600, + permitirReintentar: true + } + }, + pistas: [ + 'Mercado → precios por oferta/demanda, propiedad privada, competencia', + 'Planificación → el Estado decide todo, propiedad estatal', + 'Mixto → combinación de ambos, mayoría de países actuales' + ], + solucion: `Sistemas económicos: +- **Mercado**: Decisiones descentralizadas, precios por oferta/demanda, propiedad privada +- **Planificación Centralizada**: Decisiones del Estado, distribución planificada, propiedad estatal +- **Mixto**: Combinación de mercado y Estado (ej: Europa, América Latina)` +}, + +// ============================================ +// EJERCICIO 9: Interactive - Constructor de FPP +// ============================================ +{ + id: 'interactive-constructor-fpp', + tipo: 'interactive', + titulo: 'Interactive: Constructor de Frontera de Posibilidades de Producción', + descripcion: 'Construye la curva FPP arrastrando puntos para representar diferentes escenarios económicos', + instrucciones: `La Frontera de Posibilidades de Producción (FPP) muestra las combinaciones máximas de dos bienes que una economía puede producir. + +En este ejercicio: +1. Arrastra los puntos para dibujar la curva FPP +2. Los puntos naranjas representan puntos de producción +3. Arrastra el punto verde para explorar la curva +4. Observa cómo cambia el costo de oportunidad + +Experimenta con diferentes formas de la curva y observa las implicaciones.`, + dificultad: 'medio', + duracionEstimada: 15, + objetivosAprendizaje: [ + 'Comprender la forma y significado de la FPP', + 'Visualizar puntos eficientes, ineficientes e inalcanzables', + 'Relacionar la pendiente con el costo de oportunidad', + 'Entender por qué la FPP es convexa' + ], + config: { + tipo: 'fpp', + puntosInteractivos: [ + { x: 0, y: 100, movible: false, etiqueta: 'Solo Bienes de Capital' }, + { x: 20, y: 85, movible: true, etiqueta: 'A' }, + { x: 40, y: 60, movible: true, etiqueta: 'B' }, + { x: 60, y: 30, movible: true, etiqueta: 'C' }, + { x: 80, y: 10, movible: true, etiqueta: 'D' }, + { x: 100, y: 0, movible: false, etiqueta: 'Solo Bienes de Consumo' } + ], + restricciones: { xMin: 0, xMax: 100, yMin: 0, yMax: 100 }, + feedbackEnTiempoReal: true + }, + pistas: [ + 'La FPP tiene pendiente negativa: para más de un bien, menos del otro', + 'Puntos sobre la curva son eficientes', + 'Puntos dentro de la curva son ineficientes', + 'Puntos fuera son inalcanzables con los recursos actuales' + ], + solucion: `La FPP representa: +1. **Pendiente negativa**: Trade-off entre bienes +2. **Forma convexa**: Costos de oportunidad crecientes +3. **Sobre la curva**: Eficiente +4. **Dentro de la curva**: Ineficiente +5. **Fuera de la curva**: Inalcanzable (sin crecimiento)` +}, + +// ============================================ +// EJERCICIO 10: Quiz - Agentes Económicos +// ============================================ +{ + id: 'quiz-agentes-economicos', + tipo: 'quiz', + titulo: 'Quiz: Identificación de Agentes Económicos', + descripcion: 'Identifica qué agente económico realiza cada actividad', + instrucciones: `Los cuatro agentes económicos fundamentales son: + +1. **Familias/Hogares**: Individuos que consumen bienes y ofrecen factores productivos +2. **Empresas**: Organizaciones que producen bienes y servicios +3. **Estado/Gobierno**: Instuciones públicas que regulan y participan en la economía +4. **Sector Exterior**: Agentes económicos de otros países + +Identifica qué agente realiza cada actividad económica.`, + dificultad: 'facil', + duracionEstimada: 8, + objetivosAprendizaje: [ + 'Identificar los cuatro agentes económicos', + 'Reconocer las funciones de cada agente', + 'Comprender la interdependencia entre agentes' + ], + config: { + preguntas: [ + { + id: 'p1', + pregunta: 'Una familia compra un automóvil nuevo', + opciones: ['Familias', 'Empresas', 'Estado', 'Sector Exterior'], + respuestaCorrecta: 'Familias', + explicacion: 'Las familias son agentes consumidores que demandan bienes y servicios' + }, + { + id: 'p2', + pregunta: 'Una fábrica de coches produce vehículos', + opciones: ['Familias', 'Empresas', 'Estado', 'Sector Exterior'], + respuestaCorrecta: 'Empresas', + explicacion: 'Las empresas son los agentes productores por excelencia' + }, + { + id: 'p3', + pregunta: 'El gobierno recauda impuestos', + opciones: ['Familias', 'Empresas', 'Estado', 'Sector Exterior'], + respuestaCorrecta: 'Estado', + explicacion: 'El Estado tiene la función de recaudo fiscal para financiar gasto público' + }, + { + id: 'p4', + pregunta: 'Una empresa importa materias primas de China', + opciones: ['Familias', 'Empresas', 'Estado', 'Sector Exterior'], + respuestaCorrecta: 'Sector Exterior', + explicacion: 'Las importaciones involucran al sector externo (resto del mundo)' + }, + { + id: 'p5', + pregunta: 'Un trabajador vende su fuerza de trabajo', + opciones: ['Familias', 'Empresas', 'Estado', 'Sector Exterior'], + respuestaCorrecta: 'Familias', + explicacion: 'Las familias ofrecen factores productivos como el trabajo' + }, + { + id: 'p6', + pregunta: 'El gobierno construye carreteras', + opciones: ['Familias', 'Empresas', 'Estado', 'Sector Exterior'], + respuestaCorrecta: 'Estado', + explicacion: 'El Estado realiza gasto público en infraestructura' + }, + { + id: 'p7', + pregunta: 'Una empresa exporta productos al extranjero', + opciones: ['Familias', 'Empresas', 'Estado', 'Sector Exterior'], + respuestaCorrecta: 'Sector Exterior', + explicacion: 'Las exportaciones involucran al sector externo' + }, + { + id: 'p8', + pregunta: 'Una familia recibe una transferencia del gobierno', + opciones: ['Familias', 'Empresas', 'Estado', 'Sector Exterior'], + respuestaCorrecta: 'Estado', + explicacion: 'Las transferencias (pensiones, subsidios) son realizadas por el Estado' + } + ], + modo: 'seleccion-unica', + configuracionVisual: { + mostrarBarraProgreso: true, + mostrarPuntaje: true, + retroalimentacionInmediata: true, + tiempoLimite: 480, + permitirReintentar: true + } + }, + pistas: [ + 'Familias → consumen y ofrecen trabajo/capital', + 'Empresas → producen bienes y servicios', + 'Estado → recauda, regula, gasta', + 'Sector Exterior → importan y exportan con otros países' + ], + solucion: `Agentes económicos: +- **Familias**: Consumidores, oferentes de factores +- **Empresas**: Productores de bienes y servicios +- **Estado**: Regulador, recaudador, gasta en bienes públicos +- **Sector Exterior**: Comercia con el resto del mundo` +}, + +// ============================================ +// EJERCICIO 11: Matching - Roles de Agentes Económicos +// ============================================ +{ + id: 'matching-roles-agentes', + tipo: 'matching', + titulo: 'Matching: Roles y Acciones de los Agentes Económicos', + descripcion: 'Relaciona cada agente económico con sus acciones características', + instrucciones: `Relaciona correctamente cada agente económico con las acciones que realiza. + +Tienes dos columnas: +- **Columna Izquierda**: Agentes Económicos +- **Columna Derecha**: Acciones que realizan + +Arrastra cada acción a su agente correspondiente.`, + dificultad: 'facil', + duracionEstimada: 8, + objetivosAprendizaje: [ + 'Identificar las funciones de cada agente económico', + 'Comprender el rol de cada agente en la economía', + 'Relacionar teoría con ejemplos prácticos' + ], + config: { + columnas: [ + { + titulo: 'Agentes Económicos', + elementos: ['Familias', 'Empresas', 'Estado', 'Sector Exterior'] + }, + { + titulo: 'Acciones', + elementos: [ + 'Consumen bienes y servicios', + 'Producen bienes y servicios', + 'Recaudan impuestos', + 'Importan y exportan', + 'Ofrecen factores productivos', + 'Realizan gasto público', + 'Obtienen beneficios', + 'Intercambian con el resto del mundo' + ] + } + ], + parejasCorrectas: [ + { izquierda: 'Familias', derecha: 'Consumen bienes y servicios' }, + { izquierda: 'Familias', derecha: 'Ofrecen factores productivos' }, + { izquierda: 'Empresas', derecha: 'Producen bienes y servicios' }, + { izquierda: 'Empresas', derecha: 'Obtienen beneficios' }, + { izquierda: 'Estado', derecha: 'Recaudan impuestos' }, + { izquierda: 'Estado', derecha: 'Realizan gasto público' }, + { izquierda: 'Sector Exterior', derecha: 'Importan y exportan' }, + { izquierda: 'Sector Exterior', derecha: 'Intercambian con el resto del mundo' } + ], + modo: 'arrastrar' + }, + pistas: [ + 'Las familias ofrecen trabajo y capital a las empresas', + 'Las empresas pagan salarios, alquileres e intereses', + 'El Estado financia su gasto con impuestos', + 'El sector exterior conecta la economía con el mundo' + ], + solucion: `Roles principales: +- **Familias**: Consumo, oferta de factores (trabajo, capital, tierra) +- **Empresas**: Producción, creación de empleo, búsqueda de beneficios +- **Estado**: Regulación, redistribución, provisión de bienes públicos +- **Sector Exterior**: Comercio internacional, flujos financieros` +}, + +// ============================================ +// EJERCICIO 12: Quiz - Factores de Producción +// ============================================ +{ + id: 'quiz-factores-produccion', + tipo: 'quiz', + titulo: 'Quiz: Factores de Producción', + descripcion: 'Identifica los factores de producción y su remuneración', + instrucciones: `Los cuatro factores de producción son: + +1. **Tierra**: Recursos naturales. Remuneración: Renta +2. **Trabajo**: Esfuerzo humano. Remuneración: Salario +3. **Capital**: Bienes produzidos para producir otros bienes. Remuneración: Interés +4. **Tecnología/Empresa**: Capacidad organizativa e innovación. Remuneración: Beneficio + +Identifica cada factor y su remuneración correspondiente.`, + dificultad: 'facil', + duracionEstimada: 8, + objetivosAprendizaje: [ + 'Identificar los cuatro factores de producción', + 'Conocer la remuneración de cada factor', + 'Comprender cómo se genera el ingreso en la economía' + ], + config: { + preguntas: [ + { + id: 'p1', + pregunta: 'Un agricultor usa un campo fértil para cultivar trigo. ¿Qué factor usa?', + opciones: ['Tierra', 'Trabajo', 'Capital', 'Tecnología'], + respuestaCorrecta: 'Tierra', + explicacion: 'La tierra incluye todos los recursos naturales' + }, + { + id: 'p2', + pregunta: 'Un obrero construye una casa. ¿Qué factor representa su trabajo?', + opciones: ['Tierra', 'Trabajo', 'Capital', 'Tecnología'], + respuestaCorrecta: 'Trabajo', + explicacion: 'El esfuerzo físico e intelectual de las personas es el factor trabajo' + }, + { + id: 'p3', + pregunta: 'Una empresa compra maquinaria para fabricar muebles. ¿Qué factor es la maquinaria?', + opciones: ['Tierra', 'Trabajo', 'Capital', 'Tecnología'], + respuestaCorrecta: 'Capital', + explicacion: 'El capital son los bienes producidos para producir otros bienes' + }, + { + id: 'p4', + pregunta: 'Un trabajador recibe su mensualidad. ¿Cómo se llama esta remuneración?', + opciones: ['Renta', 'Salario', 'Interés', 'Beneficio'], + respuestaCorrecta: 'Salario', + explicacion: 'El salario es la remuneración del factor trabajo' + }, + { + id: 'p5', + pregunta: 'Una empresa obtiene ganancias por su actividad. ¿Cómo se llama esta remuneración?', + opciones: ['Renta', 'Salario', 'Interés', 'Beneficio'], + respuestaCorrecta: 'Beneficio', + explicacion: 'El beneficio es la remuneración del factor empresa/tecnología' + }, + { + id: 'p6', + pregunta: 'Un propietario alquila un edificio de oficinas. ¿Qué remuneración recibe?', + opciones: ['Renta', 'Salario', 'Interés', 'Beneficio'], + respuestaCorrecta: 'Renta', + explicacion: 'La renta es la remuneración del factor tierra (recursos naturales)' + }, + { + id: 'p7', + pregunta: 'Un banco paga intereses a los ahorradores. ¿Qué factor se está remunerando?', + opciones: ['Tierra', 'Trabajo', 'Capital', 'Tecnología'], + respuestaCorrecta: 'Capital', + explicacion: 'El interés es la remuneración del capital (recursos financieros)' + }, + { + id: 'p8', + pregunta: 'Un emprendedor desarrolla un nuevo producto. ¿Qué factor está usando?', + opciones: ['Tierra', 'Trabajo', 'Capital', 'Tecnología/Empresa'], + respuestaCorrecta: 'Tecnología/Empresa', + explicacion: 'La capacidad empresarial y la innovación representan el factor tecnología' + } + ], + modo: 'seleccion-unica', + configuracionVisual: { + mostrarBarraProgreso: true, + mostrarPuntaje: true, + retroalimentacionInmediata: true, + tiempoLimite: 480, + permitirReintentar: true + } + }, + pistas: [ + 'Tierra → recursos naturales → RENTA', + 'Trabajo → esfuerzo humano → SALARIO', + 'Capital → bienes de producción → INTERÉS', + 'Tecnología/Empresa → innovación → BENEFICIO' + ], + solucion: `Factores de producción y remuneraciones: +- **Tierra** (recursos naturales) → **Renta** +- **Trabajo** (esfuerzo humano) → **Salario** +- **Capital** (bienes para producir) → **Interés** +- **Tecnología/Empresa** (innovación) → **Beneficio**` +}, + +// ============================================ +// EJERCICIO 13: Calculadora - Productividad +// ============================================ +{ + id: 'calculadora-productividad', + tipo: 'calculadora', + titulo: 'Calculadora: Cálculo de Productividad', + descripcion: 'Calcula la productividad laboral usando la fórmula: Productividad = Output / Input', + instrucciones: `La productividad mide la eficiencia con la que se usan los recursos para producir. + +**Fórmula**: Productividad = Output (producción) / Input (recursos utilizados) + +Ejemplos de productividad laboral: +- Productos por hora de trabajo +- Ventas por empleado +- Unidades producidas por trabajador + +Calcula la productividad en cada escenario.`, + dificultad: 'medio', + duracionEstimada: 12, + objetivosAprendizaje: [ + 'Comprender el concepto de productividad', + 'Aplicar la fórmula de productividad', + 'Interpretar resultados de productividad', + 'Relacionar productividad con eficiencia económica' + ], + config: { + formula: 'Productividad = Output / Input', + variables: [ + { nombre: 'Producción total', simbolo: 'Q', unidad: 'unidades' }, + { nombre: 'Horas de trabajo', simbolo: 'L', unidad: 'horas' }, + { nombre: 'Número de trabajadores', simbolo: 'N', unidad: 'trabajadores' }, + { nombre: 'Capital invertido', simbolo: 'K', unidad: 'euros' } + ], + permiteDecimales: true, + pasos: [ + '1. Identificar el output (producción total)', + '2. Identificar el input (recurso usado)', + '3. Dividir output entre input', + '4. Interpretar el resultado' + ], + preguntas: [ + { + id: 'calc1', + pregunta: 'Una fábrica produce 500 unidades en 10 horas de trabajo. ¿Cuál es la productividad por hora?', + output: 500, + input: 10, + unidadOutput: 'unidades', + unidadInput: 'horas', + resultadoEsperado: 50, + explicacion: 'Productividad = 500 / 10 = 50 unidades por hora' + }, + { + id: 'calc2', + pregunta: 'Un empleado vende 2,000 euros en productos en una jornada de 8 horas. ¿Cuál es su productividad por hora?', + output: 2000, + input: 8, + unidadOutput: 'euros', + unidadInput: 'horas', + resultadoEsperado: 250, + explicacion: 'Productividad = 2000 / 8 = 250 euros/hora' + }, + { + id: 'calc3', + pregunta: 'Una empresa produce 10,000 prendas con 25 trabajadores en una semana. ¿Cuál es la productividad por trabajador?', + output: 10000, + input: 25, + unidadOutput: 'prendas', + unidadInput: 'trabajadores', + resultadoEsperado: 400, + explicacion: 'Productividad = 10000 / 25 = 400 prendas/trabajador' + }, + { + id: 'calc4', + pregunta: 'Una mina extrae 800 toneladas de carbón con 40 mineros en un día. ¿Cuál es la productividad por minero?', + output: 800, + input: 40, + unidadOutput: 'toneladas', + unidadInput: 'mineros', + resultadoEsperado: 20, + explicacion: 'Productividad = 800 / 40 = 20 toneladas/minero' + }, + { + id: 'calc5', + pregunta: 'Un restaurante sirve 360 comidas con 6 cocineros en un turno. ¿Cuál es la productividad por cocinero?', + output: 360, + input: 6, + unidadOutput: 'comidas', + unidadInput: 'cocineros', + resultadoEsperado: 60, + explicacion: 'Productividad = 360 / 6 = 60 comidas/cocinero' + } + ] + }, + pistas: [ + 'Productividad = Cantidad producida / Recursos utilizados', + 'El resultado siempre tiene unidades: output por cada unidad de input', + 'Mayor productividad = mayor eficiencia' + ], + solucion: `La productividad mide la eficiencia: +- **Fórmula**: Output / Input +- **Unidades**: unidades de output por unidad de input +- **Mayor productividad** = más eficiente +- **Para mejorarla**: aumentar output o reducir input` + } +]}; + +// 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 const ejercicioMicroMacro = ejercicios.ejercicios[3]; +export const ejercicioProblemaEconomico = ejercicios.ejercicios[4]; +export const ejercicioEscasezRecursos = ejercicios.ejercicios[5]; +export const ejercicioEconomiaPositivaNormativa = ejercicios.ejercicios[6]; +export const ejercicioSistemasEconomicos = ejercicios.ejercicios[7]; +export const ejercicioConstructorFPP = ejercicios.ejercicios[8]; +export const ejercicioAgentesEconomicos = ejercicios.ejercicios[9]; +export const ejercicioRolesAgentes = ejercicios.ejercicios[10]; +export const ejercicioFactoresProduccion = ejercicios.ejercicios[11]; +export const ejercicioProductividad = ejercicios.ejercicios[12]; + +export default ejercicios; diff --git a/frontend/src/content/modulo1/factores.ts b/frontend/src/content/modulo1/factores.ts new file mode 100644 index 0000000..30f9cac --- /dev/null +++ b/frontend/src/content/modulo1/factores.ts @@ -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; diff --git a/frontend/src/content/modulo1/index.ts b/frontend/src/content/modulo1/index.ts new file mode 100644 index 0000000..095135f --- /dev/null +++ b/frontend/src/content/modulo1/index.ts @@ -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; diff --git a/frontend/src/content/modulo1/introduccion.ts b/frontend/src/content/modulo1/introduccion.ts new file mode 100644 index 0000000..f7dc9f9 --- /dev/null +++ b/frontend/src/content/modulo1/introduccion.ts @@ -0,0 +1,81 @@ +export interface Seccion { + titulo: string; + contenido: string; +} + +export interface Ejercicio { + id: string; + tipo: 'slider' | 'quiz' | 'juego' | 'calculadora' | 'matching' | 'interactive'; + titulo: string; + descripcion: string; + config: Record; +} + +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; diff --git a/frontend/src/content/modulo2/demanda.ts b/frontend/src/content/modulo2/demanda.ts new file mode 100644 index 0000000..6ab499c --- /dev/null +++ b/frontend/src/content/modulo2/demanda.ts @@ -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 +}; diff --git a/frontend/src/content/modulo2/ejercicios.ts b/frontend/src/content/modulo2/ejercicios.ts new file mode 100644 index 0000000..1e06202 --- /dev/null +++ b/frontend/src/content/modulo2/ejercicios.ts @@ -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 + } +}; diff --git a/frontend/src/content/modulo2/equilibrio.ts b/frontend/src/content/modulo2/equilibrio.ts new file mode 100644 index 0000000..9fa66b1 --- /dev/null +++ b/frontend/src/content/modulo2/equilibrio.ts @@ -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 +}; diff --git a/frontend/src/content/modulo2/oferta.ts b/frontend/src/content/modulo2/oferta.ts new file mode 100644 index 0000000..7e67074 --- /dev/null +++ b/frontend/src/content/modulo2/oferta.ts @@ -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 +}; diff --git a/frontend/src/content/modulo3/clasificacion.ts b/frontend/src/content/modulo3/clasificacion.ts new file mode 100644 index 0000000..7540821 --- /dev/null +++ b/frontend/src/content/modulo3/clasificacion.ts @@ -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; diff --git a/frontend/src/content/modulo3/conceptos.ts b/frontend/src/content/modulo3/conceptos.ts new file mode 100644 index 0000000..c683823 --- /dev/null +++ b/frontend/src/content/modulo3/conceptos.ts @@ -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; diff --git a/frontend/src/content/modulo3/ejercicios.ts b/frontend/src/content/modulo3/ejercicios.ts new file mode 100644 index 0000000..a85fa24 --- /dev/null +++ b/frontend/src/content/modulo3/ejercicios.ts @@ -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; + 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 +}; diff --git a/frontend/src/content/modulo3/tipos.ts b/frontend/src/content/modulo3/tipos.ts new file mode 100644 index 0000000..fc5166a --- /dev/null +++ b/frontend/src/content/modulo3/tipos.ts @@ -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; diff --git a/frontend/src/content/modulo4/costos.ts b/frontend/src/content/modulo4/costos.ts new file mode 100644 index 0000000..a5407d8 --- /dev/null +++ b/frontend/src/content/modulo4/costos.ts @@ -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; +} + +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; diff --git a/frontend/src/content/modulo4/ejercicios.ts b/frontend/src/content/modulo4/ejercicios.ts new file mode 100644 index 0000000..7284195 --- /dev/null +++ b/frontend/src/content/modulo4/ejercicios.ts @@ -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; + solucion?: Record; +} + +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; diff --git a/frontend/src/content/modulo4/mercado.ts b/frontend/src/content/modulo4/mercado.ts new file mode 100644 index 0000000..9f764d6 --- /dev/null +++ b/frontend/src/content/modulo4/mercado.ts @@ -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; +} + +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; diff --git a/frontend/src/content/modulo4/produccion.ts b/frontend/src/content/modulo4/produccion.ts new file mode 100644 index 0000000..a6617f8 --- /dev/null +++ b/frontend/src/content/modulo4/produccion.ts @@ -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; +} + +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; diff --git a/frontend/src/hooks/useEjercicioProgreso.ts b/frontend/src/hooks/useEjercicioProgreso.ts new file mode 100644 index 0000000..bce4a78 --- /dev/null +++ b/frontend/src/hooks/useEjercicioProgreso.ts @@ -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; + 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(null); + const [puntuacionAnterior, setPuntuacionAnterior] = useState(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; diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..a69a6c6 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,54 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +@layer utilities { + .text-primary { + @apply text-blue-600; + } + .bg-primary { + @apply bg-blue-600; + } + .hover\:bg-primary:hover { + @apply bg-blue-700; + } + .focus\:ring-primary:focus { + --tw-ring-color: rgb(37 99 235); + } + .bg-success { + @apply bg-emerald-500; + } + .text-success { + @apply text-emerald-500; + } + .bg-secondary { + @apply bg-violet-600; + } + .text-secondary { + @apply text-violet-600; + } + .bg-warning { + @apply bg-amber-500; + } + .text-warning { + @apply text-amber-500; + } + .bg-error { + @apply bg-red-500; + } + .text-error { + @apply text-red-500; + } +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..9aa52ff --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './index.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/frontend/src/pages/ClasesGrabadas.tsx b/frontend/src/pages/ClasesGrabadas.tsx new file mode 100644 index 0000000..6913632 --- /dev/null +++ b/frontend/src/pages/ClasesGrabadas.tsx @@ -0,0 +1,224 @@ +import { useState } from 'react'; +import { Card } from '../components/ui/Card'; +import { Button } from '../components/ui/Button'; +import { + Headphones, + Download, + Play, + Clock, + BookOpen, + Calendar, + ArrowLeft +} from 'lucide-react'; +import { Link } from 'react-router-dom'; + +interface Clase { + id: number; + titulo: string; + modulo: string; + duracion: string; + fecha: string; + descripcion: string; + archivo: string; +} + +const CLASES: Clase[] = [ + { + id: 1, + titulo: 'Clase 1: Fundamentos de Economía', + modulo: 'Módulo 1', + duracion: '63 minutos', + fecha: '27 de Enero, 2025', + descripcion: 'Introducción a la economía, el problema económico fundamental, sistemas económicos, frontera de posibilidades de producción, agentes económicos y factores de producción.', + archivo: '/audios/clase1_completa.m4a' + }, + { + id: 2, + titulo: 'Clase 2: Oferta, Demanda y Equilibrio', + modulo: 'Módulo 2', + duracion: '103 minutos', + fecha: '30 de Enero, 2025', + descripcion: 'Ley de la demanda, ley de la oferta, equilibrio de mercado, elasticidad de la demanda y controles de precio. Análisis completo del funcionamiento de los mercados.', + archivo: '/audios/clase2_completa.m4a' + }, + { + id: 3, + titulo: 'Clase 3: Elasticidad y Teoría del Consumidor', + modulo: 'Módulo 3', + duracion: '52 minutos', + fecha: '3 de Febrero, 2025', + descripcion: 'Elasticidad precio, ingreso y cruzada. Utilidad total y marginal, restricción presupuestaria y maximización de la satisfacción del consumidor.', + archivo: '/audios/clase3_completa.m4a' + }, + { + id: 4, + titulo: 'Clase 4: Teoría del Productor', + modulo: 'Módulo 4', + duracion: '46 minutos', + fecha: '6 de Febrero, 2025', + descripcion: 'Función de producción, ley de rendimientos decrecientes, costos a corto y largo plazo, ingresos y maximización de beneficios en competencia perfecta.', + archivo: '/audios/clase4_completa.m4a' + } +]; + +export function ClasesGrabadasPage() { + const [claseReproduciendo, setClaseReproduciendo] = useState(null); + + return ( +
+
+ {/* Header */} +
+ + + Volver al Dashboard + + +
+
+ +
+
+

Clases Grabadas

+

+ Escucha las clases completas en audio o descárgalas +

+
+
+
+ + {/* Info Banner */} + +
+
+ +
+
+

+ ¿Cómo usar las clases grabadas? +

+
    +
  • + + Cada clase corresponde a un módulo del curso +
  • +
  • + + Puedes escuchar directamente en la web o descargar para escuchar offline +
  • +
  • + + Te recomendamos escuchar la clase antes de hacer los ejercicios del módulo +
  • +
  • + + Total: {CLASES.length} clases · Duración total aproximada: 4.5 horas +
  • +
+
+
+
+ + {/* Lista de Clases */} +
+ {CLASES.map((clase) => ( + +
+ {/* Icono/Info */} +
+
+ {clase.id} +
+ +
+ + {clase.modulo} + + +

+ {clase.titulo} +

+ +
+ + + {clase.duracion} + + + + {clase.fecha} + +
+
+
+ + {/* Descripción */} +
+

+ {clase.descripcion} +

+
+ + {/* Acciones */} +
+ + + + + +
+
+ + {/* Reproductor de Audio */} + {claseReproduciendo === clase.id && ( +
+ +

+ 💡 Tip: Puedes descargar el audio para escucharlo sin conexión +

+
+ )} +
+ ))} +
+ + {/* Footer */} +
+

+ ¿Ya escuchaste las clases? Pasa a los ejercicios interactivos para practicar. +

+ + + +
+
+
+ ); +} + +export default ClasesGrabadasPage; diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx new file mode 100644 index 0000000..b0567f2 --- /dev/null +++ b/frontend/src/pages/Dashboard.tsx @@ -0,0 +1,281 @@ +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 { 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, Headphones } from 'lucide-react'; +import { SistemaAnuncios } from '../components/announcements/SistemaAnuncios'; + +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 { + puntuacionTotal, + nivel, + calcularPorcentajeModulo, + getBadgesDesbloqueados, + getBadgesBloqueados, + modulos, + loadProgreso, + isLoading, + error, + } = useProgressStore(); + + useEffect(() => { + loadProgreso(); + }, [loadProgreso]); + + const handleLogout = async () => { + await logout(); + }; + + if (isLoading && Object.keys(modulos).length === 0) { + return ( +
+
+ +

Cargando tu progreso...

+
+
+ ); + } + + if (error) { + return ( +
+
+
+ + + +
+

Error al cargar el progreso

+

{error}

+ +
+
+ ); + } + + // Calcular progreso total + const totalProgreso = Math.round( + 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 ( +
+
+
+
+
+ +
+

Economía Interactiva

+
+ +
+
+ + {usuario?.nombre || 'Usuario'} + + {nivel} + + {usuario?.rol === 'admin' && ( + + Admin + + )} +
+ +
+
+
+ +
+
+

Tu progreso

+

Continúa donde lo dejaste y desbloquea nuevos logros

+
+ + {/* Sistema de Anuncios */} + + + {/* Stats Cards */} +
+ +
+
+

Progreso total

+

{totalProgreso}%

+
+ +
+
+
+
+

+ {totalProgreso === 100 ? '¡Has completado todos los módulos!' : 'Sigue así, vas por buen camino'} +

+ + + +
+
+

Puntuación total

+

{puntuacionTotal.toLocaleString()}

+
+ +
+

+ Acumula puntos completando ejercicios para subir de nivel +

+
+ + +
+
+

Logros

+

+ {badgesDesbloqueados.length}/{badgesDesbloqueados.length + badgesBloqueados.length} +

+
+ +
+

+ {badgesBloqueados.length === 0 + ? '¡Todos los logros desbloqueados!' + : `${badgesBloqueados.length} logros por desbloquear`} +

+
+
+ +
+ {/* Columna izquierda - Módulos */} +
+ {/* Puntuación y Nivel */} + + +
+

Módulos

+ {usuario?.rol === 'admin' && ( + + + + )} +
+ +
+ {MODULOS_CONFIG.map((modulo) => { + const porcentaje = calcularPorcentajeModulo(modulo.id, modulo.totalEjercicios); + const completados = getEjerciciosCompletados(modulo.id); + + return ( + + +
+
+
+ {modulo.numero} +
+
+

{modulo.titulo}

+

+ {completados}/{modulo.totalEjercicios} ejercicios +

+
+
+
+ + + +
+ {porcentaje}% completado + {porcentaje === 100 && ( + + + Completado + + )} +
+
+ + ); + })} +
+ +
+ + + + + + + + + +
+
+ + {/* Columna derecha - Logros */} +
+ +
+
+
+
+ ); +} + +export default Dashboard; diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx new file mode 100644 index 0000000..4f08a58 --- /dev/null +++ b/frontend/src/pages/Login.tsx @@ -0,0 +1,34 @@ +import { Navigate } from 'react-router-dom'; +import { useAuthStore } from '../stores/authStore'; +import { LoginForm } from '../components/auth/LoginForm'; +import { BookOpen } from 'lucide-react'; + +export function Login() { + const { isAuthenticated } = useAuthStore(); + + if (isAuthenticated) { + return ; + } + + return ( +
+
+
+
+ +
+

Plataforma de Economía

+

Inicia sesión para continuar

+
+ +
+ +
+ +

+ Sistema de aprendizaje interactivo +

+
+
+ ); +} diff --git a/frontend/src/pages/Modulo.tsx b/frontend/src/pages/Modulo.tsx new file mode 100644 index 0000000..e98b5c0 --- /dev/null +++ b/frontend/src/pages/Modulo.tsx @@ -0,0 +1,558 @@ +// @ts-nocheck +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 { 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'; + +// 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 { DefinicionEconomiaQuiz } from '../components/exercises/modulo1/DefinicionEconomiaQuiz'; +import { EscasezSimulator } from '../components/exercises/modulo1/EscasezSimulator'; +import { ProblemaEconomicoFundamental } from '../components/exercises/modulo1/ProblemaEconomicoFundamental'; +import { EconomiaPositivaVsNormativa } from '../components/exercises/modulo1/EconomiaPositivaVsNormativa'; +import { RazonamientoEconomico } from '../components/exercises/modulo1/RazonamientoEconomico'; +import { SistemasEconomicosQuiz } from '../components/exercises/modulo1/SistemasEconomicosQuiz'; +import { ComparativaSistemas } from '../components/exercises/modulo1/ComparativaSistemas'; +import { CasosPaises } from '../components/exercises/modulo1/CasosPaises'; +import { VentajasDesventajasSistemas } from '../components/exercises/modulo1/VentajasDesventajasSistemas'; +import { FPPConstructor } from '../components/exercises/modulo1/FPPConstructor'; +import { FPPAnalizador } from '../components/exercises/modulo1/FPPAnalizador'; +import { CostoOportunidadCalculator } from '../components/exercises/modulo1/CostoOportunidadCalculator'; +import { CrecimientoEconomicoFPP } from '../components/exercises/modulo1/CrecimientoEconomicoFPP'; +import { AgentesEconomicosQuiz } from '../components/exercises/modulo1/AgentesEconomicosQuiz'; +import { RolesAgentesMatching } from '../components/exercises/modulo1/RolesAgentesMatching'; +import { FlujoCircularBasico } from '../components/exercises/modulo1/FlujoCircularBasico'; +import { FactoresProduccionQuiz } from '../components/exercises/modulo1/FactoresProduccionQuiz'; +import { ProductividadCalculator } from '../components/exercises/modulo1/ProductividadCalculator'; +import { CostoOportunidadCotidiano } from '../components/exercises/modulo1/CostoOportunidadCotidiano'; +import { VentajaComparativaCalculator } from '../components/exercises/modulo1/VentajaComparativaCalculator'; +// Imports Módulo 2 - Oferta, Demanda y Equilibrio +import { ConstructorCurvas } from '../components/exercises/modulo2/ConstructorCurvas'; +import { IdentificarShocks } from '../components/exercises/modulo2/IdentificarShocks'; +import { SimuladorPrecios } from '../components/exercises/modulo2/SimuladorPrecios'; +import { LeyDemandaQuiz } from '../components/exercises/modulo2/LeyDemandaQuiz'; +import { CurvaDemandaConstructor } from '../components/exercises/modulo2/CurvaDemandaConstructor'; +import { TablaDemanda } from '../components/exercises/modulo2/TablaDemanda'; +import { DemandaIndividualVsMercado } from '../components/exercises/modulo2/DemandaIndividualVsMercado'; +import { DesplazamientoVsMovimiento } from '../components/exercises/modulo2/DesplazamientoVsMovimiento'; +import { FactoresDesplazanDemanda } from '../components/exercises/modulo2/FactoresDesplazanDemanda'; +import { LeyOfertaQuiz } from '../components/exercises/modulo2/LeyOfertaQuiz'; +import { CurvaOfertaConstructor } from '../components/exercises/modulo2/CurvaOfertaConstructor'; +import { TablaOferta } from '../components/exercises/modulo2/TablaOferta'; +import { FactoresDesplazanOferta } from '../components/exercises/modulo2/FactoresDesplazanOferta'; +import { OfertaCortoLargoPlazo } from '../components/exercises/modulo2/OfertaCortoLargoPlazo'; +import { EquilibrioFinder } from '../components/exercises/modulo2/EquilibrioFinder'; +import { EquilibrioGrafico } from '../components/exercises/modulo2/EquilibrioGrafico'; +import { AjusteEquilibrio } from '../components/exercises/modulo2/AjusteEquilibrio'; +import { ExcesoDemandaEscasez } from '../components/exercises/modulo2/ExcesoDemandaEscasez'; +import { ExcesoOfertaSuperavit } from '../components/exercises/modulo2/ExcesoOfertaSuperavit'; +import { CalculoElasticidadPrecio } from '../components/exercises/modulo2/CalculoElasticidadPrecio'; +import { ElasticidadElasticaInelastica } from '../components/exercises/modulo2/ElasticidadElasticaInelastica'; +import { FactoresElasticidad } from '../components/exercises/modulo2/FactoresElasticidad'; +import { ElasticidadIngresoTotal } from '../components/exercises/modulo2/ElasticidadIngresoTotal'; +import { PrecioMaximoTecho } from '../components/exercises/modulo2/PrecioMaximoTecho'; +import { PrecioMinimoPiso } from '../components/exercises/modulo2/PrecioMinimoPiso'; +import { SimuladorControles } from '../components/exercises/modulo2/SimuladorControles'; +import { ControlesVidaReal } from '../components/exercises/modulo2/ControlesVidaReal'; +import { CambiosEquilibrio } from '../components/exercises/modulo2/CambiosEquilibrio'; + +// Imports Módulo 3 - Utilidad y Elasticidad +import { ClasificadorBienes } from '../components/exercises/modulo3/ClasificadorBienes'; +import { CalculadoraElasticidad } from '../components/exercises/modulo3/CalculadoraElasticidad'; +import { EjerciciosExamen } from '../components/exercises/modulo3/EjerciciosExamen'; +import { FormulaElasticidad } from '../components/exercises/modulo3/FormulaElasticidad'; +import { MetodoPuntoMedio } from '../components/exercises/modulo3/MetodoPuntoMedio'; +import { ElasticidadCurva } from '../components/exercises/modulo3/ElasticidadCurva'; +import { ElasticidadRectas } from '../components/exercises/modulo3/ElasticidadRectas'; +import { ClasificacionElasticidad } from '../components/exercises/modulo3/ClasificacionElasticidad'; + +import { FormulaElasticidadIngreso } from '../components/exercises/modulo3/FormulaElasticidadIngreso'; +import { BienesNormalesInferiores } from '../components/exercises/modulo3/BienesNormalesInferiores'; +import { BienesLujoNecesarios } from '../components/exercises/modulo3/BienesLujoNecesarios'; +import { CurvaEngel } from '../components/exercises/modulo3/CurvaEngel'; +import { FormulaElasticidadCruzada } from '../components/exercises/modulo3/FormulaElasticidadCruzada'; +import { SustitutosComplementarios } from '../components/exercises/modulo3/SustitutosComplementarios'; +import { GradoRelacion } from '../components/exercises/modulo3/GradoRelacion'; +import { UtilidadTotalVsMarginal } from '../components/exercises/modulo3/UtilidadTotalVsMarginal'; +import { LeyUtilidadMarginalDecreciente } from '../components/exercises/modulo3/LeyUtilidadMarginalDecreciente'; +import { MaximizacionUtilidad } from '../components/exercises/modulo3/MaximizacionUtilidad'; +import { CurvasIndiferencia } from '../components/exercises/modulo3/CurvasIndiferencia'; +import { CanastaOptima } from '../components/exercises/modulo3/CanastaOptima'; +import { DecisionesPrecios } from '../components/exercises/modulo3/DecisionesPrecios'; +import { ParadojaAguaDiamantes } from '../components/exercises/modulo3/ParadojaAguaDiamantes'; + +// Imports Módulo 4 - Teoría del Productor +import { CalculadoraCostos } from '../components/exercises/modulo4/CalculadoraCostos'; +import { SimuladorProduccion } from '../components/exercises/modulo4/SimuladorProduccion'; +import { VisualizadorExcedentes } from '../components/exercises/modulo4/VisualizadorExcedentes'; +import { FuncionProduccion } from '../components/exercises/modulo4/FuncionProduccion'; +import { CortoVsLargoPlazo } from '../components/exercises/modulo4/CortoVsLargoPlazo'; +import { ProductoTotal } from '../components/exercises/modulo4/ProductoTotal'; +import { ProductoMedio } from '../components/exercises/modulo4/ProductoMedio'; +import { ProductoMarginal } from '../components/exercises/modulo4/ProductoMarginal'; +import { LeyRendimientosDecrecientes } from '../components/exercises/modulo4/LeyRendimientosDecrecientes'; +import { EtapasProduccion } from '../components/exercises/modulo4/EtapasProduccion'; +import { ProductorRacional } from '../components/exercises/modulo4/ProductorRacional'; +import { CostosFijosVsVariables } from '../components/exercises/modulo4/CostosFijosVsVariables'; +import { TablaCostos } from '../components/exercises/modulo4/TablaCostos'; +import { CurvasCosto } from '../components/exercises/modulo4/CurvasCosto'; +import { CostoTotalMedioMarginal } from '../components/exercises/modulo4/CostoTotalMedioMarginal'; +import { CostosMedios } from '../components/exercises/modulo4/CostosMedios'; +import { RelacionCMgCMe } from '../components/exercises/modulo4/RelacionCMgCMe'; +import { CurvaCostoLargoPlazo } from '../components/exercises/modulo4/CurvaCostoLargoPlazo'; +import { EconomiasEscala } from '../components/exercises/modulo4/EconomiasEscala'; +import { DiseconomiasEscala } from '../components/exercises/modulo4/DiseconomiasEscala'; +import { IngresoTotal } from '../components/exercises/modulo4/IngresoTotal'; +import { IngresoMarginal } from '../components/exercises/modulo4/IngresoMarginal'; +import { IngresoCompetenciaPerfecta } from '../components/exercises/modulo4/IngresoCompetenciaPerfecta'; +import { ReglaImgCmg } from '../components/exercises/modulo4/ReglaImgCmg'; +import { PuntoCierreEquilibrio } from '../components/exercises/modulo4/PuntoCierreEquilibrio'; + +const MODULOS_INFO: Record = { + 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_POR_MODULO: Record void }>; +}>> = { + 1: [ + { id: 'definicion-economia-quiz', titulo: 'Definición de Economía', descripcion: 'Aprende qué es la economía y sus objetivos principales', componente: DefinicionEconomiaQuiz }, + { id: 'escasez-simulator', titulo: 'Simulador de Escasez', descripcion: 'Comprende el concepto de escasez y sus implicaciones', componente: EscasezSimulator }, + { id: 'problema-economico-fundamental', titulo: 'Problema Económico Fundamental', descripcion: 'Explora las preguntas básicas de toda economía', componente: ProblemaEconomicoFundamental }, + { id: 'economia-positiva-vs-normativa', titulo: 'Economía Positiva vs Normativa', descripcion: 'Diferencia entre análisis descriptivo y prescriptivo', componente: EconomiaPositivaVsNormativa }, + { id: 'razonamiento-economico', titulo: 'Razonamiento Económico', descripcion: 'Desarrolla el pensamiento económico lógico', componente: RazonamientoEconomico }, + { id: 'sistemas-economicos-quiz', titulo: 'Sistemas Económicos', descripcion: 'Conoce los diferentes sistemas económicos', componente: SistemasEconomicosQuiz }, + { id: 'comparativa-sistemas', titulo: 'Comparativa de Sistemas', descripcion: 'Compara características de distintos sistemas', componente: ComparativaSistemas }, + { id: 'casos-paises', titulo: 'Casos de Países', descripcion: 'Analiza ejemplos reales de países', componente: CasosPaises }, + { id: 'ventajas-desventajas-sistemas', titulo: 'Ventajas y Desventajas', descripcion: 'Evalúa pros y contras de cada sistema', componente: VentajasDesventajasSistemas }, + { id: 'fpp-constructor', titulo: 'Constructor de FPP', descripcion: 'Construye la Frontera de Posibilidades de Producción', componente: FPPConstructor }, + { id: 'fpp-analizador', titulo: 'Analizador de FPP', descripcion: 'Analiza puntos en la frontera de posibilidades', componente: FPPAnalizador }, + { id: 'costo-oportunidad-calculator', titulo: 'Calculadora de Costo de Oportunidad', descripcion: 'Calcula costos de oportunidad en la FPP', componente: CostoOportunidadCalculator }, + { id: 'crecimiento-economico-fpp', titulo: 'Crecimiento Económico y FPP', descripcion: 'Observa cómo crece la FPP con el desarrollo', componente: CrecimientoEconomicoFPP }, + { id: 'agentes-economicos-quiz', titulo: 'Agentes Económicos', descripcion: 'Identifica hogares, empresas y gobierno', componente: AgentesEconomicosQuiz }, + { id: 'roles-agentes-matching', titulo: 'Roles de Agentes', descripcion: 'Relaciona agentes con sus funciones', componente: RolesAgentesMatching }, + { id: 'flujo-circular-basico', titulo: 'Flujo Circular Básico', descripcion: 'Comprende el flujo real y monetario', componente: FlujoCircularBasico }, + { id: 'factores-produccion-quiz', titulo: 'Factores de Producción', descripcion: 'Tierra, trabajo, capital y tecnología', componente: FactoresProduccionQuiz }, + { id: 'productividad-calculator', titulo: 'Calculadora de Productividad', descripcion: 'Calcula la eficiencia productiva', componente: ProductividadCalculator }, + { id: 'costo-oportunidad-cotidiano', titulo: 'Costo de Oportunidad Cotidiano', descripcion: 'Encuentra costos de oportunidad en tu vida', componente: CostoOportunidadCotidiano }, + { id: 'ventaja-comparativa-calculator', titulo: 'Ventaja Comparativa', descripcion: 'Calcula ventajas comparativas entre países', componente: VentajaComparativaCalculator }, + { 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: 'ley-demanda-quiz', titulo: 'Ley de la Demanda', descripcion: 'Comprende la relación inversa entre precio y cantidad demandada', componente: LeyDemandaQuiz }, + { id: 'curva-demanda-constructor', titulo: 'Constructor de Curva de Demanda', descripcion: 'Construye la curva de demanda paso a paso', componente: CurvaDemandaConstructor }, + { id: 'tabla-demanda', titulo: 'Tabla de Demanda', descripcion: 'Interpreta tablas de demanda y sus variaciones', componente: TablaDemanda }, + { id: 'demanda-individual-vs-mercado', titulo: 'Demanda Individual vs Mercado', descripcion: 'Diferencia entre demanda individual y agregada', componente: DemandaIndividualVsMercado }, + { id: 'desplazamiento-vs-movimiento', titulo: 'Desplazamiento vs Movimiento', descripcion: 'Distingue cambios en la demanda de variaciones en la cantidad', componente: DesplazamientoVsMovimiento }, + { id: 'factores-desplazan-demanda', titulo: 'Factores que Desplazan la Demanda', descripcion: 'Identifica los determinantes de la demanda', componente: FactoresDesplazanDemanda }, + { id: 'ley-oferta-quiz', titulo: 'Ley de la Oferta', descripcion: 'Comprende la relación directa entre precio y cantidad ofrecida', componente: LeyOfertaQuiz }, + { id: 'curva-oferta-constructor', titulo: 'Constructor de Curva de Oferta', descripcion: 'Construye la curva de oferta paso a paso', componente: CurvaOfertaConstructor }, + { id: 'tabla-oferta', titulo: 'Tabla de Oferta', descripcion: 'Interpreta tablas de oferta y sus variaciones', componente: TablaOferta }, + { id: 'factores-desplazan-oferta', titulo: 'Factores que Desplazan la Oferta', descripcion: 'Identifica los determinantes de la oferta', componente: FactoresDesplazanOferta }, + { id: 'oferta-corto-largo-plazo', titulo: 'Oferta a Corto vs Largo Plazo', descripcion: 'Diferencias en la elasticidad de la oferta según el tiempo', componente: OfertaCortoLargoPlazo }, + { id: 'equilibrio-finder', titulo: 'Buscador de Equilibrio', descripcion: 'Encuentra el punto de equilibrio de mercado', componente: EquilibrioFinder }, + { id: 'equilibrio-grafico', titulo: 'Equilibrio Gráfico', descripcion: 'Visualiza el equilibrio en el gráfico de oferta y demanda', componente: EquilibrioGrafico }, + { id: 'constructor-curvas', titulo: 'Constructor de Curvas', descripcion: 'Construye curvas de oferta y demanda', componente: ConstructorCurvas }, + { id: 'ajuste-equilibrio', titulo: 'Ajuste al Equilibrio', descripcion: 'Observa cómo el mercado se ajusta al equilibrio', componente: AjusteEquilibrio }, + { id: 'exceso-demanda-escasez', titulo: 'Exceso de Demanda (Escasez)', descripcion: 'Analiza situaciones de escasez en el mercado', componente: ExcesoDemandaEscasez }, + { id: 'exceso-oferta-superavit', titulo: 'Exceso de Oferta (Superávit)', descripcion: 'Analiza situaciones de superávit en el mercado', componente: ExcesoOfertaSuperavit }, + { id: 'calculo-elasticidad-precio', titulo: 'Cálculo de Elasticidad Precio', descripcion: 'Calcula la elasticidad precio de la demanda', componente: CalculoElasticidadPrecio }, + { id: 'elasticidad-elastica-inelastica', titulo: 'Elástica vs Inelástica', descripcion: 'Distingue entre demanda elástica e inelástica', componente: ElasticidadElasticaInelastica }, + { id: 'factores-elasticidad', titulo: 'Factores de la Elasticidad', descripcion: 'Identifica qué determina la elasticidad de la demanda', componente: FactoresElasticidad }, + { id: 'elasticidad-ingreso-total', titulo: 'Elasticidad e Ingreso Total', descripcion: 'Relación entre elasticidad e ingreso de los productores', componente: ElasticidadIngresoTotal }, + { id: 'precio-maximo-techo', titulo: 'Precio Máximo (Techo)', descripcion: 'Analiza el efecto de los precios máximos', componente: PrecioMaximoTecho }, + { id: 'precio-minimo-piso', titulo: 'Precio Mínimo (Piso)', descripcion: 'Analiza el efecto de los precios mínimos', componente: PrecioMinimoPiso }, + { id: 'simulador-controles', titulo: 'Simulador de Controles de Precios', descripcion: 'Simula diferentes controles de precios', componente: SimuladorControles }, + { id: 'controles-vida-real', titulo: 'Controles en la Vida Real', descripcion: 'Ejemplos reales de controles de precios', componente: ControlesVidaReal }, + { id: 'cambios-equilibrio', titulo: 'Cambios en el Equilibrio', descripcion: 'Analiza cómo cambian los shocks el equilibrio', componente: CambiosEquilibrio }, + { 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: 'formula-elasticidad', titulo: 'Fórmula de Elasticidad', descripcion: 'Aprende la fórmula de elasticidad precio', componente: FormulaElasticidad }, + { id: 'metodo-punto-medio', titulo: 'Método del Punto Medio', descripcion: 'Calcula elasticidad usando el método del punto medio', componente: MetodoPuntoMedio }, + { id: 'calculadora-elasticidad', titulo: 'Calculadora de Elasticidad', descripcion: 'Calcula elasticidades de demanda', componente: CalculadoraElasticidad }, + { id: 'elasticidad-curva', titulo: 'Elasticidad en la Curva', descripcion: 'Analiza la elasticidad en diferentes puntos de la curva', componente: ElasticidadCurva }, + { id: 'elasticidad-rectas', titulo: 'Elasticidad y Rectas', descripcion: 'Relación entre pendiente y elasticidad', componente: ElasticidadRectas }, + { id: 'clasificacion-elasticidad', titulo: 'Clasificación de Elasticidad', descripcion: 'Clasifica bienes según su elasticidad', componente: ClasificacionElasticidad }, + + { id: 'clasificador-bienes', titulo: 'Clasificador de Bienes', descripcion: 'Clasifica bienes según su elasticidad', componente: ClasificadorBienes }, + { id: 'formula-elasticidad-ingreso', titulo: 'Elasticidad Ingreso', descripcion: 'Calcula la elasticidad ingreso de la demanda', componente: FormulaElasticidadIngreso }, + { id: 'bienes-normales-inferiores', titulo: 'Bienes Normales vs Inferiores', descripcion: 'Distingue bienes normales de inferiores', componente: BienesNormalesInferiores }, + { id: 'bienes-lujo-necesarios', titulo: 'Bienes de Lujo vs Necesarios', descripcion: 'Clasifica bienes según su elasticidad ingreso', componente: BienesLujoNecesarios }, + { id: 'curva-engel', titulo: 'Curva de Engel', descripcion: 'Relación entre ingreso y consumo', componente: CurvaEngel }, + { id: 'formula-elasticidad-cruzada', titulo: 'Elasticidad Cruzada', descripcion: 'Calcula elasticidad entre bienes relacionados', componente: FormulaElasticidadCruzada }, + { id: 'sustitutos-complementarios', titulo: 'Sustitutos vs Complementarios', descripcion: 'Identifica bienes sustitutos y complementarios', componente: SustitutosComplementarios }, + { id: 'grado-relacion', titulo: 'Grado de Relación', descripcion: 'Mide la fuerza de la relación entre bienes', componente: GradoRelacion }, + { id: 'utilidad-total-vs-marginal', titulo: 'Utilidad Total vs Marginal', descripcion: 'Diferencia entre utilidad total y marginal', componente: UtilidadTotalVsMarginal }, + { id: 'ley-utilidad-marginal-decreciente', titulo: 'Utilidad Marginal Decreciente', descripcion: 'Comprende la ley de utilidad marginal decreciente', componente: LeyUtilidadMarginalDecreciente }, + { id: 'maximizacion-utilidad', titulo: 'Maximización de Utilidad', descripcion: 'Optimiza la canasta de consumo del consumidor', componente: MaximizacionUtilidad }, + { id: 'curvas-indiferencia', titulo: 'Curvas de Indiferencia', descripcion: 'Visualiza preferencias del consumidor', componente: CurvasIndiferencia }, + { id: 'canasta-optima', titulo: 'Canasta Óptima', descripcion: 'Encuentra la combinación óptima de bienes', componente: CanastaOptima }, + { id: 'decisiones-precios', titulo: 'Decisiones de Precios', descripcion: 'Toma decisiones basadas en elasticidad', componente: DecisionesPrecios }, + { id: 'paradoja-agua-diamantes', titulo: 'Paradoja del Agua y los Diamantes', descripcion: 'Resuelve la paradoja del valor', componente: ParadojaAguaDiamantes }, + { id: 'ejercicios-examen', titulo: 'Ejercicios de Examen', descripcion: 'Pon a prueba tus conocimientos', componente: EjerciciosExamen }, + ], + 4: [ + { id: 'funcion-produccion', titulo: 'Función de Producción', descripcion: 'Comprende la relación entre insumos y producto', componente: FuncionProduccion }, + { id: 'corto-vs-largo-plazo', titulo: 'Corto vs Largo Plazo', descripcion: 'Diferencias en el análisis productivo según el tiempo', componente: CortoVsLargoPlazo }, + { id: 'producto-total', titulo: 'Producto Total', descripcion: 'Analiza la producción total de la empresa', componente: ProductoTotal }, + { id: 'producto-medio', titulo: 'Producto Medio', descripcion: 'Calcula el producto por unidad de factor', componente: ProductoMedio }, + { id: 'producto-marginal', titulo: 'Producto Marginal', descripcion: 'Analiza el producto adicional de cada unidad', componente: ProductoMarginal }, + { id: 'ley-rendimientos-decrecientes', titulo: 'Rendimientos Decrecientes', descripcion: 'Comprende la ley de rendimientos decrecientes', componente: LeyRendimientosDecrecientes }, + { id: 'etapas-produccion', titulo: 'Etapas de Producción', descripcion: 'Identifica las etapas de la producción', componente: EtapasProduccion }, + { id: 'productor-racional', titulo: 'Productor Racional', descripcion: 'Determina la zona de producción racional', componente: ProductorRacional }, + { id: 'costos-fijos-vs-variables', titulo: 'Costos Fijos vs Variables', descripcion: 'Distingue entre costos fijos y variables', componente: CostosFijosVsVariables }, + { id: 'tabla-costos', titulo: 'Tabla de Costos', descripcion: 'Interpreta tablas de costos de producción', componente: TablaCostos }, + { id: 'curvas-costo', titulo: 'Curvas de Costo', descripcion: 'Visualiza las curvas de costos', componente: CurvasCosto }, + { id: 'calculadora-costos', titulo: 'Calculadora de Costos', descripcion: 'Calcula costos de producción', componente: CalculadoraCostos }, + { id: 'costo-total-medio-marginal', titulo: 'Costo Total, Medio y Marginal', descripcion: 'Relación entre los diferentes costos', componente: CostoTotalMedioMarginal }, + { id: 'costos-medios', titulo: 'Costos Medios', descripcion: 'Analiza los costos medios de producción', componente: CostosMedios }, + { id: 'relacion-cmg-cme', titulo: 'Relación CMg y CMe', descripcion: 'Relación entre costo marginal y costo medio', componente: RelacionCMgCMe }, + { id: 'curva-costo-largo-plazo', titulo: 'Curva de Costo Largo Plazo', descripcion: 'Analiza costos cuando todos los factores son variables', componente: CurvaCostoLargoPlazo }, + { id: 'economias-escala', titulo: 'Economías de Escala', descripcion: 'Ventajas de la producción a gran escala', componente: EconomiasEscala }, + { id: 'diseconomias-escala', titulo: 'Diseconomías de Escala', descripcion: 'Desventajas de la producción excesiva', componente: DiseconomiasEscala }, + { id: 'ingreso-total', titulo: 'Ingreso Total', descripcion: 'Calcula los ingresos totales de la empresa', componente: IngresoTotal }, + { id: 'ingreso-marginal', titulo: 'Ingreso Marginal', descripcion: 'Analiza el ingreso adicional por unidad vendida', componente: IngresoMarginal }, + { id: 'ingreso-competencia-perfecta', titulo: 'Ingreso en Competencia Perfecta', descripcion: 'Características del ingreso en competencia perfecta', componente: IngresoCompetenciaPerfecta }, + { id: 'regla-img-cmg', titulo: 'Regla IMg = CMg', descripcion: 'Maximización de beneficios: igualar ingreso y costo marginal', componente: ReglaImgCmg }, + { id: 'punto-cierre-equilibrio', titulo: 'Punto de Cierre y Equilibrio', descripcion: 'Determina cuándo cerrar o continuar produciendo', componente: PuntoCierreEquilibrio }, + { 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 { + puntuacionTotal, + getProgresoEjercicio, + saveProgreso, + calcularPorcentajeModulo, + loadProgreso, + isLoading, + error, + } = useProgressStore(); + + const [ejercicioActivo, setEjercicioActivo] = useState(null); + + useEffect(() => { + loadProgreso(); + }, [loadProgreso]); + + 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 { + await saveProgreso(moduloInfo.id, ejercicioId, puntuacion); + setEjercicioActivo(null); + } catch (err) { + console.error('Error al guardar progreso:', err); + } + }; + + 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; + }; + + if (ejercicioActivo) { + const ejercicio = ejercicios.find(e => e.id === ejercicioActivo); + if (!ejercicio) return null; + + const EjercicioComponent = ejercicio.componente; + + return ( +
+
+
+ +
+
+ +
+
+

{ejercicio.titulo}

+

{ejercicio.descripcion}

+
+ + handleCompleteEjercicio(ejercicio.id, puntuacion)} + /> +
+
+ ); + } + + if (isLoading) { + return ( +
+
+ +

Cargando ejercicios...

+
+
+ ); + } + + if (error) { + return ( +
+
+
+ + + +
+

Error al cargar el progreso

+

{error}

+ +
+
+ ); + } + + return ( +
+
+
+ + + Volver al Dashboard + +
+
+ +
+
+
+
+ {num} +
+
+

{moduloInfo.titulo}

+

{moduloInfo.descripcion}

+
+
+ + +
+
+

Tu progreso en este módulo

+

{porcentaje}%

+
+
+

Ejercicios

+

{completados}/{ejercicios.length}

+
+
+ +
+ +
+
+
+ +
+

Ejercicios

+ +
+ +
+ {ejercicios.map((ejercicio, index) => { + const progreso = getProgresoEjercicioLocal(ejercicio.id); + const completado = progreso?.completado || false; + const bloqueado = isEjercicioBloqueado(index); + + return ( + + !bloqueado && setEjercicioActivo(ejercicio.id)} + > +
+
+ {completado ? ( + + ) : bloqueado ? ( + + ) : ( + {index + 1} + )} +
+ +
+

+ {ejercicio.titulo} +

+

{ejercicio.descripcion}

+ {completado && progreso && progreso.puntuacion > 0 && ( +
+ + Mejor puntuación: {progreso.puntuacion} pts + + + ({progreso.intentos} {progreso.intentos === 1 ? 'intento' : 'intentos'}) + +
+ )} +
+ + +
+
+
+ ); + })} +
+ + {porcentaje === 100 && ( + + +
+
+ +
+
+

¡Felicitaciones!

+

+ Has completado todos los ejercicios de este módulo. + {num < 4 ? ' ¡Continúa con el siguiente módulo!' : ' ¡Has completado todos los módulos!'} +

+
+ {num < 4 && ( + + + + )} +
+
+
+ )} + +
+
+ ); +} + +export default Modulo; diff --git a/frontend/src/pages/Modulos.tsx b/frontend/src/pages/Modulos.tsx new file mode 100644 index 0000000..7150d0c --- /dev/null +++ b/frontend/src/pages/Modulos.tsx @@ -0,0 +1,161 @@ +import { Link } from 'react-router-dom'; +import { Card } from '../components/ui/Card'; +import { Button } from '../components/ui/Button'; +import { useProgresoStore } from '../stores/progresoStore'; +import { ArrowRight, ArrowLeft, CheckCircle, Lock, Play } from 'lucide-react'; + +const MODULOS = [ + { + numero: 1, + 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 ( +
+
+
+ + + Volver al Dashboard + +
+
+ +
+
+

Módulos Educativos

+

+ Explora los 4 módulos de economía. Cada uno contiene ejercicios interactivos + para fortalecer tu comprensión de los conceptos. +

+
+ +
+ {MODULOS.map((modulo) => { + const { completados, porcentaje } = getModuloProgress(modulo.numero, modulo.totalEjercicios); + const estaCompletado = porcentaje === 100; + + return ( + +
+
+
+ {modulo.numero} +
+
+ +
+
+

{modulo.titulo}

+ {estaCompletado && ( + + + Completado + + )} +
+

{modulo.descripcion}

+ +
+ {modulo.temas.map((tema) => ( + + {tema} + + ))} +
+ +
+
+
+

+ {completados}/{modulo.totalEjercicios} ejercicios completados ({porcentaje}%) +

+
+ +
+ {modulo.bloqueado ? ( + + ) : ( + + + + )} +
+
+ + ); + })} +
+
+
+ ); +} diff --git a/frontend/src/pages/Recursos.tsx b/frontend/src/pages/Recursos.tsx new file mode 100644 index 0000000..4a49746 --- /dev/null +++ b/frontend/src/pages/Recursos.tsx @@ -0,0 +1,212 @@ +import { useState } from 'react'; +import { Card } from '../components/ui/Card'; +import { Button } from '../components/ui/Button'; +import { FileText, Download, BookOpen, ArrowLeft, X, Eye } 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() { + const [pdfSeleccionado, setPdfSeleccionado] = useState(null); + const [pdfTitulo, setPdfTitulo] = useState(''); + + const abrirPdf = (archivo: string, titulo: string) => { + setPdfSeleccionado(archivo); + setPdfTitulo(titulo); + }; + + const cerrarPdf = () => { + setPdfSeleccionado(null); + setPdfTitulo(''); + }; + + return ( +
+
+ {/* Header */} +
+ + + Volver al Dashboard + + +

Recursos de Estudio

+

+ Material académico en PDF para consultar offline +

+
+ + {/* Info Card */} + +
+
+ +
+
+

Material de Apoyo

+

+ Estos documentos PDF contienen el contenido teórico de cada módulo. + Úsalos como referencia mientras realizas los ejercicios interactivos. +

+
+
+
+ + {/* Recursos Grid */} +
+ {recursos.map((recurso) => ( + +
+
+ +
+ +
+
+ + {recurso.modulo} + +
+ +

+ {recurso.titulo} +

+ +

+ {recurso.descripcion} +

+ +
+ + + + + +
+
+
+
+ ))} +
+ + {/* Footer */} +
+

+ ¿Tienes dudas sobre el contenido? Revisa los ejercicios interactivos en cada módulo. +

+ + + +
+
+ + {/* Modal para visualizar PDF */} + {pdfSeleccionado && ( +
+
+ {/* Header del modal */} +
+

+ {pdfTitulo} +

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