diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 184fa6b..f26a648 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -103,8 +103,7 @@ func main() { progreso := protected.Group("/progreso") { progreso.GET("", progresoHandler.GetProgreso) - progreso.GET("/modulo/:numero", progresoHandler.GetProgresoModulo) - progreso.PUT("/:ejercicioId", progresoHandler.UpdateProgreso) + progreso.POST("", progresoHandler.SaveProgreso) progreso.GET("/resumen", progresoHandler.GetResumen) } @@ -214,12 +213,12 @@ func runMigrations(ctx context.Context, dbPool *pgxpool.Pool) { func seedEjercicios(ctx context.Context, pool *pgxpool.Pool) { ejercicios := []struct { - ID string + ID string ModuloNumero int - Titulo string - Tipo string - Contenido string - Orden int + Titulo string + Tipo string + Contenido string + Orden int }{ // Módulo 1 {"m1e1", 1, "Simulador de Disyuntivas", "interactivo", `{"tipo":"slider","descripcion":"Elige cuanto producir de cada bien"}`, 1}, diff --git a/backend/internal/handlers/progreso.go b/backend/internal/handlers/progreso.go index 6470adb..292933f 100644 --- a/backend/internal/handlers/progreso.go +++ b/backend/internal/handlers/progreso.go @@ -2,7 +2,6 @@ package handlers import ( "net/http" - "strconv" "github.com/gin-gonic/gin" "github.com/google/uuid" @@ -34,7 +33,7 @@ func (h *ProgresoHandler) GetProgreso(c *gin.Context) { return } - progresos, err := h.progresoRepo.GetByUsuario(c.Request.Context(), userID.(uuid.UUID)) + progresos, err := h.progresoRepo.GetProgresoByUsuarioID(userID.(uuid.UUID)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al obtener progreso"}) return @@ -47,72 +46,32 @@ func (h *ProgresoHandler) GetProgreso(c *gin.Context) { c.JSON(http.StatusOK, progresos) } -// GetProgresoModulo godoc -// @Summary Obtener progreso por módulo -// @Description Obtiene el progreso del usuario en un módulo específico -// @Tags progreso -// @Produce json -// @Security BearerAuth -// @Param numero path int true "Número del módulo" -// @Success 200 {array} models.Progreso -// @Router /api/progreso/modulo/{numero} [get] -func (h *ProgresoHandler) GetProgresoModulo(c *gin.Context) { - userID, exists := c.Get("user_id") - if !exists { - c.JSON(http.StatusUnauthorized, gin.H{"error": "No autorizado"}) - return - } - - moduloNumero, err := strconv.Atoi(c.Param("numero")) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Número de módulo inválido"}) - return - } - - progresos, err := h.progresoRepo.GetByModulo(c.Request.Context(), userID.(uuid.UUID), moduloNumero) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al obtener progreso"}) - return - } - - if progresos == nil { - progresos = []models.Progreso{} - } - - c.JSON(http.StatusOK, progresos) -} - -// UpdateProgreso godoc -// @Summary Guardar avance -// @Description Guarda el progreso de un ejercicio +// SaveProgreso godoc +// @Summary Guardar/actualizar progreso +// @Description Guarda o actualiza el progreso de un ejercicio // @Tags progreso // @Accept json // @Produce json -// @Param ejercicioId path int true "ID del ejercicio" -// @Param progreso body models.ProgresoUpdate true "Datos del progreso" +// @Param progreso body models.Progreso true "Datos del progreso" // @Security BearerAuth // @Success 200 {object} map[string]string -// @Router /api/progreso/{ejercicioId} [put] -func (h *ProgresoHandler) UpdateProgreso(c *gin.Context) { +// @Router /api/progreso [post] +func (h *ProgresoHandler) SaveProgreso(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "No autorizado"}) return } - ejercicioID, err := strconv.Atoi(c.Param("ejercicioId")) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "ID de ejercicio inválido"}) - return - } - - var req models.ProgresoUpdate - if err := c.ShouldBindJSON(&req); err != nil { + var progreso models.Progreso + if err := c.ShouldBindJSON(&progreso); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - err = h.progresoRepo.Upsert(c.Request.Context(), userID.(uuid.UUID), ejercicioID, &req) + progreso.UsuarioID = userID.(uuid.UUID) + + err := h.progresoRepo.SaveProgreso(&progreso) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al guardar progreso: " + err.Error()}) return @@ -123,11 +82,11 @@ func (h *ProgresoHandler) UpdateProgreso(c *gin.Context) { // GetResumen godoc // @Summary Obtener resumen -// @Description Obtiene estadísticas del progreso del usuario +// @Description Obtiene estadísticas del progreso del usuario (puntos totales, etc.) // @Tags progreso // @Produce json // @Security BearerAuth -// @Success 200 {object} models.ProgresoResumen +// @Success 200 {object} models.ResumenProgreso // @Router /api/progreso/resumen [get] func (h *ProgresoHandler) GetResumen(c *gin.Context) { userID, exists := c.Get("user_id") @@ -136,7 +95,7 @@ func (h *ProgresoHandler) GetResumen(c *gin.Context) { return } - resumen, err := h.progresoRepo.GetResumen(c.Request.Context(), userID.(uuid.UUID)) + resumen, err := h.progresoRepo.GetResumen(userID.(uuid.UUID)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al obtener resumen"}) return diff --git a/backend/internal/handlers/users.go b/backend/internal/handlers/users.go index 7e108e2..db7f7b1 100644 --- a/backend/internal/handlers/users.go +++ b/backend/internal/handlers/users.go @@ -216,7 +216,7 @@ func (h *UsersHandler) GetUserProgreso(c *gin.Context) { return } - progresos, err := h.progresoRepo.GetByUsuarioID(c.Request.Context(), id) + progresos, err := h.progresoRepo.GetProgresoByUsuarioID(id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al obtener progreso"}) return diff --git a/backend/internal/models/progreso.go b/backend/internal/models/progreso.go index 29004c5..9c57a0a 100644 --- a/backend/internal/models/progreso.go +++ b/backend/internal/models/progreso.go @@ -7,15 +7,31 @@ import ( ) type Progreso struct { - ID uuid.UUID `json:"id"` - UsuarioID uuid.UUID `json:"usuario_id"` - ModuloNumero int `json:"modulo_numero"` - EjercicioID int `json:"ejercicio_id"` - Completado bool `json:"completado"` - Puntuacion int `json:"puntuacion"` - Intentos int `json:"intentos"` - UltimaVez time.Time `json:"ultima_vez"` - RespuestaJSON string `json:"respuesta_json,omitempty"` + ID uuid.UUID `json:"id"` + UsuarioID uuid.UUID `json:"usuario_id"` + ModuloNumero int `json:"modulo_numero"` + EjercicioID string `json:"ejercicio_id"` + Completado bool `json:"completado"` + Puntuacion int `json:"puntuacion"` + Intentos int `json:"intentos"` + UltimaVez time.Time `json:"ultima_vez"` +} + +type Badge struct { + ID string `json:"id"` + Nombre string `json:"nombre"` + Descripcion string `json:"descripcion"` + Icono string `json:"icono"` + Desbloqueado bool `json:"desbloqueado"` +} + +type ResumenProgreso struct { + PuntosTotales int `json:"puntos_totales"` + EjerciciosCompletados int `json:"ejercicios_completados"` + TotalEjercicios int `json:"total_ejercicios"` + TotalPuntuacion int `json:"totalPuntuacion"` + Badges []Badge `json:"badges"` + Nivel string `json:"nivel"` } type ProgresoUpdate struct { @@ -25,8 +41,8 @@ type ProgresoUpdate struct { } type ProgresoResumen struct { - TotalEjercicios int `json:"total_ejercicios"` + TotalEjercicios int `json:"total_ejercicios"` EjerciciosCompletados int `json:"ejercicios_completados"` - PromedioPuntuacion int `json:"promedio_puntuacion"` - ModulosCompletados int `json:"modulos_completados"` + PromedioPuntuacion int `json:"promedio_puntuacion"` + ModulosCompletados int `json:"modulos_completados"` } diff --git a/backend/internal/repository/progreso.go b/backend/internal/repository/progreso.go index 7459bad..56108ff 100644 --- a/backend/internal/repository/progreso.go +++ b/backend/internal/repository/progreso.go @@ -17,13 +17,13 @@ func NewProgresoRepository(db *pgxpool.Pool) *ProgresoRepository { return &ProgresoRepository{db: db} } -func (r *ProgresoRepository) GetByUsuario(ctx context.Context, usuarioID uuid.UUID) ([]models.Progreso, error) { +func (r *ProgresoRepository) GetProgresoByUsuarioID(usuarioID uuid.UUID) ([]models.Progreso, error) { query := ` - SELECT id, usuario_id, modulo_numero, ejercicio_id, completado, puntuacion, intentos, ultima_vez, respuesta_json + SELECT id, usuario_id, modulo_numero, ejercicio_id, completado, puntuacion, intentos, ultima_vez FROM progreso_usuario WHERE usuario_id = $1 ORDER BY ultima_vez DESC ` - rows, err := r.db.Query(ctx, query, usuarioID) + rows, err := r.db.Query(context.Background(), query, usuarioID) if err != nil { return nil, err } @@ -34,7 +34,7 @@ func (r *ProgresoRepository) GetByUsuario(ctx context.Context, usuarioID uuid.UU var p models.Progreso err := rows.Scan( &p.ID, &p.UsuarioID, &p.ModuloNumero, &p.EjercicioID, - &p.Completado, &p.Puntuacion, &p.Intentos, &p.UltimaVez, &p.RespuestaJSON) + &p.Completado, &p.Puntuacion, &p.Intentos, &p.UltimaVez) if err != nil { return nil, err } @@ -43,99 +43,80 @@ func (r *ProgresoRepository) GetByUsuario(ctx context.Context, usuarioID uuid.UU return progresos, nil } -func (r *ProgresoRepository) GetByModulo(ctx context.Context, usuarioID uuid.UUID, moduloNumero int) ([]models.Progreso, error) { +func (r *ProgresoRepository) SaveProgreso(progreso *models.Progreso) error { query := ` - SELECT id, usuario_id, modulo_numero, ejercicio_id, completado, puntuacion, intentos, ultima_vez, respuesta_json - FROM progreso_usuario WHERE usuario_id = $1 AND modulo_numero = $2 - ORDER BY ejercicio_id - ` - rows, err := r.db.Query(ctx, query, usuarioID, moduloNumero) - if err != nil { - return nil, err - } - defer rows.Close() - - var progresos []models.Progreso - for rows.Next() { - var p models.Progreso - err := rows.Scan( - &p.ID, &p.UsuarioID, &p.ModuloNumero, &p.EjercicioID, - &p.Completado, &p.Puntuacion, &p.Intentos, &p.UltimaVez, &p.RespuestaJSON) - if err != nil { - return nil, err - } - progresos = append(progresos, p) - } - return progresos, nil -} - -func (r *ProgresoRepository) GetByEjercicio(ctx context.Context, usuarioID uuid.UUID, ejercicioID int) (*models.Progreso, error) { - query := ` - SELECT id, usuario_id, modulo_numero, ejercicio_id, completado, puntuacion, intentos, ultima_vez, respuesta_json - FROM progreso_usuario WHERE usuario_id = $1 AND ejercicio_id = $2 - ` - var p models.Progreso - err := r.db.QueryRow(ctx, query, usuarioID, ejercicioID).Scan( - &p.ID, &p.UsuarioID, &p.ModuloNumero, &p.EjercicioID, - &p.Completado, &p.Puntuacion, &p.Intentos, &p.UltimaVez, &p.RespuestaJSON) - if err != nil { - return nil, err - } - return &p, nil -} - -func (r *ProgresoRepository) Upsert(ctx context.Context, usuarioID uuid.UUID, ejercicioID int, update *models.ProgresoUpdate) error { - query := ` - INSERT INTO progreso_usuario (id, usuario_id, modulo_numero, ejercicio_id, completado, puntuacion, intentos, ultima_vez, respuesta_json) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + INSERT INTO progreso_usuario (id, usuario_id, modulo_numero, ejercicio_id, completado, puntuacion, intentos, ultima_vez) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (usuario_id, modulo_numero, ejercicio_id) - DO UPDATE SET completado = $5, puntuacion = $6, intentos = $7, ultima_vez = $8, respuesta_json = $9 + DO UPDATE SET completado = $5, puntuacion = $6, intentos = progreso_usuario.intentos + 1, ultima_vez = $8 ` - moduloNumero, err := r.getModuloByEjercicio(ctx, ejercicioID) - if err != nil { - return err + if progreso.ID == uuid.Nil { + progreso.ID = uuid.New() + } + if progreso.UltimaVez.IsZero() { + progreso.UltimaVez = time.Now() + } + if progreso.Intentos == 0 { + progreso.Intentos = 1 } - existing, _ := r.GetByEjercicio(ctx, usuarioID, ejercicioID) - var intentos int - if existing != nil { - intentos = existing.Intentos + 1 - } else { - intentos = 1 - } - - _, err = r.db.Exec(ctx, query, - uuid.New(), usuarioID, moduloNumero, ejercicioID, - update.Completado, update.Puntuacion, intentos, time.Now(), update.RespuestaJSON) + _, err := r.db.Exec(context.Background(), query, + progreso.ID, progreso.UsuarioID, progreso.ModuloNumero, progreso.EjercicioID, + progreso.Completado, progreso.Puntuacion, progreso.Intentos, progreso.UltimaVez) return err } -func (r *ProgresoRepository) getModuloByEjercicio(ctx context.Context, ejercicioID int) (int, error) { - var moduloNumero int - err := r.db.QueryRow(ctx, "SELECT modulo_numero FROM ejercicios WHERE id = $1", ejercicioID).Scan(&moduloNumero) - return moduloNumero, err -} - -func (r *ProgresoRepository) GetResumen(ctx context.Context, usuarioID uuid.UUID) (*models.ProgresoResumen, error) { +func (r *ProgresoRepository) GetResumen(usuarioID uuid.UUID) (*models.ResumenProgreso, error) { query := ` SELECT - COUNT(DISTINCT ejercicio_id) as total, - COUNT(CASE WHEN completado THEN 1 END) as completados, - COALESCE(AVG(CASE WHEN completado THEN puntuacion END), 0)::int as promedio, - COUNT(DISTINCT CASE WHEN completado THEN modulo_numero END) as modulos + COALESCE(SUM(puntuacion), 0) as puntos_totales, + COUNT(CASE WHEN completado THEN 1 END) as ejercicios_completados, + COUNT(*) as total_ejercicios FROM progreso_usuario WHERE usuario_id = $1 ` - var resumen models.ProgresoResumen - err := r.db.QueryRow(ctx, query, usuarioID).Scan( - &resumen.TotalEjercicios, &resumen.EjerciciosCompletados, - &resumen.PromedioPuntuacion, &resumen.ModulosCompletados) + var resumen models.ResumenProgreso + err := r.db.QueryRow(context.Background(), query, usuarioID).Scan( + &resumen.PuntosTotales, &resumen.EjerciciosCompletados, &resumen.TotalEjercicios) if err != nil { return nil, err } + + // Alias para compatibilidad con frontend + resumen.TotalPuntuacion = resumen.PuntosTotales + + // Calcular nivel basado en puntuación + resumen.Nivel = calcularNivel(resumen.PuntosTotales) + + // Generar badges basados en progreso + resumen.Badges = generarBadges(resumen.PuntosTotales, resumen.EjerciciosCompletados) + return &resumen, nil } -func (r *ProgresoRepository) GetByUsuarioID(ctx context.Context, usuarioID uuid.UUID) ([]models.Progreso, error) { - return r.GetByUsuario(ctx, usuarioID) +func calcularNivel(puntuacion int) string { + if puntuacion >= 2000 { + return "Maestro" + } + if puntuacion >= 1000 { + return "Experto" + } + if puntuacion >= 300 { + return "Aprendiz" + } + return "Novato" +} + +func generarBadges(puntuacion, ejerciciosCompletados int) []models.Badge { + badges := []models.Badge{ + {ID: "primer-ejercicio", Nombre: "Primer Ejercicio", Descripcion: "Completa tu primer ejercicio", Icono: "star", Desbloqueado: ejerciciosCompletados >= 1}, + {ID: "primer-modulo", Nombre: "Primer Módulo", Descripcion: "Completa todas las lecciones de un módulo", Icono: "award", Desbloqueado: ejerciciosCompletados >= 3}, + {ID: "aprendiz", Nombre: "Aprendiz", Descripcion: "Alcanza el nivel Aprendiz", Icono: "book", Desbloqueado: puntuacion >= 300}, + {ID: "experto", Nombre: "Experto", Descripcion: "Alcanza el nivel Experto", Icono: "trophy", Desbloqueado: puntuacion >= 1000}, + {ID: "maestro", Nombre: "Maestro", Descripcion: "Alcanza el nivel Maestro", Icono: "crown", Desbloqueado: puntuacion >= 2000}, + {ID: "puntos-500", Nombre: "500 Puntos", Descripcion: "Acumula 500 puntos", Icono: "target", Desbloqueado: puntuacion >= 500}, + {ID: "puntos-1000", Nombre: "1000 Puntos", Descripcion: "Acumula 1000 puntos", Icono: "zap", Desbloqueado: puntuacion >= 1000}, + {ID: "puntos-2000", Nombre: "2000 Puntos", Descripcion: "Acumula 2000 puntos", Icono: "flame", Desbloqueado: puntuacion >= 2000}, + } + return badges } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a3a2d8e..5cf14e3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "axios": "^1.6.2", + "framer-motion": "^12.34.0", "lucide-react": "^0.294.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -1809,6 +1810,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.34.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.34.0.tgz", + "integrity": "sha512-+/H49owhzkzQyxtn7nZeF4kdH++I2FWrESQ184Zbcw5cEqNHYkE5yxWxcTLSj5lNx3NWdbIRy5FHqUvetD8FWg==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.34.0", + "motion-utils": "^12.29.2", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2153,6 +2181,21 @@ "node": ">= 0.6" } }, + "node_modules/motion-dom": { + "version": "12.34.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.34.0.tgz", + "integrity": "sha512-Lql3NuEcScRDxTAO6GgUsRHBZOWI/3fnMlkMcH5NftzcN37zJta+bpbMAV9px4Nj057TuvRooMK7QrzMCgtz6Q==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.29.2" + } + }, + "node_modules/motion-utils": { + "version": "12.29.2", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz", + "integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2850,6 +2893,12 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 43e6e7c..76c3e33 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "axios": "^1.6.2", + "framer-motion": "^12.34.0", "lucide-react": "^0.294.0", "react": "^18.2.0", "react-dom": "^18.2.0", 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 index 9f1d0f9..1733eb4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,3 +1,4 @@ +import { useEffect } from 'react'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { useAuthStore } from './stores/authStore'; import { Login } from './pages/Login'; @@ -5,6 +6,7 @@ import { Dashboard } from './pages/Dashboard'; import { Modulos } from './pages/Modulos'; import { Modulo } from './pages/Modulo'; import { AdminPanel } from './pages/admin/AdminPanel'; +import { RecursosPage } from './pages/Recursos'; function ProtectedRoute({ children }: { children: React.ReactNode }) { const { isAuthenticated, isLoading } = useAuthStore(); @@ -25,6 +27,12 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) { } function App() { + const { checkAuth } = useAuthStore(); + + useEffect(() => { + checkAuth(); + }, [checkAuth]); + return ( @@ -53,6 +61,38 @@ function App() { } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> } /> + + + + } + /> } /> 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/index.ts b/frontend/src/components/exercises/index.ts new file mode 100644 index 0000000..572f765 --- /dev/null +++ b/frontend/src/components/exercises/index.ts @@ -0,0 +1 @@ +export { EjercicioWrapper } from './EjercicioWrapper'; diff --git a/frontend/src/components/exercises/modulo1/FlujoCircular.tsx b/frontend/src/components/exercises/modulo1/FlujoCircular.tsx new file mode 100644 index 0000000..5559c74 --- /dev/null +++ b/frontend/src/components/exercises/modulo1/FlujoCircular.tsx @@ -0,0 +1,431 @@ +import { useState, useEffect } from 'react'; +import { motion } from 'framer-motion'; +import { Card, CardHeader } from '../../ui/Card'; +import { Button } from '../../ui/Button'; +import { CheckCircle, XCircle, Trophy, Users, Building2, Landmark, Globe, RefreshCw } from 'lucide-react'; + +interface FlujoCircularProps { + ejercicioId: string; + onComplete?: (puntuacion: number) => void; +} + +type Agente = 'familias' | 'empresas' | 'estado' | 'exterior'; +type TipoFlujo = 'real' | 'monetario'; + +interface Elemento { + id: string; + texto: string; + tipo: TipoFlujo; + origen: Agente; + destino: Agente; +} + +interface Nivel { + nombre: string; + descripcion: string; + agentes: Agente[]; + elementos: Elemento[]; +} + +const NIVELES: Nivel[] = [ + { + nombre: 'Básico', + descripcion: 'Solo Familias y Empresas', + agentes: ['familias', 'empresas'], + elementos: [ + { id: 'trabajo', texto: '💪 Trabajo', tipo: 'real', origen: 'familias', destino: 'empresas' }, + { id: 'salarios', texto: '💵 Salarios', tipo: 'monetario', origen: 'empresas', destino: 'familias' }, + { id: 'bienes', texto: '📦 Bienes', tipo: 'real', origen: 'empresas', destino: 'familias' }, + { id: 'gasto', texto: '💳 Gasto', tipo: 'monetario', origen: 'familias', destino: 'empresas' }, + ] + }, + { + nombre: 'Intermedio', + descripcion: 'Incluye al Estado', + agentes: ['familias', 'empresas', 'estado'], + elementos: [ + { id: 'trabajo', texto: '💪 Trabajo', tipo: 'real', origen: 'familias', destino: 'empresas' }, + { id: 'tierra', texto: '🌾 Tierra', tipo: 'real', origen: 'familias', destino: 'empresas' }, + { id: 'capital', texto: '💰 Capital', tipo: 'real', origen: 'familias', destino: 'empresas' }, + { id: 'salarios', texto: '💵 Salarios', tipo: 'monetario', origen: 'empresas', destino: 'familias' }, + { id: 'renta', texto: '🏠 Renta', tipo: 'monetario', origen: 'empresas', destino: 'familias' }, + { id: 'intereses', texto: '📈 Intereses', tipo: 'monetario', origen: 'empresas', destino: 'familias' }, + { id: 'bienes', texto: '📦 Bienes', tipo: 'real', origen: 'empresas', destino: 'familias' }, + { id: 'servicios', texto: '🔧 Servicios', tipo: 'real', origen: 'empresas', destino: 'familias' }, + { id: 'gasto', texto: '💳 Gasto', tipo: 'monetario', origen: 'familias', destino: 'empresas' }, + { id: 'impuestos', texto: '📝 Impuestos', tipo: 'monetario', origen: 'familias', destino: 'estado' }, + { id: 'transferencias', texto: '🎁 Transferencias', tipo: 'monetario', origen: 'estado', destino: 'familias' }, + { id: 'gasto-publico', texto: '🏗️ Gasto Público', tipo: 'monetario', origen: 'estado', destino: 'empresas' }, + ] + }, + { + nombre: 'Avanzado', + descripcion: 'Todos los agentes incluyendo Sector Externo', + agentes: ['familias', 'empresas', 'estado', 'exterior'], + elementos: [ + { id: 'trabajo', texto: '💪 Trabajo', tipo: 'real', origen: 'familias', destino: 'empresas' }, + { id: 'tierra', texto: '🌾 Tierra', tipo: 'real', origen: 'familias', destino: 'empresas' }, + { id: 'capital', texto: '💰 Capital', tipo: 'real', origen: 'familias', destino: 'empresas' }, + { id: 'salarios', texto: '💵 Salarios', tipo: 'monetario', origen: 'empresas', destino: 'familias' }, + { id: 'renta', texto: '🏠 Renta', tipo: 'monetario', origen: 'empresas', destino: 'familias' }, + { id: 'intereses', texto: '📈 Intereses', tipo: 'monetario', origen: 'empresas', destino: 'familias' }, + { id: 'bienes', texto: '📦 Bienes', tipo: 'real', origen: 'empresas', destino: 'familias' }, + { id: 'servicios', texto: '🔧 Servicios', tipo: 'real', origen: 'empresas', destino: 'familias' }, + { id: 'gasto', texto: '💳 Gasto', tipo: 'monetario', origen: 'familias', destino: 'empresas' }, + { id: 'impuestos', texto: '📝 Impuestos', tipo: 'monetario', origen: 'familias', destino: 'estado' }, + { id: 'transferencias', texto: '🎁 Transferencias', tipo: 'monetario', origen: 'estado', destino: 'familias' }, + { id: 'gasto-publico', texto: '🏗️ Gasto Público', tipo: 'monetario', origen: 'estado', destino: 'empresas' }, + { id: 'exportaciones', texto: '📤 Exportaciones', tipo: 'real', origen: 'empresas', destino: 'exterior' }, + { id: 'importaciones', texto: '📥 Importaciones', tipo: 'real', origen: 'exterior', destino: 'empresas' }, + { id: 'divisas-ent', texto: '💱 Divisas (Ent.)', tipo: 'monetario', origen: 'exterior', destino: 'empresas' }, + { id: 'divisas-sal', texto: '💱 Divisas (Sal.)', tipo: 'monetario', origen: 'empresas', destino: 'exterior' }, + ] + } +]; + +const AGENTE_CONFIG: Record = { + familias: { icon: , label: 'Familias', color: 'bg-green-100 text-green-700 border-green-300', position: 'left-4 top-1/2 -translate-y-1/2' }, + empresas: { icon: , label: 'Empresas', color: 'bg-blue-100 text-blue-700 border-blue-300', position: 'right-4 top-1/2 -translate-y-1/2' }, + estado: { icon: , label: 'Estado', color: 'bg-orange-100 text-orange-700 border-orange-300', position: 'left-1/2 -translate-x-1/2 top-4' }, + exterior: { icon: , label: 'Sector Externo', color: 'bg-purple-100 text-purple-700 border-purple-300', position: 'left-1/2 -translate-x-1/2 bottom-4' }, +}; + +export function FlujoCircular({ ejercicioId: _ejercicioId, onComplete }: FlujoCircularProps) { + const [nivelActual, setNivelActual] = useState(0); + const [elementosColocados, setElementosColocados] = useState>({}); + const [elementoSeleccionado, setElementoSeleccionado] = useState(null); + const [puntuacion, setPuntuacion] = useState(0); + const [completado, setCompletado] = useState(false); + const [aciertos, setAciertos] = useState(0); + const [errores, setErrores] = useState(0); + + const nivel = NIVELES[nivelActual]; + + useEffect(() => { + const inicial: Record = {}; + nivel.elementos.forEach(el => { + inicial[el.id] = null; + }); + setElementosColocados(inicial); + }, [nivel]); + + const handleElementoClick = (elementoId: string) => { + if (elementosColocados[elementoId]) return; + setElementoSeleccionado(elementoId === elementoSeleccionado ? null : elementoId); + }; + + const handleConexionClick = (origen: Agente, destino: Agente) => { + if (!elementoSeleccionado) return; + + const elemento = nivel.elementos.find(el => el.id === elementoSeleccionado); + if (!elemento) return; + + const esCorrecto = elemento.origen === origen && elemento.destino === destino; + + setElementosColocados(prev => ({ + ...prev, + [elementoSeleccionado]: { origen, destino } + })); + + if (esCorrecto) { + setAciertos(prev => prev + 1); + setPuntuacion(prev => prev + 10); + } else { + setErrores(prev => prev + 1); + setPuntuacion(prev => Math.max(0, prev - 2)); + } + + setElementoSeleccionado(null); + }; + + const verificarCompletitud = () => { + const todosColocados = nivel.elementos.every(el => elementosColocados[el.id] !== null); + if (todosColocados) { + const bonus = 50; + setPuntuacion(prev => prev + bonus); + + if (nivelActual < NIVELES.length - 1) { + setNivelActual(prev => prev + 1); + } else { + setCompletado(true); + if (onComplete) { + onComplete(puntuacion + bonus); + } + } + } + }; + + useEffect(() => { + const todosColocados = nivel.elementos.every(el => elementosColocados[el.id] !== null); + if (todosColocados && !completado) { + setTimeout(verificarCompletitud, 500); + } + }, [elementosColocados]); + + const handleReiniciarNivel = () => { + const inicial: Record = {}; + nivel.elementos.forEach(el => { + inicial[el.id] = null; + }); + setElementosColocados(inicial); + setElementoSeleccionado(null); + setAciertos(0); + setErrores(0); + }; + + const getConexionesPosibles = (): { origen: Agente; destino: Agente; label: string }[] => { + const conexiones: { origen: Agente; destino: Agente; label: string }[] = []; + + if (nivel.agentes.includes('familias') && nivel.agentes.includes('empresas')) { + conexiones.push({ origen: 'familias', destino: 'empresas', label: 'Familias → Empresas' }); + conexiones.push({ origen: 'empresas', destino: 'familias', label: 'Empresas → Familias' }); + } + + if (nivel.agentes.includes('estado')) { + if (nivel.agentes.includes('familias')) { + conexiones.push({ origen: 'familias', destino: 'estado', label: 'Familias → Estado' }); + conexiones.push({ origen: 'estado', destino: 'familias', label: 'Estado → Familias' }); + } + if (nivel.agentes.includes('empresas')) { + conexiones.push({ origen: 'estado', destino: 'empresas', label: 'Estado → Empresas' }); + } + } + + if (nivel.agentes.includes('exterior') && nivel.agentes.includes('empresas')) { + conexiones.push({ origen: 'empresas', destino: 'exterior', label: 'Empresas → Exterior' }); + conexiones.push({ origen: 'exterior', destino: 'empresas', label: 'Exterior → Empresas' }); + } + + return conexiones; + }; + + if (completado) { + return ( + +
+ + + + +

¡Juego Completado!

+

+ Has completado todos los niveles del Flujo Circular +

+ +
+

Puntuación Final

+

{puntuacion} puntos

+
+ +
+
+

{aciertos}

+

Aciertos

+
+
+

{errores}

+

Errores

+
+
+ + +
+
+ ); + } + + return ( + + + + + } + /> + +
+
+ {NIVELES.map((_, idx) => ( +
+ {idx < nivelActual ? : idx + 1} +
+ ))} +
+
+

Puntuación

+

{puntuacion} pts

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

Elementos ({nivel.elementos.length})

+

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

+ +
+ {nivel.elementos.map((elemento) => { + const colocado = elementosColocados[elemento.id]; + const seleccionado = elementoSeleccionado === elemento.id; + const esCorrecto = colocado && colocado.origen === elemento.origen && colocado.destino === elemento.destino; + + return ( + handleElementoClick(elemento.id)} + disabled={!!colocado} + whileHover={!colocado ? { scale: 1.02 } : {}} + whileTap={!colocado ? { scale: 0.98 } : {}} + className={`w-full p-3 rounded-lg border-2 text-left transition-all ${ + colocado + ? esCorrecto + ? 'border-green-300 bg-green-50' + : 'border-red-300 bg-red-50' + : seleccionado + ? 'border-blue-500 bg-blue-50 shadow-md' + : 'border-gray-200 bg-white hover:border-blue-300' + }`} + > +
+
+ {elemento.texto.split(' ')[0]} + {elemento.texto.split(' ').slice(1).join(' ')} +
+ {colocado && ( + esCorrecto + ? + : + )} +
+
+ + {elemento.tipo === 'real' ? 'Real' : 'Monetario'} + + {colocado && !esCorrecto && ( + + {AGENTE_CONFIG[elemento.origen].label} → {AGENTE_CONFIG[elemento.destino].label} + + )} +
+
+ ); + })} +
+ +
+

Leyenda:

+
+ + + Flujo Real + + + + Flujo Monetario + +
+
+
+
+
+ ); +} + +export default FlujoCircular; 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/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/index.ts b/frontend/src/components/exercises/modulo1/index.ts new file mode 100644 index 0000000..e5334ca --- /dev/null +++ b/frontend/src/components/exercises/modulo1/index.ts @@ -0,0 +1,3 @@ +export { SimuladorDisyuntivas } from './SimuladorDisyuntivas'; +export { QuizBienes } from './QuizBienes'; +export { FlujoCircular } from './FlujoCircular'; 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/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/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/index.ts b/frontend/src/components/exercises/modulo2/index.ts new file mode 100644 index 0000000..76cf17d --- /dev/null +++ b/frontend/src/components/exercises/modulo2/index.ts @@ -0,0 +1,3 @@ +export { ConstructorCurvas } from './ConstructorCurvas'; +export { SimuladorPrecios } from './SimuladorPrecios'; +export { IdentificarShocks } from './IdentificarShocks'; 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/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/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/index.ts b/frontend/src/components/exercises/modulo3/index.ts new file mode 100644 index 0000000..5a5ca7b --- /dev/null +++ b/frontend/src/components/exercises/modulo3/index.ts @@ -0,0 +1,3 @@ +export { CalculadoraElasticidad } from './CalculadoraElasticidad'; +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/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/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..50633ca --- /dev/null +++ b/frontend/src/components/exercises/modulo4/index.ts @@ -0,0 +1,3 @@ +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..c1fe10b --- /dev/null +++ b/frontend/src/components/progress/ScoreDisplay.tsx @@ -0,0 +1,212 @@ +import { useState, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Star } from 'lucide-react'; +import type { NivelUsuario } from '../../types'; + +interface ScoreDisplayProps { + puntos: number; + animar?: boolean; + showNivel?: boolean; + size?: 'sm' | 'md' | 'lg'; +} + +const NIVELES_CONFIG: Record = { + Novato: { + color: 'text-gray-600', + bgColor: 'bg-gray-100', + icon: '🌱' + }, + Aprendiz: { + color: 'text-blue-600', + bgColor: 'bg-blue-100', + icon: '📚' + }, + Experto: { + color: 'text-purple-600', + bgColor: 'bg-purple-100', + icon: '🏆' + }, + Maestro: { + color: 'text-yellow-600', + bgColor: 'bg-yellow-100', + icon: '👑' + }, +}; + +function calcularNivel(puntuacion: number): NivelUsuario { + if (puntuacion >= 2000) return 'Maestro'; + if (puntuacion >= 1000) return 'Experto'; + if (puntuacion >= 300) return 'Aprendiz'; + return 'Novato'; +} + +function calcularProgresoNivel(puntuacion: number): { actual: number; siguiente: number; porcentaje: number } { + if (puntuacion >= 2000) { + return { actual: 2000, siguiente: 2000, porcentaje: 100 }; + } else if (puntuacion >= 1000) { + return { actual: puntuacion, siguiente: 2000, porcentaje: ((puntuacion - 1000) / 1000) * 100 }; + } else if (puntuacion >= 300) { + return { actual: puntuacion, siguiente: 1000, porcentaje: ((puntuacion - 300) / 700) * 100 }; + } else { + return { actual: puntuacion, siguiente: 300, porcentaje: (puntuacion / 300) * 100 }; + } +} + +export function ScoreDisplay({ + puntos, + animar = false, + showNivel = true, + size = 'md' +}: ScoreDisplayProps) { + const [puntosAnimados, setPuntosAnimados] = useState(0); + const [puntosPrevios, setPuntosPrevios] = useState(puntos); + const [cambioReciente, setCambioReciente] = useState(0); + + const nivel = calcularNivel(puntos); + const configNivel = NIVELES_CONFIG[nivel]; + const progresoNivel = calcularProgresoNivel(puntos); + + useEffect(() => { + if (animar && puntos !== puntosPrevios) { + const diferencia = puntos - puntosPrevios; + setCambioReciente(diferencia); + + // Animar contador + const duracion = 1500; + const pasos = 60; + let pasoActual = 0; + + const intervalo = setInterval(() => { + pasoActual++; + const progreso = pasoActual / pasos; + // Función de easing + const easeOutQuart = 1 - Math.pow(1 - progreso, 4); + + setPuntosAnimados(Math.round(puntosPrevios + (diferencia * easeOutQuart))); + + if (pasoActual >= pasos) { + clearInterval(intervalo); + setPuntosAnimados(puntos); + setPuntosPrevios(puntos); + + // Ocultar el cambio después de 3 segundos + setTimeout(() => setCambioReciente(0), 3000); + } + }, duracion / pasos); + + return () => clearInterval(intervalo); + } else { + setPuntosAnimados(puntos); + setPuntosPrevios(puntos); + } + }, [puntos, animar, puntosPrevios]); + + const sizeClasses = { + sm: { + container: 'p-2', + puntos: 'text-xl', + label: 'text-xs', + icon: 16, + }, + md: { + container: 'p-4', + puntos: 'text-3xl', + label: 'text-sm', + icon: 24, + }, + lg: { + container: 'p-6', + puntos: 'text-5xl', + label: 'text-base', + icon: 32, + }, + }; + + return ( +
+
+
+
+ +
+
+

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

Cargando tu progreso...

+
+
+ ); + } + + if (error) { + return ( +
+
+
+ + + +
+

Error al cargar el progreso

+

{error}

+ +
+
+ ); + } + + // Calcular progreso total const totalProgreso = Math.round( - modulosProgreso.reduce((acc, mod) => acc + mod.porcentaje, 0) / modulosProgreso.length + MODULOS_CONFIG.reduce((acc, mod) => { + return acc + calcularPorcentajeModulo(mod.id, mod.totalEjercicios); + }, 0) / MODULOS_CONFIG.length ); + const badgesDesbloqueados = getBadgesDesbloqueados(); + const badgesBloqueados = getBadgesBloqueados(); + + // Calcular ejercicios completados por módulo + const getEjerciciosCompletados = (moduloId: string) => { + const modulo = modulos[moduloId]; + if (!modulo) return 0; + return Object.values(modulo.ejercicios).filter(ej => ej.completado).length; + }; + return (
-
+
-
+
-

Economía

+

Economía Interactiva

- {usuario?.nombre} + {usuario?.nombre || 'Usuario'} + + {nivel} + {usuario?.rol === 'admin' && ( - + Admin )} @@ -91,86 +118,154 @@ export function Dashboard() {

Tu progreso

-

Continúa donde lo dejaste

+

Continúa donde lo dejaste y desbloquea nuevos logros

- -
-
-

Progreso total

-

{totalProgreso}% completado

+ {/* 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 + + )} +
+
+ + ); + })} +
+ +
+ + + + + +
-
{totalProgreso}%
-
-
+
- - -
-

Módulos

- {usuario?.rol === 'admin' && ( - - - - )} -
- -
- {modulosProgreso.map((modulo) => ( - - -
-
-
- {modulo.numero} -
-
-

{modulo.titulo}

-

- {modulo.ejerciciosCompletados}/{modulo.totalEjercicios} ejercicios -

-
-
-
- -
-
-
- -
- {modulo.porcentaje}% completado - {modulo.porcentaje === 100 && ( - - - Completado - - )} -
- - - ))} -
- -
- - -
); } + +export default Dashboard; diff --git a/frontend/src/pages/Modulo.tsx b/frontend/src/pages/Modulo.tsx index 0e43e98..ccb8a9e 100644 --- a/frontend/src/pages/Modulo.tsx +++ b/frontend/src/pages/Modulo.tsx @@ -1,63 +1,213 @@ import { useState, useEffect } from 'react'; import { useParams, Link } from 'react-router-dom'; +import { motion } from 'framer-motion'; import { Card } from '../components/ui/Card'; import { Button } from '../components/ui/Button'; -import { progresoService } from '../services/api'; -import type { Progreso } from '../types'; -import { ArrowLeft, CheckCircle, Play } from 'lucide-react'; +import { Loader } from '../components/ui/Loader'; +import { useProgressStore } from '../stores/progressStore'; +import { ScoreDisplay } from '../components/progress/ScoreDisplay'; +import { + ArrowLeft, + CheckCircle, + Play, + Lock, + Trophy, + TrendingUp, + RotateCcw +} from 'lucide-react'; +import type { EjercicioProgreso } from '../stores/progressStore'; -const MODULOS_INFO: Record = { - 1: { titulo: 'Fundamentos de Economía', descripcion: 'Introducción a los conceptos básicos de economía' }, - 2: { titulo: 'Oferta, Demanda y Equilibrio', descripcion: 'Curvas de oferta y demanda en el mercado' }, - 3: { titulo: 'Utilidad y Elasticidad', descripcion: 'Teoría del consumidor y elasticidades' }, - 4: { titulo: 'Teoría del Productor', descripcion: 'Costos de producción y competencia perfecta' }, +// Importar ejercicios reales +import { FlujoCircular } from '../components/exercises/modulo1/FlujoCircular'; +import { QuizBienes } from '../components/exercises/modulo1/QuizBienes'; +import { SimuladorDisyuntivas } from '../components/exercises/modulo1/SimuladorDisyuntivas'; +import { ConstructorCurvas } from '../components/exercises/modulo2/ConstructorCurvas'; +import { IdentificarShocks } from '../components/exercises/modulo2/IdentificarShocks'; +import { SimuladorPrecios } from '../components/exercises/modulo2/SimuladorPrecios'; +import { ClasificadorBienes } from '../components/exercises/modulo3/ClasificadorBienes'; +import { CalculadoraElasticidad } from '../components/exercises/modulo3/CalculadoraElasticidad'; +import { EjerciciosExamen } from '../components/exercises/modulo3/EjerciciosExamen'; +import { CalculadoraCostos } from '../components/exercises/modulo4/CalculadoraCostos'; +import { SimuladorProduccion } from '../components/exercises/modulo4/SimuladorProduccion'; +import { VisualizadorExcedentes } from '../components/exercises/modulo4/VisualizadorExcedentes'; + +const MODULOS_INFO: Record = { + 1: { + id: 'modulo1', + titulo: 'Fundamentos de Economía', + descripcion: 'Introducción a los conceptos básicos de economía', + color: 'from-blue-500 to-blue-600' + }, + 2: { + id: 'modulo2', + titulo: 'Oferta, Demanda y Equilibrio', + descripcion: 'Curvas de oferta y demanda en el mercado', + color: 'from-green-500 to-green-600' + }, + 3: { + id: 'modulo3', + titulo: 'Utilidad y Elasticidad', + descripcion: 'Teoría del consumidor y elasticidades', + color: 'from-purple-500 to-purple-600' + }, + 4: { + id: 'modulo4', + titulo: 'Teoría del Productor', + descripcion: 'Costos de producción y competencia perfecta', + color: 'from-orange-500 to-orange-600' + }, }; -const EJERCICIOS_MOCK = [ - { id: 'e1', titulo: 'Conceptos básicos', descripcion: 'Repasa los fundamentos de la economía' }, - { id: 'e2', titulo: 'Agentes económicos', descripcion: 'Identifica los diferentes agentes en la economía' }, - { id: 'e3', titulo: 'Factores de producción', descripcion: 'Aprende sobre tierra, trabajo y capital' }, - { id: 'e4', titulo: 'Flujo circular', descripcion: 'Comprende el flujo de bienes y dinero' }, - { id: 'e5', titulo: 'Evaluación final', descripcion: 'Pon a prueba todo lo aprendido' }, -]; +const EJERCICIOS_POR_MODULO: Record void }>; +}>> = { + 1: [ + { id: 'simulador-disyuntivas', titulo: 'Simulador de Disyuntivas', descripcion: 'Explora las decisiones económicas fundamentales', componente: SimuladorDisyuntivas }, + { id: 'quiz-bienes', titulo: 'Quiz de Bienes', descripcion: 'Identifica diferentes tipos de bienes', componente: QuizBienes }, + { id: 'flujo-circular', titulo: 'Flujo Circular', descripcion: 'Comprende el flujo de bienes y dinero en la economía', componente: FlujoCircular }, + ], + 2: [ + { id: 'constructor-curvas', titulo: 'Constructor de Curvas', descripcion: 'Construye curvas de oferta y demanda', componente: ConstructorCurvas }, + { id: 'identificar-shocks', titulo: 'Identificar Shocks', descripcion: 'Reconoce cambios en el mercado', componente: IdentificarShocks }, + { id: 'simulador-precios', titulo: 'Simulador de Precios', descripcion: 'Simula el equilibrio de precios', componente: SimuladorPrecios }, + ], + 3: [ + { id: 'clasificador-bienes', titulo: 'Clasificador de Bienes', descripcion: 'Clasifica bienes según su elasticidad', componente: ClasificadorBienes }, + { id: 'calculadora-elasticidad', titulo: 'Calculadora de Elasticidad', descripcion: 'Calcula elasticidades de demanda', componente: CalculadoraElasticidad }, + { id: 'ejercicios-examen', titulo: 'Ejercicios de Examen', descripcion: 'Pon a prueba tus conocimientos', componente: EjerciciosExamen }, + ], + 4: [ + { id: 'calculadora-costos', titulo: 'Calculadora de Costos', descripcion: 'Calcula costos de producción', componente: CalculadoraCostos }, + { id: 'simulador-produccion', titulo: 'Simulador de Producción', descripcion: 'Simula la producción óptima', componente: SimuladorProduccion }, + { id: 'visualizador-excedentes', titulo: 'Visualizador de Excedentes', descripcion: 'Visualiza excedentes del consumidor y productor', componente: VisualizadorExcedentes }, + ], +}; export function Modulo() { const { numero } = useParams<{ numero: string }>(); const num = parseInt(numero || '1', 10); - const [progresos, setProgresos] = useState([]); + + const { + puntuacionTotal, + getProgresoEjercicio, + saveProgreso, + calcularPorcentajeModulo, + loadProgreso, + isLoading, + error, + } = useProgressStore(); - const moduloInfo = MODULOS_INFO[num] || MODULOS_INFO[1]; - const ejercicios = EJERCICIOS_MOCK; + const [ejercicioActivo, setEjercicioActivo] = useState(null); useEffect(() => { loadProgreso(); - }, [num]); + }, [loadProgreso]); - const loadProgreso = async () => { + const moduloInfo = MODULOS_INFO[num] || MODULOS_INFO[1]; + const ejercicios = EJERCICIOS_POR_MODULO[num] || []; + const porcentaje = calcularPorcentajeModulo(moduloInfo.id, ejercicios.length); + + const getProgresoEjercicioLocal = (ejercicioId: string): EjercicioProgreso | undefined => { + return getProgresoEjercicio(moduloInfo.id, ejercicioId); + }; + + const handleCompleteEjercicio = async (ejercicioId: string, puntuacion: number) => { try { - const data = await progresoService.getProgreso(); - setProgresos(data); - } catch { - // Silencio + await saveProgreso(moduloInfo.id, ejercicioId, puntuacion); + setEjercicioActivo(null); + } catch (err) { + console.error('Error al guardar progreso:', err); } }; - const getProgresoForEjercicio = (ejercicioId: string) => { - return progresos.find( - (p) => p.modulo_numero === num && p.ejercicio_id === ejercicioId - ); + const completados = ejercicios.filter( + (e) => getProgresoEjercicioLocal(e.id)?.completado + ).length; + + // Determinar si un ejercicio está bloqueado (el primero siempre desbloqueado) + const isEjercicioBloqueado = (index: number): boolean => { + if (index === 0) return false; + // Ejercicio anterior completado? + const ejercicioAnterior = ejercicios[index - 1]; + return !getProgresoEjercicioLocal(ejercicioAnterior.id)?.completado; }; - const completados = ejercicios.filter( - (e) => getProgresoForEjercicio(e.id)?.completado - ).length; - const porcentaje = Math.round((completados / ejercicios.length) * 100); + if (ejercicioActivo) { + const ejercicio = ejercicios.find(e => e.id === ejercicioActivo); + if (!ejercicio) return null; + + const EjercicioComponent = ejercicio.componente; + + return ( +
+
+
+ +
+
+ +
+
+

{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 @@ -67,7 +217,7 @@ export function Modulo() {
-
+
{num}
@@ -76,76 +226,153 @@ export function Modulo() {
- -
+ +
-

Tu progreso en este módulo

+

Tu progreso en este módulo

{porcentaje}%

-

{completados}/{ejercicios.length} ejercicios

+

Ejercicios

+

{completados}/{ejercicios.length}

-
-
+
-

Ejercicios

+
+

Ejercicios

+ +
-
+
{ejercicios.map((ejercicio, index) => { - const progreso = getProgresoForEjercicio(ejercicio.id); + const progreso = getProgresoEjercicioLocal(ejercicio.id); const completado = progreso?.completado || false; + const bloqueado = isEjercicioBloqueado(index); return ( - -
-
- {completado ? ( - - ) : ( - {index + 1} - )} -
+ + !bloqueado && setEjercicioActivo(ejercicio.id)} + > +
+
+ {completado ? ( + + ) : bloqueado ? ( + + ) : ( + {index + 1} + )} +
-
-

{ejercicio.titulo}

-

{ejercicio.descripcion}

-
+
+

+ {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 && ( + + + + )}
-
-

¡Felicitaciones!

-

- Has completado todos los ejercicios de este módulo. -

-
-
- + + )} +
); } + +export default Modulo; diff --git a/frontend/src/pages/Modulos.tsx b/frontend/src/pages/Modulos.tsx index 338df74..7150d0c 100644 --- a/frontend/src/pages/Modulos.tsx +++ b/frontend/src/pages/Modulos.tsx @@ -1,7 +1,8 @@ import { Link } from 'react-router-dom'; import { Card } from '../components/ui/Card'; import { Button } from '../components/ui/Button'; -import { ArrowRight, ArrowLeft } from 'lucide-react'; +import { useProgresoStore } from '../stores/progresoStore'; +import { ArrowRight, ArrowLeft, CheckCircle, Lock, Play } from 'lucide-react'; const MODULOS = [ { @@ -9,28 +10,45 @@ const MODULOS = [ titulo: 'Fundamentos de Economía', descripcion: 'Aprende los conceptos básicos: definición de economía, agentes económicos, factores de producción y el flujo circular de la economía.', temas: ['Definición de economía', 'Agentes económicos', 'Factores de producción', 'Flujo circular'], + totalEjercicios: 5, + bloqueado: false, }, { numero: 2, titulo: 'Oferta, Demanda y Equilibrio', descripcion: 'Domina las curvas de oferta y demanda, aprende cómo se determinan los precios y entiende los controles de mercado.', temas: ['Curva de demanda', 'Curva de oferta', 'Equilibrio de mercado', 'Controles de precios'], + totalEjercicios: 5, + bloqueado: false, }, { numero: 3, titulo: 'Utilidad y Elasticidad', descripcion: 'Explora la teoría del consumidor, aprende a calcular elasticidades y clasifica diferentes tipos de bienes.', temas: ['Utilidad marginal', 'Elasticidad precio', 'Elasticidad ingreso', 'Clasificación de bienes'], + totalEjercicios: 5, + bloqueado: false, }, { numero: 4, titulo: 'Teoría del Productor', descripcion: 'Comprende los costos de producción, la toma de decisiones del productor y los fundamentos de la competencia perfecta.', temas: ['Costos de producción', 'Producción y costos', 'Competencia perfecta', 'Maximización de beneficios'], + totalEjercicios: 5, + bloqueado: false, }, ]; export function Modulos() { + const { progresoModulos } = useProgresoStore(); + + const getModuloProgress = (moduloNumero: number, totalEjercicios: number) => { + const progreso = progresoModulos.find(p => p.moduloNumero === moduloNumero); + const completados = progreso?.ejercicios.filter(e => e.completado).length || 0; + const porcentaje = Math.round((completados / totalEjercicios) * 100); + return { completados, porcentaje }; + }; + return (
@@ -52,42 +70,90 @@ export function Modulos() {
- {MODULOS.map((modulo) => ( - -
-
-
- {modulo.numero} + {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 ? ( + + ) : ( + + + + )}
- -
-

{modulo.titulo}

-

{modulo.descripcion}

- -
- {modulo.temas.map((tema) => ( - - {tema} - - ))} -
-
- -
- - - -
-
-
- ))} + + ); + })}
diff --git a/frontend/src/pages/Recursos.tsx b/frontend/src/pages/Recursos.tsx new file mode 100644 index 0000000..b22090c --- /dev/null +++ b/frontend/src/pages/Recursos.tsx @@ -0,0 +1,134 @@ +import { Card } from '../components/ui/Card'; +import { Button } from '../components/ui/Button'; +import { FileText, Download, BookOpen, ArrowLeft } from 'lucide-react'; +import { Link } from 'react-router-dom'; + +const recursos = [ + { + id: 1, + titulo: 'Resumen Clase 1 - Fundamentos de Economía', + descripcion: 'Definición de economía, agentes económicos, factores de producción y flujo circular', + archivo: '/pdfs/resumen_clase_1.pdf', + modulo: 'Módulo 1', + icono: FileText + }, + { + id: 2, + titulo: 'Resumen Clase 2 - Oferta, Demanda y Equilibrio', + descripcion: 'Ley de la demanda, ley de la oferta, equilibrio de mercado y controles de precios', + archivo: '/pdfs/resumen_clase_2.pdf', + modulo: 'Módulo 2', + icono: FileText + }, + { + id: 3, + titulo: 'Resumen Clase 3 - Elasticidad', + descripcion: 'Tipos de elasticidad, cálculos y clasificación de bienes según elasticidad', + archivo: '/pdfs/resumen_clase_3.pdf', + modulo: 'Módulo 3', + icono: FileText + }, + { + id: 4, + titulo: 'Resumen Clase 4 - Teoría del Productor', + descripcion: 'Costos, producción, competencia perfecta y maximización de beneficios', + archivo: '/pdfs/resumen_clase_4.pdf', + modulo: 'Módulo 4', + icono: FileText + } +]; + +export function RecursosPage() { + return ( +
+
+ {/* 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. +

+ + + +
+
+
+ ); +} + +export default RecursosPage; diff --git a/frontend/src/pages/modulos/Modulo1Page.tsx b/frontend/src/pages/modulos/Modulo1Page.tsx new file mode 100644 index 0000000..50be317 --- /dev/null +++ b/frontend/src/pages/modulos/Modulo1Page.tsx @@ -0,0 +1,364 @@ +// @ts-nocheck +import { useState, useEffect } from 'react'; +import { useParams, Link } from 'react-router-dom'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Card } from '../../components/ui/Card'; +import { Button } from '../../components/ui/Button'; +import { progresoService } from '../../services/api'; +import type { Progreso } from '../../types'; +import { ArrowLeft, CheckCircle, Play, BookOpen, Trophy, ChevronRight } from 'lucide-react'; + +// Importar contenido del módulo 1 +import { introduccion, agentes, factores } from '../../content/modulo1'; +import { ejercicios as modulo1Ejercicios } from '../../content/modulo1/ejercicios'; + +// Importar componentes de ejercicios +import { SimuladorDisyuntivas, QuizBienes, FlujoCircular } from '../../components/exercises/modulo1'; + +const TABS = ['Contenido', 'Ejercicios'] as const; +type Tab = typeof TABS[number]; + +interface EjercicioConfig { + id: string; + titulo: string; + descripcion: string; + componente: React.ReactNode; +} + +export function Modulo1Page() { + const { numero } = useParams<{ numero: string }>(); + const [activeTab, setActiveTab] = useState('Contenido'); + const [activeSeccion, setActiveSeccion] = useState<'introduccion' | 'agentes' | 'factores'>('introduccion'); + const [activeEjercicio, setActiveEjercicio] = useState(null); + const [progresos, setProgresos] = useState([]); + const [loading, setLoading] = useState(false); + + // Cargar progreso al montar + useEffect(() => { + loadProgreso(); + }, []); + + const loadProgreso = async () => { + try { + const data = await progresoService.getProgreso(); + setProgresos(data); + } catch { + // Silenciar error + } + }; + + const getProgresoForEjercicio = (ejercicioId: string) => { + return progresos.find( + (p) => p.modulo_numero === 1 && p.ejercicio_id === ejercicioId + ); + }; + + const handleCompleteEjercicio = async (ejercicioId: string, puntuacion: number) => { + setLoading(true); + try { + await progresoService.saveProgreso(ejercicioId, puntuacion); + await loadProgreso(); + } catch { + // Silenciar error + } finally { + setLoading(false); + } + }; + + // Configuración de ejercicios con sus componentes + const ejerciciosConfig: EjercicioConfig[] = [ + { + id: 'simulador-disyuntivas', + titulo: modulo1Ejercicios.ejercicios[0].titulo, + descripcion: modulo1Ejercicios.ejercicios[0].descripcion, + componente: ( + handleCompleteEjercicio('simulador-disyuntivas', puntuacion)} + /> + ), + }, + { + id: 'quiz-clasificacion-bienes', + titulo: modulo1Ejercicios.ejercicios[1].titulo, + descripcion: modulo1Ejercicios.ejercicios[1].descripcion, + componente: ( + handleCompleteEjercicio('quiz-clasificacion-bienes', puntuacion)} + /> + ), + }, + { + id: 'juego-flujo-circular', + titulo: modulo1Ejercicios.ejercicios[2].titulo, + descripcion: modulo1Ejercicios.ejercicios[2].descripcion, + componente: ( + handleCompleteEjercicio('juego-flujo-circular', puntuacion)} + /> + ), + }, + ]; + + const seccionesContenido = { + introduccion: { + titulo: introduccion.titulo, + data: introduccion, + }, + agentes: { + titulo: agentes.titulo, + data: agentes, + }, + factores: { + titulo: factores.titulo, + data: factores, + }, + }; + + const currentSeccion = seccionesContenido[activeSeccion]; + + // Calcular progreso del módulo + const ejerciciosCompletados = ejerciciosConfig.filter( + (e) => getProgresoForEjercicio(e.id)?.completado + ).length; + const porcentajeProgreso = Math.round((ejerciciosCompletados / ejerciciosConfig.length) * 100); + + return ( +
+ {/* Header */} +
+
+
+ + + Volver a Módulos + +
+ Progreso: +
+
+
+ {porcentajeProgreso}% +
+
+
+
+ + {/* Título del módulo */} +
+
+
+
+ 1 +
+
+

Módulo 1: Fundamentos de Economía

+

+ Introducción a los conceptos básicos, agentes económicos y factores de producción +

+
+
+
+
+ + {/* Tabs */} +
+
+ {TABS.map((tab) => ( + + ))} +
+ + {/* Contenido según tab activo */} + + {activeTab === 'Contenido' ? ( + + {/* Navegación de secciones */} +
+ +

Secciones

+ +
+
+ + {/* Contenido de la sección */} +
+ +

{currentSeccion.titulo}

+
+ {currentSeccion.data.contenido.map((seccion, index) => ( +
+

{seccion.titulo}

+
+ {seccion.contenido.split('\n\n').map((parrafo, pIndex) => ( +

+ {parrafo} +

+ ))} +
+
+ ))} +
+
+ + {/* Ejercicios relacionados con la sección */} + {currentSeccion.data.ejercicios && currentSeccion.data.ejercicios.length > 0 && ( + +

Ejercicios Relacionados

+

+ Practica lo aprendido con estos ejercicios interactivos +

+ +
+ )} +
+
+ ) : ( + + {activeEjercicio ? ( + // Vista de ejercicio activo +
+
+ + {loading && Guardando progreso...} +
+ {ejerciciosConfig.find((e) => e.id === activeEjercicio)?.componente} +
+ ) : ( + // Lista de ejercicios +
+ {ejerciciosConfig.map((ejercicio, index) => { + const progreso = getProgresoForEjercicio(ejercicio.id); + const completado = progreso?.completado || false; + + return ( + setActiveEjercicio(ejercicio.id)} + > +
+
+ {completado ? ( + + ) : ( + {index + 1} + )} +
+
+

{ejercicio.titulo}

+

{ejercicio.descripcion}

+ {completado && progreso && ( +
+ + Completado + + + {progreso.puntuacion} pts + +
+ )} +
+
+ +
+ ); + })} +
+ )} + + {/* Mensaje de completado */} + {ejerciciosCompletados === ejerciciosConfig.length && ejerciciosConfig.length > 0 && ( + +
+
+ +
+
+

¡Felicitaciones!

+

+ Has completado todos los ejercicios de este módulo. +

+
+
+
+ )} +
+ )} +
+
+
+ ); +} + +export default Modulo1Page; diff --git a/frontend/src/pages/modulos/Modulo2Page.tsx b/frontend/src/pages/modulos/Modulo2Page.tsx new file mode 100644 index 0000000..aca2778 --- /dev/null +++ b/frontend/src/pages/modulos/Modulo2Page.tsx @@ -0,0 +1,499 @@ +// @ts-nocheck +import { useState, useEffect } from 'react'; +import { useParams, Link } from 'react-router-dom'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Card } from '../../components/ui/Card'; +import { Button } from '../../components/ui/Button'; +import { progresoService } from '../../services/api'; +import type { Progreso } from '../../types'; +import { ArrowLeft, CheckCircle, Play, BookOpen, Trophy, ChevronRight } from 'lucide-react'; + +// Importar contenido del módulo 2 +import { default as demandaContent } from '../../content/modulo2/demanda'; +import { default as ofertaContent } from '../../content/modulo2/oferta'; +import { default as equilibrioContent } from '../../content/modulo2/equilibrio'; + +// Importar componentes de ejercicios +import { ConstructorCurvas, SimuladorPrecios, IdentificarShocks } from '../../components/exercises/modulo2'; + +const TABS = ['Contenido', 'Ejercicios'] as const; +type Tab = typeof TABS[number]; + +interface EjercicioConfig { + id: string; + titulo: string; + descripcion: string; + componente: React.ReactNode; +} + +export function Modulo2Page() { + const { numero } = useParams<{ numero: string }>(); + const [activeTab, setActiveTab] = useState('Contenido'); + const [activeSeccion, setActiveSeccion] = useState<'demanda' | 'oferta' | 'equilibrio'>('demanda'); + const [activeEjercicio, setActiveEjercicio] = useState(null); + const [progresos, setProgresos] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + loadProgreso(); + }, []); + + const loadProgreso = async () => { + try { + const data = await progresoService.getProgreso(); + setProgresos(data); + } catch { + // Silenciar error + } + }; + + const getProgresoForEjercicio = (ejercicioId: string) => { + return progresos.find( + (p) => p.modulo_numero === 2 && p.ejercicio_id === ejercicioId + ); + }; + + const handleCompleteEjercicio = async (ejercicioId: string, puntuacion: number) => { + setLoading(true); + try { + await progresoService.saveProgreso(ejercicioId, puntuacion); + await loadProgreso(); + } catch { + // Silenciar error + } finally { + setLoading(false); + } + }; + + // Configuración de ejercicios + const ejerciciosConfig: EjercicioConfig[] = [ + { + id: 'constructor-curvas', + titulo: 'Constructor de Curvas de Oferta y Demanda', + descripcion: 'Construye curvas de oferta y demanda arrastrando puntos para entender sus pendientes y movimientos.', + componente: ( + handleCompleteEjercicio('constructor-curvas', puntuacion)} + /> + ), + }, + { + id: 'simulador-precios', + titulo: 'Simulador de Precios Intervenidos', + descripcion: 'Ajusta precios máximos y mínimos para observar sus efectos en el mercado: escasez, superávit, y pérdida de bienestar.', + componente: ( + handleCompleteEjercicio('simulador-precios', puntuacion)} + /> + ), + }, + { + id: 'identificar-shocks', + titulo: 'Identificador de Shocks del Mercado', + descripcion: 'Analiza escenarios económicos reales e identifica si afectan la oferta, la demanda, ambas, y cómo cambian precio y cantidad de equilibrio.', + componente: ( + handleCompleteEjercicio('identificar-shocks', puntuacion)} + /> + ), + }, + ]; + + // Estructura de contenido del módulo 2 + const seccionesContenido = { + demanda: { + titulo: 'Ley de la Demanda', + contenido: [ + { + titulo: demandaContent.definicion.titulo, + texto: demandaContent.definicion.definicion, + elementos: demandaContent.definicion.elementosClave, + }, + { + titulo: demandaContent.ley.titulo, + texto: demandaContent.ley.enunciado, + efectos: demandaContent.ley.efectos, + }, + { + titulo: 'Factores que Desplazan la Demanda', + texto: 'Los siguientes factores causan desplazamientos de la curva de demanda:', + factores: demandaContent.factores, + }, + ], + }, + oferta: { + titulo: 'Ley de la Oferta', + contenido: [ + { + titulo: ofertaContent.definicion.titulo, + texto: ofertaContent.definicion.definicion, + elementos: ofertaContent.definicion.elementosClave, + }, + { + titulo: ofertaContent.ley.titulo, + texto: ofertaContent.ley.enunciado, + razones: ofertaContent.ley.razones, + }, + { + titulo: 'Factores que Desplazan la Oferta', + texto: 'Los siguientes factores causan desplazamientos de la curva de oferta:', + factores: ofertaContent.factores, + }, + ], + }, + equilibrio: { + titulo: 'Equilibrio de Mercado', + contenido: [ + { + titulo: equilibrioContent.definicion.titulo, + texto: equilibrioContent.definicion.definicion, + caracteristicas: equilibrioContent.definicion.caracteristicas, + }, + { + titulo: 'Excedentes del Mercado', + texto: 'En el equilibrio se generan beneficios para consumidores y productores:', + excedentes: [ + equilibrioContent.excedentes.excedenteConsumidor, + equilibrioContent.excedentes.excedenteProductor, + ], + }, + { + titulo: 'Controles de Precio', + texto: 'Los gobiernos pueden intervenir el mercado estableciendo precios máximos o mínimos:', + controles: [ + equilibrioContent.controles.precioMaximo, + equilibrioContent.controles.precioMinimo, + ], + }, + ], + }, + }; + + const currentSeccion = seccionesContenido[activeSeccion]; + + // Calcular progreso + const ejerciciosCompletados = ejerciciosConfig.filter( + (e) => getProgresoForEjercicio(e.id)?.completado + ).length; + const porcentajeProgreso = Math.round((ejerciciosCompletados / ejerciciosConfig.length) * 100); + + return ( +
+ {/* Header */} +
+
+
+ + + Volver a Módulos + +
+ Progreso: +
+
+
+ {porcentajeProgreso}% +
+
+
+
+ + {/* Título del módulo */} +
+
+
+
+ 2 +
+
+

Módulo 2: Oferta, Demanda y Equilibrio

+

+ Curvas de oferta y demanda, equilibrio de mercado y controles de precio +

+
+
+
+
+ + {/* Tabs */} +
+
+ {TABS.map((tab) => ( + + ))} +
+ + {/* Contenido según tab activo */} + + {activeTab === 'Contenido' ? ( + + {/* Navegación de secciones */} +
+ +

Secciones

+ +
+
+ + {/* Contenido de la sección */} +
+ +

{currentSeccion.titulo}

+
+ {currentSeccion.contenido.map((item, index) => ( +
+

{item.titulo}

+

{item.texto}

+ + {/* Mostrar elementos clave si existen */} + {item.elementos && ( +
    + {item.elementos.map((el: any, i: number) => ( +
  • + {el.elemento}: {el.descripcion} +
  • + ))} +
+ )} + + {/* Mostrar efectos/razones si existen */} + {item.efectos && ( +
+ {item.efectos.map((efecto: any, i: number) => ( +
+

{efecto.nombre}

+

{efecto.descripcion}

+

Ejemplo: {efecto.ejemplo}

+
+ ))} +
+ )} + + {/* Mostrar razones si existen */} + {item.razones && ( +
+ {item.razones.map((razon: any, i: number) => ( +
+

{razon.nombre}

+

{razon.descripcion}

+

Ejemplo: {razon.ejemplo}

+
+ ))} +
+ )} + + {/* Mostrar factores si existen */} + {item.factores && ( +
+ {item.factores.map((factor: any, i: number) => ( +
+
+ {factor.icono} +

{factor.nombre}

+
+

{factor.descripcion}

+

{factor.ejemplo}

+
+ ))} +
+ )} + + {/* Mostrar características si existen */} + {item.caracteristicas && ( +
    + {item.caracteristicas.map((car: any, i: number) => ( +
  • + {car.caracteristica}: {car.explicacion} +
  • + ))} +
+ )} + + {/* Mostrar excedentes si existen */} + {item.excedentes && ( +
+ {item.excedentes.map((exc: any, i: number) => ( +
+

{exc.nombre}

+

{exc.definicion}

+

Fórmula: {exc.formula}

+
+ ))} +
+ )} + + {/* Mostrar controles si existen */} + {item.controles && ( +
+ {item.controles.map((control: any, i: number) => ( +
+

{control.nombre}

+

{control.definicion}

+

+ Condición: {control.condicionEfectivo} +

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

Ejercicios Relacionados

+

+ Pon a prueba tus conocimientos con ejercicios interactivos sobre oferta, demanda y equilibrio +

+ +
+
+
+ ) : ( + + {activeEjercicio ? ( +
+
+ + {loading && Guardando progreso...} +
+ {ejerciciosConfig.find((e) => e.id === activeEjercicio)?.componente} +
+ ) : ( +
+ {ejerciciosConfig.map((ejercicio, index) => { + const progreso = getProgresoForEjercicio(ejercicio.id); + const completado = progreso?.completado || false; + + return ( + setActiveEjercicio(ejercicio.id)} + > +
+
+ {completado ? ( + + ) : ( + {index + 1} + )} +
+
+

{ejercicio.titulo}

+

{ejercicio.descripcion}

+ {completado && progreso && ( +
+ + Completado + + + {progreso.puntuacion} pts + +
+ )} +
+
+ +
+ ); + })} +
+ )} + + {ejerciciosCompletados === ejerciciosConfig.length && ejerciciosConfig.length > 0 && ( + +
+
+ +
+
+

¡Felicitaciones!

+

+ Has completado todos los ejercicios de este módulo. +

+
+
+
+ )} +
+ )} +
+
+
+ ); +} + +export default Modulo2Page; diff --git a/frontend/src/pages/modulos/Modulo3Page.tsx b/frontend/src/pages/modulos/Modulo3Page.tsx new file mode 100644 index 0000000..3142ae3 --- /dev/null +++ b/frontend/src/pages/modulos/Modulo3Page.tsx @@ -0,0 +1,610 @@ +// @ts-nocheck +import { useState, useEffect } from 'react'; +import { useParams, Link } from 'react-router-dom'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Card } from '../../components/ui/Card'; +import { Button } from '../../components/ui/Button'; +import { progresoService } from '../../services/api'; +import type { Progreso } from '../../types'; +import { ArrowLeft, CheckCircle, Play, BookOpen, Trophy, ChevronRight } from 'lucide-react'; + +// Importar contenido del módulo 3 +import { conceptosElasticidad } from '../../content/modulo3/conceptos'; +import { tiposElasticidad } from '../../content/modulo3/tipos'; +import { clasificacionBienes } from '../../content/modulo3/clasificacion'; +import { ejercicios as modulo3Ejercicios } from '../../content/modulo3/ejercicios'; + +// Importar componentes de ejercicios +import { CalculadoraElasticidad, ClasificadorBienes, EjerciciosExamen } from '../../components/exercises/modulo3'; + +const TABS = ['Contenido', 'Ejercicios'] as const; +type Tab = typeof TABS[number]; + +interface EjercicioConfig { + id: string; + titulo: string; + descripcion: string; + componente: React.ReactNode; +} + +interface ContenidoItem { + titulo: string; + texto: string; + interpretacion?: string; + formula?: { latex?: string; ecuacion?: string } | string; + interpretaciones?: Array<{ + rango: string; + clasificacion: string; + significado?: string; + descripcion?: string; + ejemplo?: string; + }>; + factores?: Array<{ + factor?: string; + nombre?: string; + efecto?: string; + descripcion?: string; + explicacion?: string; + }>; + reglas?: Array<{ + elasticidad: string; + efectoPrecioArriba: string; + efectoPrecioAbajo: string; + }>; + clasificacion?: Array<{ + tipo: string; + descripcion: string; + condicion: string; + subtipos?: Array<{ tipo: string; rango: string }>; + }>; + categorias?: Array<{ + tipo: string; + descripcion: string; + condicion: string; + ejemplos?: string[]; + }>; + matriz?: Array<{ + combinacion: string; + ejemplo: string; + caracteristicas: string; + }>; + determinantes?: string[]; +} + +export function Modulo3Page() { + const { numero } = useParams<{ numero: string }>(); + const [activeTab, setActiveTab] = useState('Contenido'); + const [activeSeccion, setActiveSeccion] = useState<'conceptos' | 'tipos' | 'clasificacion'>('conceptos'); + const [activeEjercicio, setActiveEjercicio] = useState(null); + const [progresos, setProgresos] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + loadProgreso(); + }, []); + + const loadProgreso = async () => { + try { + const data = await progresoService.getProgreso(); + setProgresos(data); + } catch { + // Silenciar error + } + }; + + const getProgresoForEjercicio = (ejercicioId: string) => { + return progresos.find( + (p) => p.modulo_numero === 3 && p.ejercicio_id === ejercicioId + ); + }; + + const handleCompleteEjercicio = async (ejercicioId: string, puntuacion: number) => { + setLoading(true); + try { + await progresoService.saveProgreso(ejercicioId, puntuacion); + await loadProgreso(); + } catch { + // Silenciar error + } finally { + setLoading(false); + } + }; + + // Configuración de ejercicios + const ejerciciosConfig: EjercicioConfig[] = [ + { + id: 'calculadora-elasticidad', + titulo: 'Calculadora de Elasticidad', + descripcion: 'Calcula paso a paso la elasticidad precio, ingreso y cruzada con ejemplos prácticos.', + componente: ( + handleCompleteEjercicio('calculadora-elasticidad', puntuacion)} + /> + ), + }, + { + id: 'clasificador-bienes', + titulo: 'Clasificador de Bienes', + descripcion: 'Clasifica bienes según su elasticidad ingreso y cruzada. Identifica normales, inferiores, lujos, sustitutos y complementos.', + componente: ( + handleCompleteEjercicio('clasificador-bienes', puntuacion)} + /> + ), + }, + { + id: 'ejercicios-examen', + titulo: 'Ejercicios Tipo Examen', + descripcion: 'Resuelve problemas integradores de elasticidad con dificultad de examen.', + componente: ( + handleCompleteEjercicio('ejercicios-examen', puntuacion)} + /> + ), + }, + ]; + + // Estructura de contenido del módulo 3 + const seccionesContenido: { + conceptos: { titulo: string; contenido: ContenidoItem[] }; + tipos: { titulo: string; contenido: ContenidoItem[] }; + clasificacion: { titulo: string; contenido: ContenidoItem[] }; + } = { + conceptos: { + titulo: 'Conceptos Fundamentales', + contenido: [ + { + titulo: conceptosElasticidad.definicionElasticidad.titulo, + texto: conceptosElasticidad.definicionElasticidad.definicion, + interpretacion: conceptosElasticidad.definicionElasticidad.interpretacionIntuitiva, + formula: conceptosElasticidad.definicionElasticidad.formulaGeneral, + }, + { + titulo: conceptosElasticidad.elasticidadPrecioDemanda.titulo, + texto: conceptosElasticidad.elasticidadPrecioDemanda.definicion, + interpretaciones: conceptosElasticidad.elasticidadPrecioDemanda.interpretacion, + }, + { + titulo: conceptosElasticidad.determinantesElasticidad.titulo, + texto: 'Los siguientes factores determinan la elasticidad de un bien:', + factores: conceptosElasticidad.determinantesElasticidad.factores, + }, + { + titulo: conceptosElasticidad.relacionIngresoTotal.titulo, + texto: conceptosElasticidad.relacionIngresoTotal.definicion, + reglas: conceptosElasticidad.relacionIngresoTotal.reglas, + }, + ], + }, + tipos: { + titulo: 'Tipos de Elasticidad', + contenido: [ + { + titulo: 'Elasticidad Precio de la Demanda (Ed)', + texto: tiposElasticidad.tipos[0].descripcion, + formula: tiposElasticidad.tipos[0].formula, + determinantes: tiposElasticidad.tipos[0].determinantes, + }, + { + titulo: 'Elasticidad Ingreso de la Demanda (Ei)', + texto: tiposElasticidad.tipos[1].descripcion, + formula: tiposElasticidad.tipos[1].formula, + clasificacion: tiposElasticidad.tipos[1].clasificacionBienes, + }, + { + titulo: 'Elasticidad Cruzada (Exy)', + texto: tiposElasticidad.tipos[2].descripcion, + formula: tiposElasticidad.tipos[2].formula, + categorias: tiposElasticidad.tipos[2].clasificacionBienes, + }, + { + titulo: 'Elasticidad Precio de la Oferta (Es)', + texto: tiposElasticidad.tipos[3].descripcion, + formula: tiposElasticidad.tipos[3].formula, + interpretacion: tiposElasticidad.tipos[3].interpretacion, + }, + ], + }, + clasificacion: { + titulo: 'Clasificación de Bienes', + contenido: [ + { + titulo: 'Según Elasticidad Ingreso', + texto: clasificacionBienes.clasificacionPorIngreso.descripcion, + formula: clasificacionBienes.clasificacionPorIngreso.formulaReferencia, + categorias: clasificacionBienes.clasificacionPorIngreso.categorias, + }, + { + titulo: 'Según Elasticidad Cruzada', + texto: clasificacionBienes.clasificacionPorElasticidadCruzada.descripcion, + formula: clasificacionBienes.clasificacionPorElasticidadCruzada.formulaReferencia, + categorias: clasificacionBienes.clasificacionPorElasticidadCruzada.categorias, + }, + { + titulo: 'Matriz de Clasificación Completa', + texto: clasificacionBienes.matrizClasificacionCompleta.descripcion, + matriz: clasificacionBienes.matrizClasificacionCompleta.matriz, + }, + ], + }, + }; + + const currentSeccion = seccionesContenido[activeSeccion]; + + // Calcular progreso + const ejerciciosCompletados = ejerciciosConfig.filter( + (e) => getProgresoForEjercicio(e.id)?.completado + ).length; + const porcentajeProgreso = Math.round((ejerciciosCompletados / ejerciciosConfig.length) * 100); + + return ( +
+ {/* Header */} +
+
+
+ + + Volver a Módulos + +
+ Progreso: +
+
+
+ {porcentajeProgreso}% +
+
+
+
+ + {/* Título del módulo */} +
+
+
+
+ 3 +
+
+

Módulo 3: Elasticidad

+

+ Tipos de elasticidad, clasificación de bienes y análisis de sensibilidad +

+
+
+
+
+ + {/* Tabs */} +
+
+ {TABS.map((tab) => ( + + ))} +
+ + {/* Contenido según tab activo */} + + {activeTab === 'Contenido' ? ( + + {/* Navegación de secciones */} +
+ +

Secciones

+ +
+
+ + {/* Contenido de la sección */} +
+ +

{currentSeccion.titulo}

+
+ {currentSeccion.contenido.map((item, index) => ( +
+

{item.titulo}

+

{item.texto}

+ + {/* Mostrar interpretación si existe */} + {item.interpretacion && ( +
+

{item.interpretacion}

+
+ )} + + {/* Mostrar fórmula si existe */} + {item.formula && ( +
+
+                              {item.formula.latex || item.formula.ecuacion || item.formula}
+                            
+
+ )} + + {/* Mostrar interpretaciones de elasticidad */} + {item.interpretaciones && ( +
+ {item.interpretaciones.map((interp: any, i: number) => ( +
1' || interp.rango === 'Es > 1' + ? 'bg-green-50 border-green-200' + : interp.rango === '|Ed| < 1' || interp.rango === 'Es < 1' + ? 'bg-yellow-50 border-yellow-200' + : 'bg-blue-50 border-blue-200' + }`}> +

{interp.clasificacion}

+

{interp.significado || interp.descripcion}

+ {interp.ejemplo && ( +

Ejemplo: {interp.ejemplo}

+ )} +
+ ))} +
+ )} + + {/* Mostrar factores determinantes */} + {item.factores && ( +
+ {item.factores.map((factor: any, i: number) => ( +
+
+

{factor.factor || factor.nombre}

+

{factor.efecto || factor.descripcion}

+ {factor.explicacion && ( +

{factor.explicacion}

+ )} +
+
+ ))} +
+ )} + + {/* Mostrar reglas de ingreso total */} + {item.reglas && ( +
+ {item.reglas.map((regla: any, i: number) => ( +
+

{regla.elasticidad}

+
+

Precio ↑: {regla.efectoPrecioArriba}

+

Precio ↓: {regla.efectoPrecioAbajo}

+
+
+ ))} +
+ )} + + {/* Mostrar clasificación de bienes por ingreso */} + {item.clasificacion && item.titulo?.includes('Ingreso') && ( +
+ {item.clasificacion.map((cat: any, i: number) => ( +
0') && !cat.condicion?.includes('<') + ? 'bg-green-50 border-green-200' + : 'bg-red-50 border-red-200' + }`}> +

{cat.tipo}

+

{cat.descripcion}

+

Condición: {cat.condicion}

+ {cat.subtipos && ( +
+ {cat.subtipos.map((sub: any, j: number) => ( +
+ {sub.tipo}: {sub.rango} +
+ ))} +
+ )} +
+ ))} +
+ )} + + {/* Mostrar clasificación por elasticidad cruzada */} + {item.categorias && item.titulo?.includes('Cruzada') && ( +
+ {item.categorias.map((cat: any, i: number) => ( +
0') + ? 'bg-green-50 border-green-200' + : cat.condicion?.includes('< 0') + ? 'bg-red-50 border-red-200' + : 'bg-gray-50 border-gray-200' + }`}> +

{cat.tipo}

+

{cat.descripcion}

+

Exy {cat.condicion}

+ {cat.ejemplos && ( +

+ Ejemplos: {cat.ejemplos.slice(0, 3).join(', ')} +

+ )} +
+ ))} +
+ )} + + {/* Mostrar matriz de clasificación */} + {item.matriz && ( +
+ + + + + + + + + + {item.matriz.map((fila: any, i: number) => ( + + + + + + ))} + +
CombinaciónEjemploCaracterísticas
{fila.combinacion}{fila.ejemplo}{fila.caracteristicas}
+
+ )} +
+ ))} +
+
+ + +

Ejercicios Relacionados

+

+ Practica el cálculo y clasificación de elasticidad con ejercicios interactivos +

+ +
+
+
+ ) : ( + + {activeEjercicio ? ( +
+
+ + {loading && Guardando progreso...} +
+ {ejerciciosConfig.find((e) => e.id === activeEjercicio)?.componente} +
+ ) : ( +
+ {ejerciciosConfig.map((ejercicio, index) => { + const progreso = getProgresoForEjercicio(ejercicio.id); + const completado = progreso?.completado || false; + + return ( + setActiveEjercicio(ejercicio.id)} + > +
+
+ {completado ? ( + + ) : ( + {index + 1} + )} +
+
+

{ejercicio.titulo}

+

{ejercicio.descripcion}

+ {completado && progreso && ( +
+ + Completado + + + {progreso.puntuacion} pts + +
+ )} +
+
+ +
+ ); + })} +
+ )} + + {ejerciciosCompletados === ejerciciosConfig.length && ejerciciosConfig.length > 0 && ( + +
+
+ +
+
+

¡Felicitaciones!

+

+ Has completado todos los ejercicios de este módulo. +

+
+
+
+ )} +
+ )} +
+
+
+ ); +} + +export default Modulo3Page; diff --git a/frontend/src/pages/modulos/Modulo4Page.tsx b/frontend/src/pages/modulos/Modulo4Page.tsx new file mode 100644 index 0000000..8ba4a28 --- /dev/null +++ b/frontend/src/pages/modulos/Modulo4Page.tsx @@ -0,0 +1,423 @@ +// @ts-nocheck +import { useState, useEffect } from 'react'; +import { useParams, Link } from 'react-router-dom'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Card } from '../../components/ui/Card'; +import { Button } from '../../components/ui/Button'; +import { progresoService } from '../../services/api'; +import type { Progreso } from '../../types'; +import { ArrowLeft, CheckCircle, Play, BookOpen, Trophy, ChevronRight } from 'lucide-react'; + +// Importar contenido del módulo 4 +import { produccion } from '../../content/modulo4/produccion'; +import { costos } from '../../content/modulo4/costos'; +import { mercado } from '../../content/modulo4/mercado'; + +// Importar componentes de ejercicios +import { CalculadoraCostos, SimuladorProduccion, VisualizadorExcedentes } from '../../components/exercises/modulo4'; + +const TABS = ['Contenido', 'Ejercicios'] as const; +type Tab = typeof TABS[number]; + +interface EjercicioConfig { + id: string; + titulo: string; + descripcion: string; + componente: React.ReactNode; +} + +export function Modulo4Page() { + const { numero: _numero } = useParams<{ numero: string }>(); + const [activeTab, setActiveTab] = useState('Contenido'); + const [activeSeccion, setActiveSeccion] = useState<'produccion' | 'costos' | 'mercado'>('produccion'); + const [activeEjercicio, setActiveEjercicio] = useState(null); + const [progresos, setProgresos] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + loadProgreso(); + }, []); + + const loadProgreso = async () => { + try { + const data = await progresoService.getProgreso(); + setProgresos(data); + } catch { + // Silenciar error + } + }; + + const getProgresoForEjercicio = (ejercicioId: string) => { + return progresos.find( + (p) => p.modulo_numero === 4 && p.ejercicio_id === ejercicioId + ); + }; + + const handleCompleteEjercicio = async (ejercicioId: string, puntuacion: number) => { + setLoading(true); + try { + await progresoService.saveProgreso(ejercicioId, puntuacion); + await loadProgreso(); + } catch { + // Silenciar error + } finally { + setLoading(false); + } + }; + + // Configuración de ejercicios + const ejerciciosConfig: EjercicioConfig[] = [ + { + id: 'calculadora-costos', + titulo: 'Calculadora de Costos', + descripcion: 'Ingresa CF, CV para cada nivel de producción y calcula automáticamente todos los costos medios y marginales.', + componente: ( + handleCompleteEjercicio('calculadora-costos', 100)} + /> + ), + }, + { + id: 'simulador-produccion', + titulo: 'Simulador de Producción', + descripcion: 'Dado un precio de mercado y curva de costos, encuentra la cantidad óptima y determina si debes producir o cerrar.', + componente: ( + handleCompleteEjercicio('simulador-produccion', 100)} + /> + ), + }, + { + id: 'visualizador-excedentes', + titulo: 'Visualizador de Excedentes', + descripcion: 'Interactúa con el gráfico para ver cómo cambia el excedente del productor al variar el precio y la cantidad.', + componente: ( + handleCompleteEjercicio('visualizador-excedentes', 100)} + /> + ), + }, + ]; + + // Estructura de contenido del módulo 4 + const seccionesContenido = { + produccion: { + titulo: produccion.titulo, + contenido: produccion.contenido, + }, + costos: { + titulo: costos.titulo, + contenido: costos.contenido, + }, + mercado: { + titulo: mercado.titulo, + contenido: mercado.contenido, + }, + }; + + const currentSeccion = seccionesContenido[activeSeccion]; + + // Calcular progreso + const ejerciciosCompletados = ejerciciosConfig.filter( + (e) => getProgresoForEjercicio(e.id)?.completado + ).length; + const porcentajeProgreso = Math.round((ejerciciosCompletados / ejerciciosConfig.length) * 100); + + // Función auxiliar para renderizar contenido markdown-like + const renderContenido = (texto: string) => { + const partes = texto.split(/(\*\*.*?\*\*|\$.*?\$|`.*?`|\n\n)/); + return partes.map((parte, index) => { + if (parte.startsWith('**') && parte.endsWith('**')) { + return {parte.slice(2, -2)}; + } + if (parte.startsWith('$') && parte.endsWith('$')) { + return {parte.slice(1, -1)}; + } + if (parte.startsWith('`') && parte.endsWith('`')) { + return {parte.slice(1, -1)}; + } + if (parte === '\n\n') { + return
; + } + return {parte}; + }); + }; + + return ( +
+ {/* Header */} +
+
+
+ + + Volver a Módulos + +
+ Progreso: +
+
+
+ {porcentajeProgreso}% +
+
+
+
+ + {/* Título del módulo */} +
+
+
+
+ 4 +
+
+

Módulo 4: Teoría del Productor

+

+ Producción, costos y competencia perfecta +

+
+
+
+
+ + {/* Tabs */} +
+
+ {TABS.map((tab) => ( + + ))} +
+ + {/* Contenido según tab activo */} + + {activeTab === 'Contenido' ? ( + + {/* Navegación de secciones */} +
+ +

Secciones

+ +
+
+ + {/* Contenido de la sección */} +
+ +

{currentSeccion.titulo}

+
+ {currentSeccion.contenido.map((seccion, index) => ( +
+

{seccion.titulo}

+
+ {seccion.contenido.split('\n\n').map((parrafo, pIndex) => { + // Detectar si es una tabla + if (parrafo.includes('|') && parrafo.includes('---')) { + const lineas = parrafo.split('\n').filter(l => l.trim() && !l.includes('---')); + if (lineas.length >= 2) { + return ( +
+ + + + {lineas[0].split('|').filter(Boolean).map((cell, cIndex) => ( + + ))} + + + + {lineas.slice(1).map((linea, lIndex) => ( + + {linea.split('|').filter(Boolean).map((cell, cIndex) => ( + + ))} + + ))} + +
+ {cell.trim()} +
+ {cell.trim()} +
+
+ ); + } + } + + // Detectar si es código + if (parrafo.startsWith('```')) { + return ( +
+                                  {parrafo.replace(/```/g, '').trim()}
+                                
+ ); + } + + return ( +

+ {renderContenido(parrafo)} +

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

Ejercicios Relacionados

+

+ Practica con simuladores interactivos de costos, producción y excedentes +

+ +
+
+
+ ) : ( + + {activeEjercicio ? ( +
+
+ + {loading && Guardando progreso...} +
+ {ejerciciosConfig.find((e) => e.id === activeEjercicio)?.componente} +
+ ) : ( +
+ {ejerciciosConfig.map((ejercicio, index) => { + const progreso = getProgresoForEjercicio(ejercicio.id); + const completado = progreso?.completado || false; + + return ( + setActiveEjercicio(ejercicio.id)} + > +
+
+ {completado ? ( + + ) : ( + {index + 1} + )} +
+
+

{ejercicio.titulo}

+

{ejercicio.descripcion}

+ {completado && progreso && ( +
+ + Completado + + + {progreso.puntuacion} pts + +
+ )} +
+
+ +
+ ); + })} +
+ )} + + {ejerciciosCompletados === ejerciciosConfig.length && ejerciciosConfig.length > 0 && ( + +
+
+ +
+
+

¡Felicitaciones!

+

+ Has completado todos los ejercicios de este módulo. +

+
+
+
+ )} +
+ )} +
+
+
+ ); +} + +export default Modulo4Page; diff --git a/frontend/src/pages/modulos/index.ts b/frontend/src/pages/modulos/index.ts new file mode 100644 index 0000000..4ec1d2e --- /dev/null +++ b/frontend/src/pages/modulos/index.ts @@ -0,0 +1,4 @@ +export { Modulo1Page } from './Modulo1Page'; +export { Modulo2Page } from './Modulo2Page'; +export { Modulo3Page } from './Modulo3Page'; +export { Modulo4Page } from './Modulo4Page'; diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 0a57f9c..64b9dc3 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -1,5 +1,5 @@ import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios'; -import type { LoginRequest, LoginResponse, Usuario, Progreso, Modulo } from '../types'; +import type { LoginRequest, LoginResponse, Usuario, Progreso, Modulo, ProgresoModulo, Badge, ResumenProgreso } from '../types'; const API_BASE_URL = '/api'; @@ -96,8 +96,8 @@ export const progresoService = { return response.data; }, - async saveProgreso(progreso: Progreso): Promise { - const response = await api.post('/progreso', progreso); + async saveProgreso(data: { modulo_numero: number; ejercicio_id: string; puntuacion: number; completado?: boolean }): Promise { + const response = await api.post('/progreso', data); return response.data; }, @@ -105,16 +105,36 @@ export const progresoService = { const response = await api.get(`/admin/usuarios/${userId}/progreso`); return response.data; }, + + async getResumen(): Promise { + const response = await api.get('/progreso/resumen'); + return response.data; + }, + + async getResumenProgreso(): Promise { + const response = await api.get('/progreso/resumen'); + return response.data; + }, + + async getProgresoModulos(): Promise { + const response = await api.get('/progreso/modulos'); + return response.data; + }, + + async getBadges(): Promise { + const response = await api.get('/progreso/badges'); + return response.data; + }, }; export const moduloService = { async getModulos(): Promise { - const response = await api.get('/modulos'); + const response = await api.get('/contenido/modulos'); return response.data; }, async getModulo(numero: number): Promise { - const response = await api.get(`/modulos/${numero}`); + const response = await api.get(`/contenido/modulos/${numero}`); return response.data; }, }; diff --git a/frontend/src/stores/authStore.ts b/frontend/src/stores/authStore.ts index b771158..b4e596f 100644 --- a/frontend/src/stores/authStore.ts +++ b/frontend/src/stores/authStore.ts @@ -66,7 +66,10 @@ export const useAuthStore = create()( }), { name: 'auth-storage', - partialize: (state) => ({ isAuthenticated: state.isAuthenticated }), + partialize: (state) => ({ + isAuthenticated: state.isAuthenticated, + usuario: state.usuario + }), } ) ); diff --git a/frontend/src/stores/progresoStore.ts b/frontend/src/stores/progresoStore.ts new file mode 100644 index 0000000..9f558b9 --- /dev/null +++ b/frontend/src/stores/progresoStore.ts @@ -0,0 +1,134 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; +import type { ProgresoEjercicio, ProgresoModulo, Badge, NivelUsuario } from '../types'; +import { progresoService } from '../services/api'; + +interface ProgresoState { + progresoModulos: ProgresoModulo[]; + badges: Badge[]; + totalPuntuacion: number; + nivel: NivelUsuario; + isLoading: boolean; + error: string | null; + + // Actions + loadProgreso: () => Promise; + saveProgreso: (ejercicioId: string, puntuacion: number, moduloNumero: number) => Promise; + getProgresoModulo: (moduloNumero: number) => ProgresoModulo | undefined; + getEjercicioCompletado: (ejercicioId: string, moduloNumero: number) => boolean; + getPuntuacionEjercicio: (ejercicioId: string, moduloNumero: number) => number; + getPorcentajeModulo: (moduloNumero: number, totalEjercicios: number) => number; + clearError: () => void; +} + +export const useProgresoStore = create()( + persist( + (set, get) => ({ + progresoModulos: [], + badges: [], + totalPuntuacion: 0, + nivel: 'Novato', + isLoading: false, + error: null, + + loadProgreso: async () => { + set({ isLoading: true, error: null }); + try { + const resumen = await progresoService.getResumen(); + set({ + progresoModulos: resumen.moduloProgreso, + badges: resumen.badges, + totalPuntuacion: resumen.totalPuntuacion, + nivel: resumen.nivel, + isLoading: false, + }); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Error al cargar progreso'; + set({ error: message, isLoading: false }); + } + }, + + saveProgreso: async (ejercicioId: string, puntuacion: number, moduloNumero: number) => { + set({ isLoading: true, error: null }); + try { + await progresoService.saveProgreso({ + modulo_numero: moduloNumero, + ejercicio_id: ejercicioId, + puntuacion, + completado: true, + }); + + // Update local state + const { progresoModulos } = get(); + const moduloIndex = progresoModulos.findIndex(m => m.moduloNumero === moduloNumero); + + if (moduloIndex >= 0) { + const nuevoEjercicio: ProgresoEjercicio = { + ejercicioId, + completado: true, + puntuacion, + fecha: new Date().toISOString(), + }; + + const moduloActualizado = { ...progresoModulos[moduloIndex] }; + const ejercicioExistente = moduloActualizado.ejercicios.findIndex( + e => e.ejercicioId === ejercicioId + ); + + if (ejercicioExistente >= 0) { + moduloActualizado.ejercicios[ejercicioExistente] = nuevoEjercicio; + } else { + moduloActualizado.ejercicios.push(nuevoEjercicio); + } + + const nuevosProgresos = [...progresoModulos]; + nuevosProgresos[moduloIndex] = moduloActualizado; + + set({ progresoModulos: nuevosProgresos, isLoading: false }); + } + + // Reload full progress to get updated badges and level + await get().loadProgreso(); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Error al guardar progreso'; + set({ error: message, isLoading: false }); + } + }, + + getProgresoModulo: (moduloNumero: number) => { + return get().progresoModulos.find(m => m.moduloNumero === moduloNumero); + }, + + getEjercicioCompletado: (ejercicioId: string, moduloNumero: number) => { + const modulo = get().getProgresoModulo(moduloNumero); + if (!modulo) return false; + const ejercicio = modulo.ejercicios.find(e => e.ejercicioId === ejercicioId); + return ejercicio?.completado || false; + }, + + getPuntuacionEjercicio: (ejercicioId: string, moduloNumero: number) => { + const modulo = get().getProgresoModulo(moduloNumero); + if (!modulo) return 0; + const ejercicio = modulo.ejercicios.find(e => e.ejercicioId === ejercicioId); + return ejercicio?.puntuacion || 0; + }, + + getPorcentajeModulo: (moduloNumero: number, totalEjercicios: number) => { + const modulo = get().getProgresoModulo(moduloNumero); + if (!modulo || totalEjercicios === 0) return 0; + const completados = modulo.ejercicios.filter(e => e.completado).length; + return Math.round((completados / totalEjercicios) * 100); + }, + + clearError: () => set({ error: null }), + }), + { + name: 'progreso-storage', + partialize: (state) => ({ + progresoModulos: state.progresoModulos, + totalPuntuacion: state.totalPuntuacion, + nivel: state.nivel, + }), + } + ) +); diff --git a/frontend/src/stores/progressStore.ts b/frontend/src/stores/progressStore.ts new file mode 100644 index 0000000..17df0c1 --- /dev/null +++ b/frontend/src/stores/progressStore.ts @@ -0,0 +1,266 @@ +import { create } from 'zustand'; +import { progresoService } from '../services/api'; +import type { Progreso, Badge, NivelUsuario } from '../types'; + +export interface EjercicioProgreso { + ejercicioId: string; + completado: boolean; + puntuacion: number; + fechaCompletado?: string; + intentos: number; +} + +export interface ModuloProgreso { + moduloId: string; + ejercicios: Record; + completado: boolean; +} + +interface ProgressState { + modulos: Record; + puntuacionTotal: number; + badges: Badge[]; + nivel: NivelUsuario; + isLoading: boolean; + error: string | null; + + // Acciones + loadProgreso: () => Promise; + saveProgreso: (moduloId: string, ejercicioId: string, puntuacion: number) => Promise; + getProgresoEjercicio: (moduloId: string, ejercicioId: string) => EjercicioProgreso | undefined; + getProgresoModulo: (moduloId: string) => ModuloProgreso | undefined; + resetProgreso: (moduloId?: string) => void; + calcularPorcentajeModulo: (moduloId: string, totalEjercicios: number) => number; + getBadgesDesbloqueados: () => Badge[]; + getBadgesBloqueados: () => Badge[]; + getProgreso: (moduloId: string, ejercicioId: string) => EjercicioProgreso | undefined; +} + +function calcularNivel(puntuacion: number): NivelUsuario { + if (puntuacion >= 2000) return 'Maestro'; + if (puntuacion >= 1000) return 'Experto'; + if (puntuacion >= 300) return 'Aprendiz'; + return 'Novato'; +} + +function transformarProgresoAPI(progresoAPI: Progreso[]): Record { + const modulos: Record = {}; + + progresoAPI.forEach((p) => { + const moduloId = `modulo${p.modulo_numero}`; + + if (!modulos[moduloId]) { + modulos[moduloId] = { + moduloId, + ejercicios: {}, + completado: false, + }; + } + + modulos[moduloId].ejercicios[p.ejercicio_id] = { + ejercicioId: p.ejercicio_id, + completado: p.completado, + puntuacion: p.puntuacion, + fechaCompletado: new Date().toISOString(), + intentos: 1, + }; + }); + + return modulos; +} + +export const useProgressStore = create()( + (set, get) => ({ + modulos: {}, + puntuacionTotal: 0, + badges: [], + nivel: 'Novato', + isLoading: false, + error: null, + + loadProgreso: async () => { + set({ isLoading: true, error: null }); + + try { + const [progresoData, resumenData] = await Promise.all([ + progresoService.getProgreso(), + progresoService.getResumenProgreso(), + ]); + + const modulos = transformarProgresoAPI(progresoData); + + set({ + modulos, + puntuacionTotal: resumenData?.totalPuntuacion ?? 0, + badges: resumenData?.badges ?? [], + nivel: resumenData?.nivel ?? 'Novato', + isLoading: false, + }); + } catch (error) { + set({ + error: error instanceof Error ? error.message : 'Error al cargar el progreso', + isLoading: false, + }); + console.error('Error loading progreso:', error); + } + }, + + saveProgreso: async (moduloId: string, ejercicioId: string, puntuacion: number) => { + const moduloNumero = parseInt(moduloId.replace('modulo', ''), 10); + + set({ isLoading: true, error: null }); + + try { + // Guardar en la API + await progresoService.saveProgreso({ + modulo_numero: moduloNumero, + ejercicio_id: ejercicioId, + puntuacion, + completado: true, + }); + + // Actualizar estado local + set((state) => { + const modulo = state.modulos[moduloId] || { + moduloId, + ejercicios: {}, + completado: false, + }; + + const ejercicioExistente = modulo.ejercicios[ejercicioId]; + const esMejorPuntuacion = !ejercicioExistente || puntuacion > ejercicioExistente.puntuacion; + + const nuevoEjercicio: EjercicioProgreso = { + ejercicioId, + completado: true, + puntuacion: esMejorPuntuacion ? puntuacion : ejercicioExistente.puntuacion, + fechaCompletado: new Date().toISOString(), + intentos: (ejercicioExistente?.intentos || 0) + 1, + }; + + const nuevosEjercicios = { + ...modulo.ejercicios, + [ejercicioId]: nuevoEjercicio, + }; + + const moduloActualizado: ModuloProgreso = { + ...modulo, + ejercicios: nuevosEjercicios, + }; + + // Recalcular puntuación total + let nuevaPuntuacionTotal = 0; + Object.values(state.modulos).forEach((mod) => { + if (mod.moduloId !== moduloId) { + Object.values(mod.ejercicios).forEach((ej) => { + if (ej.completado) { + nuevaPuntuacionTotal += ej.puntuacion; + } + }); + } + }); + + // Agregar ejercicios del módulo actual + Object.values(nuevosEjercicios).forEach((ej) => { + if (ej.completado) { + nuevaPuntuacionTotal += ej.puntuacion; + } + }); + + const nuevoNivel = calcularNivel(nuevaPuntuacionTotal); + + return { + modulos: { + ...state.modulos, + [moduloId]: moduloActualizado, + }, + puntuacionTotal: nuevaPuntuacionTotal, + nivel: nuevoNivel, + isLoading: false, + }; + }); + + // Recargar resumen para obtener badges actualizados + const resumen = await progresoService.getResumenProgreso(); + set({ + badges: resumen?.badges ?? [], + nivel: resumen?.nivel ?? 'Novato', + }); + } catch (error) { + set({ + error: error instanceof Error ? error.message : 'Error al guardar el progreso', + isLoading: false, + }); + console.error('Error saving progreso:', error); + throw error; + } + }, + + getProgresoEjercicio: (moduloId: string, ejercicioId: string) => { + const state = get(); + return state.modulos[moduloId]?.ejercicios[ejercicioId]; + }, + + getProgresoModulo: (moduloId: string) => { + const state = get(); + return state.modulos[moduloId]; + }, + + resetProgreso: (moduloId?: string) => { + if (moduloId) { + set((state) => { + const { [moduloId]: _, ...restoModulos } = state.modulos; + + // Recalcular puntuación + let nuevaPuntuacionTotal = 0; + Object.values(restoModulos).forEach((mod) => { + Object.values(mod.ejercicios).forEach((ej) => { + if (ej.completado) { + nuevaPuntuacionTotal += ej.puntuacion; + } + }); + }); + + return { + modulos: restoModulos, + puntuacionTotal: nuevaPuntuacionTotal, + nivel: calcularNivel(nuevaPuntuacionTotal), + }; + }); + } else { + set({ + modulos: {}, + puntuacionTotal: 0, + nivel: 'Novato', + }); + } + }, + + calcularPorcentajeModulo: (moduloId: string, totalEjercicios: number): number => { + const state = get(); + const modulo = state.modulos[moduloId]; + if (!modulo || totalEjercicios === 0) return 0; + + const ejerciciosCompletados = Object.values(modulo.ejercicios).filter( + (ej) => ej.completado + ).length; + + return Math.round((ejerciciosCompletados / totalEjercicios) * 100); + }, + + getBadgesDesbloqueados: () => { + return get().badges.filter(b => b.desbloqueado); + }, + + getBadgesBloqueados: () => { + return get().badges.filter(b => !b.desbloqueado); + }, + + getProgreso: (moduloId: string, ejercicioId: string) => { + const state = get(); + return state.modulos[moduloId]?.ejercicios[ejercicioId]; + }, + }) +); + +export default useProgressStore; diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 4d92cb0..2ca8603 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -14,11 +14,52 @@ export interface Progreso { puntuacion: number; } +export interface ProgresoEjercicio { + ejercicioId: string; + completado: boolean; + puntuacion: number; + fecha: string; +} + +export interface ProgresoModulo { + moduloNumero: number; + ejercicios: ProgresoEjercicio[]; + porcentaje: number; +} + +export interface BadgeCondicion { + tipo: 'ejercicios_completados' | 'puntuacion_total' | 'modulo_completado'; + valor: number; + moduloId?: string; +} + +export interface Badge { + id: string; + titulo: string; + descripcion: string; + icono: string; + desbloqueado: boolean; + requisito: string; + condicion?: BadgeCondicion; + fechaDesbloqueo?: string; +} + +export type NivelUsuario = 'Novato' | 'Aprendiz' | 'Experto' | 'Maestro'; + +export interface ConfiguracionEjercicio { + dificultad: 'facil' | 'medio' | 'dificil'; + tiempoMaximo?: number; + intentosMaximos?: number; + mostrarPistas: boolean; +} + export interface Ejercicio { id: string; titulo: string; descripcion: string; tipo: 'quiz' | 'simulador' | 'ejercicio'; + configuracion?: ConfiguracionEjercicio; + moduloNumero: number; } export interface Modulo { @@ -36,6 +77,16 @@ export interface ModuloProgreso { totalEjercicios: number; } +export interface ResumenProgreso { + totalPuntuacion: number; + ejerciciosCompletados: number; + totalEjercicios: number; + porcentajeGlobal: number; + badges: Badge[]; + nivel: NivelUsuario; + moduloProgreso: ProgresoModulo[]; +} + export interface LoginRequest { email?: string; username?: string;