Fix login blank screen and progress persistence
- Fix authStore to persist user data, not just isAuthenticated - Fix progressStore handling of undefined API responses - Remove minimax.md documentation file - All progress now properly saves to PostgreSQL - Login flow working correctly
This commit is contained in:
@@ -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},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
49
frontend/package-lock.json
generated
49
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
BIN
frontend/public/pdfs/resumen_clase_1.pdf
Normal file
BIN
frontend/public/pdfs/resumen_clase_1.pdf
Normal file
Binary file not shown.
BIN
frontend/public/pdfs/resumen_clase_2.pdf
Normal file
BIN
frontend/public/pdfs/resumen_clase_2.pdf
Normal file
Binary file not shown.
BIN
frontend/public/pdfs/resumen_clase_3.pdf
Normal file
BIN
frontend/public/pdfs/resumen_clase_3.pdf
Normal file
Binary file not shown.
BIN
frontend/public/pdfs/resumen_clase_4.pdf
Normal file
BIN
frontend/public/pdfs/resumen_clase_4.pdf
Normal file
Binary file not shown.
@@ -1,3 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { useAuthStore } from './stores/authStore';
|
||||
import { Login } from './pages/Login';
|
||||
@@ -5,6 +6,7 @@ import { Dashboard } from './pages/Dashboard';
|
||||
import { Modulos } from './pages/Modulos';
|
||||
import { Modulo } from './pages/Modulo';
|
||||
import { AdminPanel } from './pages/admin/AdminPanel';
|
||||
import { RecursosPage } from './pages/Recursos';
|
||||
|
||||
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||
const { isAuthenticated, isLoading } = useAuthStore();
|
||||
@@ -25,6 +27,12 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
|
||||
function App() {
|
||||
const { checkAuth } = useAuthStore();
|
||||
|
||||
useEffect(() => {
|
||||
checkAuth();
|
||||
}, [checkAuth]);
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
@@ -53,6 +61,38 @@ function App() {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/modulo/1"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Modulo />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/modulo/2"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Modulo />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/modulo/3"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Modulo />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/modulo/4"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Modulo />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/admin"
|
||||
element={
|
||||
@@ -61,6 +101,14 @@ function App() {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/recursos"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<RecursosPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
||||
184
frontend/src/components/exercises/EjercicioWrapper.tsx
Normal file
184
frontend/src/components/exercises/EjercicioWrapper.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import React, { useState, isValidElement } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Card } from '../ui/Card';
|
||||
import { Button } from '../ui/Button';
|
||||
import { Trophy, Star, RotateCcw, Home, ArrowRight, CheckCircle } from 'lucide-react';
|
||||
import { useEjercicioProgreso } from '../../hooks/useEjercicioProgreso';
|
||||
|
||||
interface EjercicioWrapperProps {
|
||||
moduloId: string;
|
||||
ejercicioId: string;
|
||||
titulo: string;
|
||||
descripcion: string;
|
||||
puntosMaximos: number;
|
||||
onComplete?: (puntuacion?: number) => void;
|
||||
onRetry?: () => void;
|
||||
onExit?: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function EjercicioWrapper({
|
||||
moduloId,
|
||||
ejercicioId,
|
||||
titulo,
|
||||
descripcion,
|
||||
puntosMaximos,
|
||||
onComplete,
|
||||
onRetry,
|
||||
onExit,
|
||||
children,
|
||||
}: EjercicioWrapperProps) {
|
||||
const { puntuacionAnterior, intentos, guardarProgreso } = useEjercicioProgreso({
|
||||
moduloId,
|
||||
ejercicioId,
|
||||
onComplete,
|
||||
});
|
||||
|
||||
const [mostrarCompletado, setMostrarCompletado] = useState(false);
|
||||
const [puntuacionActual, setPuntuacionActual] = useState(0);
|
||||
|
||||
const handleCompletar = (puntuacion: number) => {
|
||||
guardarProgreso(puntuacion);
|
||||
setPuntuacionActual(puntuacion);
|
||||
setMostrarCompletado(true);
|
||||
};
|
||||
|
||||
const esMejorPuntuacion = puntuacionAnterior !== undefined && puntuacionActual > puntuacionAnterior;
|
||||
|
||||
// Pasar handleCompletar a los hijos
|
||||
const childrenWithProps = isValidElement(children)
|
||||
? React.cloneElement(children as React.ReactElement<any>, { onCompletar: handleCompletar })
|
||||
: children;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<AnimatePresence mode="wait">
|
||||
{!mostrarCompletado ? (
|
||||
<motion.div
|
||||
key="ejercicio"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
<div className="mb-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">{titulo}</h2>
|
||||
<p className="text-gray-600 mt-1">{descripcion}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="flex items-center gap-2 text-amber-600 bg-amber-50 px-3 py-1 rounded-full">
|
||||
<Trophy size={16} />
|
||||
<span className="font-semibold">{puntosMaximos} pts máx.</span>
|
||||
</div>
|
||||
{puntuacionAnterior !== undefined && (
|
||||
<div className="mt-2 text-sm text-gray-500">
|
||||
Mejor puntuación: {puntuacionAnterior} pts
|
||||
<span className="text-gray-400"> ({intentos} {intentos === 1 ? 'intento' : 'intentos'})</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{childrenWithProps}
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="completado"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ type: "spring", stiffness: 200 }}
|
||||
>
|
||||
<Card className="text-center py-12">
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 200,
|
||||
delay: 0.2
|
||||
}}
|
||||
className="inline-flex items-center justify-center w-24 h-24 bg-gradient-to-br from-yellow-400 to-orange-500 rounded-full mb-6 shadow-lg"
|
||||
>
|
||||
<Trophy size={48} className="text-white" />
|
||||
</motion.div>
|
||||
|
||||
<h3 className="text-3xl font-bold text-gray-900 mb-2">
|
||||
¡Ejercicio Completado!
|
||||
</h3>
|
||||
|
||||
<p className="text-gray-600 mb-8 max-w-md mx-auto">
|
||||
Has completado el ejercicio. Revisa tu puntuación y decide si quieres intentarlo de nuevo para mejorar tu marca.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4 max-w-lg mx-auto mb-8">
|
||||
<div className="bg-blue-50 rounded-xl p-4">
|
||||
<Star className="w-6 h-6 text-blue-500 mx-auto mb-2" />
|
||||
<p className="text-2xl font-bold text-blue-700">{puntuacionActual}</p>
|
||||
<p className="text-sm text-blue-600">Puntuación</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-purple-50 rounded-xl p-4">
|
||||
<Trophy className="w-6 h-6 text-purple-500 mx-auto mb-2" />
|
||||
<p className="text-2xl font-bold text-purple-700">{puntosMaximos}</p>
|
||||
<p className="text-sm text-purple-600">Máximo</p>
|
||||
</div>
|
||||
|
||||
<div className={`rounded-xl p-4 ${esMejorPuntuacion ? 'bg-green-50' : 'bg-gray-50'}`}>
|
||||
<CheckCircle className={`w-6 h-6 mx-auto mb-2 ${esMejorPuntuacion ? 'text-green-500' : 'text-gray-400'}`} />
|
||||
<p className={`text-2xl font-bold ${esMejorPuntuacion ? 'text-green-700' : 'text-gray-700'}`}>
|
||||
{Math.round((puntuacionActual / puntosMaximos) * 100)}%
|
||||
</p>
|
||||
<p className={`text-sm ${esMejorPuntuacion ? 'text-green-600' : 'text-gray-500'}`}>
|
||||
{esMejorPuntuacion ? '¡Récord!' : 'Precisión'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{esMejorPuntuacion && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-6"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 bg-green-100 text-green-700 rounded-full font-medium">
|
||||
<Star size={18} />
|
||||
¡Nueva mejor puntuación! +{puntuacionActual - (puntuacionAnterior || 0)} pts
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-4">
|
||||
<Button variant="outline" onClick={onExit}>
|
||||
<Home size={18} className="mr-2" />
|
||||
Volver al módulo
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setMostrarCompletado(false);
|
||||
if (onRetry) onRetry();
|
||||
}}
|
||||
>
|
||||
<RotateCcw size={18} className="mr-2" />
|
||||
Intentar de nuevo
|
||||
</Button>
|
||||
|
||||
{!esMejorPuntuacion && puntuacionActual < puntosMaximos && (
|
||||
<Button onClick={onExit}>
|
||||
Siguiente ejercicio
|
||||
<ArrowRight size={18} className="ml-2" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EjercicioWrapper;
|
||||
1
frontend/src/components/exercises/index.ts
Normal file
1
frontend/src/components/exercises/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { EjercicioWrapper } from './EjercicioWrapper';
|
||||
431
frontend/src/components/exercises/modulo1/FlujoCircular.tsx
Normal file
431
frontend/src/components/exercises/modulo1/FlujoCircular.tsx
Normal file
@@ -0,0 +1,431 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Card, CardHeader } from '../../ui/Card';
|
||||
import { Button } from '../../ui/Button';
|
||||
import { CheckCircle, XCircle, Trophy, Users, Building2, Landmark, Globe, RefreshCw } from 'lucide-react';
|
||||
|
||||
interface FlujoCircularProps {
|
||||
ejercicioId: string;
|
||||
onComplete?: (puntuacion: number) => void;
|
||||
}
|
||||
|
||||
type Agente = 'familias' | 'empresas' | 'estado' | 'exterior';
|
||||
type TipoFlujo = 'real' | 'monetario';
|
||||
|
||||
interface Elemento {
|
||||
id: string;
|
||||
texto: string;
|
||||
tipo: TipoFlujo;
|
||||
origen: Agente;
|
||||
destino: Agente;
|
||||
}
|
||||
|
||||
interface Nivel {
|
||||
nombre: string;
|
||||
descripcion: string;
|
||||
agentes: Agente[];
|
||||
elementos: Elemento[];
|
||||
}
|
||||
|
||||
const NIVELES: Nivel[] = [
|
||||
{
|
||||
nombre: 'Básico',
|
||||
descripcion: 'Solo Familias y Empresas',
|
||||
agentes: ['familias', 'empresas'],
|
||||
elementos: [
|
||||
{ id: 'trabajo', texto: '💪 Trabajo', tipo: 'real', origen: 'familias', destino: 'empresas' },
|
||||
{ id: 'salarios', texto: '💵 Salarios', tipo: 'monetario', origen: 'empresas', destino: 'familias' },
|
||||
{ id: 'bienes', texto: '📦 Bienes', tipo: 'real', origen: 'empresas', destino: 'familias' },
|
||||
{ id: 'gasto', texto: '💳 Gasto', tipo: 'monetario', origen: 'familias', destino: 'empresas' },
|
||||
]
|
||||
},
|
||||
{
|
||||
nombre: 'Intermedio',
|
||||
descripcion: 'Incluye al Estado',
|
||||
agentes: ['familias', 'empresas', 'estado'],
|
||||
elementos: [
|
||||
{ id: 'trabajo', texto: '💪 Trabajo', tipo: 'real', origen: 'familias', destino: 'empresas' },
|
||||
{ id: 'tierra', texto: '🌾 Tierra', tipo: 'real', origen: 'familias', destino: 'empresas' },
|
||||
{ id: 'capital', texto: '💰 Capital', tipo: 'real', origen: 'familias', destino: 'empresas' },
|
||||
{ id: 'salarios', texto: '💵 Salarios', tipo: 'monetario', origen: 'empresas', destino: 'familias' },
|
||||
{ id: 'renta', texto: '🏠 Renta', tipo: 'monetario', origen: 'empresas', destino: 'familias' },
|
||||
{ id: 'intereses', texto: '📈 Intereses', tipo: 'monetario', origen: 'empresas', destino: 'familias' },
|
||||
{ id: 'bienes', texto: '📦 Bienes', tipo: 'real', origen: 'empresas', destino: 'familias' },
|
||||
{ id: 'servicios', texto: '🔧 Servicios', tipo: 'real', origen: 'empresas', destino: 'familias' },
|
||||
{ id: 'gasto', texto: '💳 Gasto', tipo: 'monetario', origen: 'familias', destino: 'empresas' },
|
||||
{ id: 'impuestos', texto: '📝 Impuestos', tipo: 'monetario', origen: 'familias', destino: 'estado' },
|
||||
{ id: 'transferencias', texto: '🎁 Transferencias', tipo: 'monetario', origen: 'estado', destino: 'familias' },
|
||||
{ id: 'gasto-publico', texto: '🏗️ Gasto Público', tipo: 'monetario', origen: 'estado', destino: 'empresas' },
|
||||
]
|
||||
},
|
||||
{
|
||||
nombre: 'Avanzado',
|
||||
descripcion: 'Todos los agentes incluyendo Sector Externo',
|
||||
agentes: ['familias', 'empresas', 'estado', 'exterior'],
|
||||
elementos: [
|
||||
{ id: 'trabajo', texto: '💪 Trabajo', tipo: 'real', origen: 'familias', destino: 'empresas' },
|
||||
{ id: 'tierra', texto: '🌾 Tierra', tipo: 'real', origen: 'familias', destino: 'empresas' },
|
||||
{ id: 'capital', texto: '💰 Capital', tipo: 'real', origen: 'familias', destino: 'empresas' },
|
||||
{ id: 'salarios', texto: '💵 Salarios', tipo: 'monetario', origen: 'empresas', destino: 'familias' },
|
||||
{ id: 'renta', texto: '🏠 Renta', tipo: 'monetario', origen: 'empresas', destino: 'familias' },
|
||||
{ id: 'intereses', texto: '📈 Intereses', tipo: 'monetario', origen: 'empresas', destino: 'familias' },
|
||||
{ id: 'bienes', texto: '📦 Bienes', tipo: 'real', origen: 'empresas', destino: 'familias' },
|
||||
{ id: 'servicios', texto: '🔧 Servicios', tipo: 'real', origen: 'empresas', destino: 'familias' },
|
||||
{ id: 'gasto', texto: '💳 Gasto', tipo: 'monetario', origen: 'familias', destino: 'empresas' },
|
||||
{ id: 'impuestos', texto: '📝 Impuestos', tipo: 'monetario', origen: 'familias', destino: 'estado' },
|
||||
{ id: 'transferencias', texto: '🎁 Transferencias', tipo: 'monetario', origen: 'estado', destino: 'familias' },
|
||||
{ id: 'gasto-publico', texto: '🏗️ Gasto Público', tipo: 'monetario', origen: 'estado', destino: 'empresas' },
|
||||
{ id: 'exportaciones', texto: '📤 Exportaciones', tipo: 'real', origen: 'empresas', destino: 'exterior' },
|
||||
{ id: 'importaciones', texto: '📥 Importaciones', tipo: 'real', origen: 'exterior', destino: 'empresas' },
|
||||
{ id: 'divisas-ent', texto: '💱 Divisas (Ent.)', tipo: 'monetario', origen: 'exterior', destino: 'empresas' },
|
||||
{ id: 'divisas-sal', texto: '💱 Divisas (Sal.)', tipo: 'monetario', origen: 'empresas', destino: 'exterior' },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const AGENTE_CONFIG: Record<Agente, { icon: React.ReactNode; label: string; color: string; position: string }> = {
|
||||
familias: { icon: <Users size={24} />, label: 'Familias', color: 'bg-green-100 text-green-700 border-green-300', position: 'left-4 top-1/2 -translate-y-1/2' },
|
||||
empresas: { icon: <Building2 size={24} />, label: 'Empresas', color: 'bg-blue-100 text-blue-700 border-blue-300', position: 'right-4 top-1/2 -translate-y-1/2' },
|
||||
estado: { icon: <Landmark size={24} />, label: 'Estado', color: 'bg-orange-100 text-orange-700 border-orange-300', position: 'left-1/2 -translate-x-1/2 top-4' },
|
||||
exterior: { icon: <Globe size={24} />, label: 'Sector Externo', color: 'bg-purple-100 text-purple-700 border-purple-300', position: 'left-1/2 -translate-x-1/2 bottom-4' },
|
||||
};
|
||||
|
||||
export function FlujoCircular({ ejercicioId: _ejercicioId, onComplete }: FlujoCircularProps) {
|
||||
const [nivelActual, setNivelActual] = useState(0);
|
||||
const [elementosColocados, setElementosColocados] = useState<Record<string, { origen: Agente; destino: Agente } | null>>({});
|
||||
const [elementoSeleccionado, setElementoSeleccionado] = useState<string | null>(null);
|
||||
const [puntuacion, setPuntuacion] = useState(0);
|
||||
const [completado, setCompletado] = useState(false);
|
||||
const [aciertos, setAciertos] = useState(0);
|
||||
const [errores, setErrores] = useState(0);
|
||||
|
||||
const nivel = NIVELES[nivelActual];
|
||||
|
||||
useEffect(() => {
|
||||
const inicial: Record<string, { origen: Agente; destino: Agente } | null> = {};
|
||||
nivel.elementos.forEach(el => {
|
||||
inicial[el.id] = null;
|
||||
});
|
||||
setElementosColocados(inicial);
|
||||
}, [nivel]);
|
||||
|
||||
const handleElementoClick = (elementoId: string) => {
|
||||
if (elementosColocados[elementoId]) return;
|
||||
setElementoSeleccionado(elementoId === elementoSeleccionado ? null : elementoId);
|
||||
};
|
||||
|
||||
const handleConexionClick = (origen: Agente, destino: Agente) => {
|
||||
if (!elementoSeleccionado) return;
|
||||
|
||||
const elemento = nivel.elementos.find(el => el.id === elementoSeleccionado);
|
||||
if (!elemento) return;
|
||||
|
||||
const esCorrecto = elemento.origen === origen && elemento.destino === destino;
|
||||
|
||||
setElementosColocados(prev => ({
|
||||
...prev,
|
||||
[elementoSeleccionado]: { origen, destino }
|
||||
}));
|
||||
|
||||
if (esCorrecto) {
|
||||
setAciertos(prev => prev + 1);
|
||||
setPuntuacion(prev => prev + 10);
|
||||
} else {
|
||||
setErrores(prev => prev + 1);
|
||||
setPuntuacion(prev => Math.max(0, prev - 2));
|
||||
}
|
||||
|
||||
setElementoSeleccionado(null);
|
||||
};
|
||||
|
||||
const verificarCompletitud = () => {
|
||||
const todosColocados = nivel.elementos.every(el => elementosColocados[el.id] !== null);
|
||||
if (todosColocados) {
|
||||
const bonus = 50;
|
||||
setPuntuacion(prev => prev + bonus);
|
||||
|
||||
if (nivelActual < NIVELES.length - 1) {
|
||||
setNivelActual(prev => prev + 1);
|
||||
} else {
|
||||
setCompletado(true);
|
||||
if (onComplete) {
|
||||
onComplete(puntuacion + bonus);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const todosColocados = nivel.elementos.every(el => elementosColocados[el.id] !== null);
|
||||
if (todosColocados && !completado) {
|
||||
setTimeout(verificarCompletitud, 500);
|
||||
}
|
||||
}, [elementosColocados]);
|
||||
|
||||
const handleReiniciarNivel = () => {
|
||||
const inicial: Record<string, { origen: Agente; destino: Agente } | null> = {};
|
||||
nivel.elementos.forEach(el => {
|
||||
inicial[el.id] = null;
|
||||
});
|
||||
setElementosColocados(inicial);
|
||||
setElementoSeleccionado(null);
|
||||
setAciertos(0);
|
||||
setErrores(0);
|
||||
};
|
||||
|
||||
const getConexionesPosibles = (): { origen: Agente; destino: Agente; label: string }[] => {
|
||||
const conexiones: { origen: Agente; destino: Agente; label: string }[] = [];
|
||||
|
||||
if (nivel.agentes.includes('familias') && nivel.agentes.includes('empresas')) {
|
||||
conexiones.push({ origen: 'familias', destino: 'empresas', label: 'Familias → Empresas' });
|
||||
conexiones.push({ origen: 'empresas', destino: 'familias', label: 'Empresas → Familias' });
|
||||
}
|
||||
|
||||
if (nivel.agentes.includes('estado')) {
|
||||
if (nivel.agentes.includes('familias')) {
|
||||
conexiones.push({ origen: 'familias', destino: 'estado', label: 'Familias → Estado' });
|
||||
conexiones.push({ origen: 'estado', destino: 'familias', label: 'Estado → Familias' });
|
||||
}
|
||||
if (nivel.agentes.includes('empresas')) {
|
||||
conexiones.push({ origen: 'estado', destino: 'empresas', label: 'Estado → Empresas' });
|
||||
}
|
||||
}
|
||||
|
||||
if (nivel.agentes.includes('exterior') && nivel.agentes.includes('empresas')) {
|
||||
conexiones.push({ origen: 'empresas', destino: 'exterior', label: 'Empresas → Exterior' });
|
||||
conexiones.push({ origen: 'exterior', destino: 'empresas', label: 'Exterior → Empresas' });
|
||||
}
|
||||
|
||||
return conexiones;
|
||||
};
|
||||
|
||||
if (completado) {
|
||||
return (
|
||||
<Card className="w-full max-w-3xl mx-auto">
|
||||
<div className="text-center py-8">
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ type: "spring", stiffness: 200 }}
|
||||
className="inline-flex items-center justify-center w-20 h-20 bg-yellow-100 rounded-full mb-4"
|
||||
>
|
||||
<Trophy size={40} className="text-yellow-600" />
|
||||
</motion.div>
|
||||
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-2">¡Juego Completado!</h3>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Has completado todos los niveles del Flujo Circular
|
||||
</p>
|
||||
|
||||
<div className="bg-blue-50 rounded-lg p-6 mb-6">
|
||||
<p className="text-sm text-blue-600 mb-1">Puntuación Final</p>
|
||||
<p className="text-4xl font-bold text-blue-700">{puntuacion} puntos</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 max-w-xs mx-auto mb-6">
|
||||
<div className="bg-green-50 rounded-lg p-3">
|
||||
<p className="text-2xl font-bold text-green-600">{aciertos}</p>
|
||||
<p className="text-sm text-green-700">Aciertos</p>
|
||||
</div>
|
||||
<div className="bg-red-50 rounded-lg p-3">
|
||||
<p className="text-2xl font-bold text-red-600">{errores}</p>
|
||||
<p className="text-sm text-red-700">Errores</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button onClick={() => {
|
||||
setNivelActual(0);
|
||||
setPuntuacion(0);
|
||||
setCompletado(false);
|
||||
setAciertos(0);
|
||||
setErrores(0);
|
||||
handleReiniciarNivel();
|
||||
}} variant="outline">
|
||||
<RefreshCw size={16} className="mr-2" />
|
||||
Jugar de Nuevo
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-4xl mx-auto">
|
||||
<CardHeader
|
||||
title={`Nivel ${nivelActual + 1}: ${nivel.nombre}`}
|
||||
subtitle={nivel.descripcion}
|
||||
action={
|
||||
<Button variant="ghost" size="sm" onClick={handleReiniciarNivel}>
|
||||
<RefreshCw size={16} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
{NIVELES.map((_, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold ${
|
||||
idx < nivelActual
|
||||
? 'bg-green-500 text-white'
|
||||
: idx === nivelActual
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-200 text-gray-500'
|
||||
}`}
|
||||
>
|
||||
{idx < nivelActual ? <CheckCircle size={16} /> : idx + 1}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-gray-600">Puntuación</p>
|
||||
<p className="text-xl font-bold text-blue-600">{puntuacion} pts</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2">
|
||||
<div className="relative bg-gray-50 rounded-xl p-8 min-h-[400px]">
|
||||
{nivel.agentes.map((agente) => (
|
||||
<motion.div
|
||||
key={agente}
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
className={`absolute ${AGENTE_CONFIG[agente].position} w-24 h-24 rounded-xl border-2 ${AGENTE_CONFIG[agente].color} flex flex-col items-center justify-center gap-1 cursor-pointer hover:shadow-lg transition-shadow`}
|
||||
>
|
||||
{AGENTE_CONFIG[agente].icon}
|
||||
<span className="text-xs font-bold text-center">{AGENTE_CONFIG[agente].label}</span>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
<svg className="absolute inset-0 w-full h-full pointer-events-none" style={{ zIndex: 0 }}>
|
||||
{getConexionesPosibles().map((conexion, idx) => (
|
||||
<g key={idx}>
|
||||
<line
|
||||
x1={conexion.origen === 'familias' ? '15%' : conexion.origen === 'empresas' ? '85%' : conexion.origen === 'estado' ? '50%' : '50%'}
|
||||
y1={conexion.origen === 'familias' ? '50%' : conexion.origen === 'empresas' ? '50%' : conexion.origen === 'estado' ? '15%' : '85%'}
|
||||
x2={conexion.destino === 'familias' ? '15%' : conexion.destino === 'empresas' ? '85%' : conexion.destino === 'estado' ? '50%' : '50%'}
|
||||
y2={conexion.destino === 'familias' ? '50%' : conexion.destino === 'empresas' ? '50%' : conexion.destino === 'estado' ? '15%' : '85%'}
|
||||
stroke="#e5e7eb"
|
||||
strokeWidth="2"
|
||||
strokeDasharray="5,5"
|
||||
/>
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{getConexionesPosibles().map((conexion, idx) => {
|
||||
const elementosEnConexion = nivel.elementos.filter(el =>
|
||||
elementosColocados[el.id]?.origen === conexion.origen &&
|
||||
elementosColocados[el.id]?.destino === conexion.destino
|
||||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => handleConexionClick(conexion.origen, conexion.destino)}
|
||||
disabled={!elementoSeleccionado}
|
||||
className={`p-2 rounded-lg border-2 text-xs font-medium transition-all min-w-[120px] ${
|
||||
elementoSeleccionado
|
||||
? 'border-blue-300 bg-blue-50 hover:bg-blue-100 cursor-pointer'
|
||||
: 'border-gray-200 bg-white'
|
||||
}`}
|
||||
>
|
||||
<div className="text-gray-500 mb-1">{conexion.label}</div>
|
||||
<div className="flex flex-wrap gap-1 justify-center">
|
||||
{elementosEnConexion.map((el, i) => (
|
||||
<span key={i} className="text-lg" title={el.texto}>
|
||||
{el.texto.split(' ')[0]}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-700 mb-3">Elementos ({nivel.elementos.length})</h4>
|
||||
<p className="text-sm text-gray-500 mb-3">
|
||||
{elementoSeleccionado
|
||||
? 'Selecciona una conexión en el diagrama'
|
||||
: 'Haz clic en un elemento para colocarlo'}
|
||||
</p>
|
||||
|
||||
<div className="space-y-2 max-h-[300px] overflow-y-auto">
|
||||
{nivel.elementos.map((elemento) => {
|
||||
const colocado = elementosColocados[elemento.id];
|
||||
const seleccionado = elementoSeleccionado === elemento.id;
|
||||
const esCorrecto = colocado && colocado.origen === elemento.origen && colocado.destino === elemento.destino;
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
key={elemento.id}
|
||||
onClick={() => handleElementoClick(elemento.id)}
|
||||
disabled={!!colocado}
|
||||
whileHover={!colocado ? { scale: 1.02 } : {}}
|
||||
whileTap={!colocado ? { scale: 0.98 } : {}}
|
||||
className={`w-full p-3 rounded-lg border-2 text-left transition-all ${
|
||||
colocado
|
||||
? esCorrecto
|
||||
? 'border-green-300 bg-green-50'
|
||||
: 'border-red-300 bg-red-50'
|
||||
: seleccionado
|
||||
? 'border-blue-500 bg-blue-50 shadow-md'
|
||||
: 'border-gray-200 bg-white hover:border-blue-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl">{elemento.texto.split(' ')[0]}</span>
|
||||
<span className="text-sm font-medium">{elemento.texto.split(' ').slice(1).join(' ')}</span>
|
||||
</div>
|
||||
{colocado && (
|
||||
esCorrecto
|
||||
? <CheckCircle size={16} className="text-green-600" />
|
||||
: <XCircle size={16} className="text-red-600" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${
|
||||
elemento.tipo === 'real' ? 'bg-blue-100 text-blue-700' : 'bg-green-100 text-green-700'
|
||||
}`}>
|
||||
{elemento.tipo === 'real' ? 'Real' : 'Monetario'}
|
||||
</span>
|
||||
{colocado && !esCorrecto && (
|
||||
<span className="text-xs text-red-600">
|
||||
{AGENTE_CONFIG[elemento.origen].label} → {AGENTE_CONFIG[elemento.destino].label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</motion.button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-3 bg-gray-50 rounded-lg">
|
||||
<p className="text-xs text-gray-600 mb-2">Leyenda:</p>
|
||||
<div className="flex gap-4 text-xs">
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="w-3 h-3 bg-blue-100 border border-blue-300 rounded"></span>
|
||||
Flujo Real
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="w-3 h-3 bg-green-100 border border-green-300 rounded"></span>
|
||||
Flujo Monetario
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default FlujoCircular;
|
||||
310
frontend/src/components/exercises/modulo1/QuizBienes.tsx
Normal file
310
frontend/src/components/exercises/modulo1/QuizBienes.tsx
Normal file
@@ -0,0 +1,310 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Card, CardHeader } from '../../ui/Card';
|
||||
import { Button } from '../../ui/Button';
|
||||
import { CheckCircle, XCircle, ArrowRight, Trophy, BookOpen } from 'lucide-react';
|
||||
|
||||
interface QuizBienesProps {
|
||||
ejercicioId: string;
|
||||
onComplete?: (puntuacion: number) => void;
|
||||
}
|
||||
|
||||
interface Pregunta {
|
||||
id: string;
|
||||
bien: string;
|
||||
descripcion: string;
|
||||
opciones: string[];
|
||||
respuestaCorrecta: string;
|
||||
explicacionDetallada: string;
|
||||
}
|
||||
|
||||
const PREGUNTAS: Pregunta[] = [
|
||||
{
|
||||
id: 'p1',
|
||||
bien: 'Carne de primera calidad',
|
||||
descripcion: 'Carne de res premium vendida en supermercados de alta gama',
|
||||
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
|
||||
respuestaCorrecta: 'Bien de lujo',
|
||||
explicacionDetallada: 'La carne premium es considerada un bien de lujo porque cuando el ingreso aumenta significativamente, las familias aumentan su consumo de este tipo de carne sustituyendo carnes de menor calidad.'
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
bien: 'Pan',
|
||||
descripcion: 'Pan básico de consumo diario',
|
||||
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
|
||||
respuestaCorrecta: 'Bien normal',
|
||||
explicacionDetallada: 'El pan es un bien normal porque su consumo aumenta moderadamente con el ingreso, aunque llega un punto donde se estabiliza (saturación).'
|
||||
},
|
||||
{
|
||||
id: 'p3',
|
||||
bien: 'Transporte público (autobús)',
|
||||
descripcion: 'Servicio de autobuses urbanos',
|
||||
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
|
||||
respuestaCorrecta: 'Bien inferior',
|
||||
explicacionDetallada: 'El transporte público es un bien inferior porque cuando los ingresos aumentan, las personas tienden a comprar automóviles o usar taxis/Uber, reduciendo el uso del autobús.'
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
bien: 'Fideos instantáneos',
|
||||
descripcion: 'Comida rápida económica',
|
||||
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
|
||||
respuestaCorrecta: 'Bien inferior',
|
||||
explicacionDetallada: 'Los fideos instantáneos son claramente un bien inferior. A medida que aumentan los ingresos, las personas prefieren alimentos más nutritivos y de mejor calidad.'
|
||||
},
|
||||
{
|
||||
id: 'p5',
|
||||
bien: 'Vacaciones en el extranjero',
|
||||
descripcion: 'Viajes turísticos internacionales',
|
||||
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
|
||||
respuestaCorrecta: 'Bien de lujo',
|
||||
explicacionDetallada: 'Las vacaciones internacionales son un bien de lujo porque su consumo aumenta significativamente cuando el ingreso crece, incluso más que proporcionalmente.'
|
||||
},
|
||||
{
|
||||
id: 'p6',
|
||||
bien: 'Ropa de marca',
|
||||
descripcion: 'Vestimenta de diseñador',
|
||||
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
|
||||
respuestaCorrecta: 'Bien de lujo',
|
||||
explicacionDetallada: 'La ropa de marca es un bien de lujo porque su demanda crece más rápido que el ingreso, especialmente en rangos de ingreso altos.'
|
||||
},
|
||||
{
|
||||
id: 'p7',
|
||||
bien: 'Cine',
|
||||
descripcion: 'Entradas a salas de cine',
|
||||
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
|
||||
respuestaCorrecta: 'Bien normal',
|
||||
explicacionDetallada: 'El cine es un bien normal. Aunque con el auge del streaming podría debatirse, generalmente el consumo de entretenimiento aumenta con el ingreso de forma moderada.'
|
||||
},
|
||||
{
|
||||
id: 'p8',
|
||||
bien: 'Productos de marca blanca',
|
||||
descripcion: 'Productos genéricos de supermercado',
|
||||
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
|
||||
respuestaCorrecta: 'Bien inferior',
|
||||
explicacionDetallada: 'Los productos de marca blanca son bienes inferiores porque son sustituidos por marcas reconocidas cuando el consumidor tiene mayores ingresos.'
|
||||
}
|
||||
];
|
||||
|
||||
export function QuizBienes({ ejercicioId: _ejercicioId, onComplete }: QuizBienesProps) {
|
||||
const [preguntaActual, setPreguntaActual] = useState(0);
|
||||
const [respuestaSeleccionada, setRespuestaSeleccionada] = useState<string | null>(null);
|
||||
const [mostrarRetroalimentacion, setMostrarRetroalimentacion] = useState(false);
|
||||
const [puntuacion, setPuntuacion] = useState(0);
|
||||
const [respuestasCorrectas, setRespuestasCorrectas] = useState(0);
|
||||
const [completado, setCompletado] = useState(false);
|
||||
const [progreso, setProgreso] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setProgreso(((preguntaActual + (completado ? 1 : 0)) / PREGUNTAS.length) * 100);
|
||||
}, [preguntaActual, completado]);
|
||||
|
||||
const handleSeleccionarRespuesta = (opcion: string) => {
|
||||
if (mostrarRetroalimentacion) return;
|
||||
setRespuestaSeleccionada(opcion);
|
||||
};
|
||||
|
||||
const handleValidar = () => {
|
||||
if (!respuestaSeleccionada) return;
|
||||
|
||||
const esCorrecta = respuestaSeleccionada === PREGUNTAS[preguntaActual].respuestaCorrecta;
|
||||
setMostrarRetroalimentacion(true);
|
||||
|
||||
if (esCorrecta) {
|
||||
setRespuestasCorrectas(prev => prev + 1);
|
||||
setPuntuacion(prev => prev + 100);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSiguiente = () => {
|
||||
if (preguntaActual < PREGUNTAS.length - 1) {
|
||||
setPreguntaActual(prev => prev + 1);
|
||||
setRespuestaSeleccionada(null);
|
||||
setMostrarRetroalimentacion(false);
|
||||
} else {
|
||||
setCompletado(true);
|
||||
const puntuacionFinal = puntuacion + (respuestaSeleccionada === PREGUNTAS[preguntaActual].respuestaCorrecta ? 100 : 0);
|
||||
if (onComplete) {
|
||||
onComplete(puntuacionFinal);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleReiniciar = () => {
|
||||
setPreguntaActual(0);
|
||||
setRespuestaSeleccionada(null);
|
||||
setMostrarRetroalimentacion(false);
|
||||
setPuntuacion(0);
|
||||
setRespuestasCorrectas(0);
|
||||
setCompletado(false);
|
||||
};
|
||||
|
||||
const pregunta = PREGUNTAS[preguntaActual];
|
||||
const esCorrecta = respuestaSeleccionada === pregunta.respuestaCorrecta;
|
||||
|
||||
if (completado) {
|
||||
return (
|
||||
<Card className="w-full max-w-2xl mx-auto">
|
||||
<div className="text-center py-8">
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ type: "spring", stiffness: 200 }}
|
||||
className="inline-flex items-center justify-center w-20 h-20 bg-yellow-100 rounded-full mb-4"
|
||||
>
|
||||
<Trophy size={40} className="text-yellow-600" />
|
||||
</motion.div>
|
||||
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-2">¡Quiz Completado!</h3>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Respondiste correctamente {respuestasCorrectas} de {PREGUNTAS.length} preguntas
|
||||
</p>
|
||||
|
||||
<div className="bg-blue-50 rounded-lg p-6 mb-6">
|
||||
<p className="text-sm text-blue-600 mb-1">Puntuación Total</p>
|
||||
<p className="text-4xl font-bold text-blue-700">{puntuacion} puntos</p>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleReiniciar} variant="outline">
|
||||
Intentar de Nuevo
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-2xl mx-auto">
|
||||
<CardHeader
|
||||
title="Quiz: Clasificación de Bienes"
|
||||
subtitle={`Pregunta ${preguntaActual + 1} de ${PREGUNTAS.length}`}
|
||||
/>
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-between text-sm text-gray-600 mb-2">
|
||||
<span>Progreso</span>
|
||||
<span>{Math.round(progreso)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
||||
<motion.div
|
||||
className="bg-blue-600 h-2.5 rounded-full"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${progreso}%` }}
|
||||
transition={{ duration: 0.3 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-2 text-blue-600 mb-3">
|
||||
<BookOpen size={20} />
|
||||
<span className="font-medium">Clasifica el siguiente bien:</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-2">{pregunta.bien}</h3>
|
||||
<p className="text-gray-600">{pregunta.descripcion}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 mb-6">
|
||||
{pregunta.opciones.map((opcion, index) => {
|
||||
const isSelected = respuestaSeleccionada === opcion;
|
||||
const isCorrect = opcion === pregunta.respuestaCorrecta;
|
||||
const showCorrect = mostrarRetroalimentacion && isCorrect;
|
||||
const showIncorrect = mostrarRetroalimentacion && isSelected && !isCorrect;
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
key={index}
|
||||
onClick={() => handleSeleccionarRespuesta(opcion)}
|
||||
disabled={mostrarRetroalimentacion}
|
||||
whileHover={!mostrarRetroalimentacion ? { scale: 1.02 } : {}}
|
||||
whileTap={!mostrarRetroalimentacion ? { scale: 0.98 } : {}}
|
||||
className={`w-full p-4 rounded-lg border-2 text-left transition-all ${
|
||||
showCorrect
|
||||
? 'border-green-500 bg-green-50'
|
||||
: showIncorrect
|
||||
? 'border-red-500 bg-red-50'
|
||||
: isSelected
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-blue-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex items-center justify-center w-8 h-8 rounded-full bg-gray-100 text-gray-700 font-semibold text-sm">
|
||||
{String.fromCharCode(65 + index)}
|
||||
</span>
|
||||
<span className="font-medium">{opcion}</span>
|
||||
</div>
|
||||
{showCorrect && <CheckCircle size={20} className="text-green-600" />}
|
||||
{showIncorrect && <XCircle size={20} className="text-red-600" />}
|
||||
</div>
|
||||
</motion.button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{mostrarRetroalimentacion && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="overflow-hidden mb-6"
|
||||
>
|
||||
<div className={`p-4 rounded-lg border ${
|
||||
esCorrecta
|
||||
? 'bg-green-50 border-green-200'
|
||||
: 'bg-red-50 border-red-200'
|
||||
}`}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{esCorrecta ? (
|
||||
<CheckCircle size={20} className="text-green-600" />
|
||||
) : (
|
||||
<XCircle size={20} className="text-red-600" />
|
||||
)}
|
||||
<span className={`font-semibold ${
|
||||
esCorrecta ? 'text-green-800' : 'text-red-800'
|
||||
}`}>
|
||||
{esCorrecta ? '¡Correcto!' : 'Incorrecto'}
|
||||
</span>
|
||||
</div>
|
||||
<p className={`text-sm ${
|
||||
esCorrecta ? 'text-green-700' : 'text-red-700'
|
||||
}`}>
|
||||
{pregunta.explicacionDetallada}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-sm text-gray-600">
|
||||
Puntuación: <span className="font-bold text-blue-600">{puntuacion}</span> pts
|
||||
</div>
|
||||
|
||||
{!mostrarRetroalimentacion ? (
|
||||
<Button
|
||||
onClick={handleValidar}
|
||||
disabled={!respuestaSeleccionada}
|
||||
>
|
||||
Validar Respuesta
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={handleSiguiente}>
|
||||
{preguntaActual < PREGUNTAS.length - 1 ? (
|
||||
<>
|
||||
Siguiente
|
||||
<ArrowRight size={16} className="ml-2" />
|
||||
</>
|
||||
) : (
|
||||
'Finalizar'
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default QuizBienes;
|
||||
@@ -0,0 +1,248 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
interface SimuladorDisyuntivasProps {
|
||||
ejercicioId: string;
|
||||
onComplete?: (puntuacion: number) => void;
|
||||
}
|
||||
|
||||
export function SimuladorDisyuntivas({ ejercicioId: _ejercicioId, onComplete }: SimuladorDisyuntivasProps) {
|
||||
const [bienX, setBienX] = useState(50);
|
||||
const [bienY, setBienY] = useState(50);
|
||||
const [validacion, setValidacion] = useState<'eficiente' | 'ineficiente' | 'inalcanzable' | null>(null);
|
||||
const [completado, setCompletado] = useState(false);
|
||||
|
||||
const MAX_X = 100;
|
||||
const MAX_Y = 80;
|
||||
|
||||
const calcularFPP = useCallback((x: number): number => {
|
||||
const ratio = x / MAX_X;
|
||||
const y = MAX_Y * Math.pow(1 - ratio, 0.7);
|
||||
return Math.max(0, Math.min(MAX_Y, y));
|
||||
}, []);
|
||||
|
||||
const validarPosicion = useCallback(() => {
|
||||
const yFPP = calcularFPP(bienX);
|
||||
const diferencia = Math.abs(bienY - yFPP);
|
||||
const tolerancia = 3;
|
||||
|
||||
if (bienY > yFPP + tolerancia) {
|
||||
return 'inalcanzable';
|
||||
} else if (diferencia <= tolerancia) {
|
||||
return 'eficiente';
|
||||
} else {
|
||||
return 'ineficiente';
|
||||
}
|
||||
}, [bienX, bienY, calcularFPP]);
|
||||
|
||||
const handleValidar = () => {
|
||||
const resultado = validarPosicion();
|
||||
setValidacion(resultado);
|
||||
|
||||
if (resultado === 'eficiente' && !completado) {
|
||||
setCompletado(true);
|
||||
if (onComplete) {
|
||||
onComplete(100);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setBienX(50);
|
||||
setBienY(50);
|
||||
setValidacion(null);
|
||||
setCompletado(false);
|
||||
};
|
||||
|
||||
// Generar puntos para la curva FPP
|
||||
const puntosFPP: string[] = [];
|
||||
for (let x = 0; x <= MAX_X; x += 2) {
|
||||
const y = calcularFPP(x);
|
||||
const svgX = 40 + (x / MAX_X) * 260;
|
||||
const svgY = 200 - (y / MAX_Y) * 180;
|
||||
puntosFPP.push(`${svgX},${svgY}`);
|
||||
}
|
||||
const pathData = puntosFPP.length > 0
|
||||
? `M ${puntosFPP.join(' L ')}`
|
||||
: '';
|
||||
|
||||
const colorValidacion = validacion === 'eficiente'
|
||||
? 'text-green-600 bg-green-50 border-green-200'
|
||||
: validacion === 'ineficiente'
|
||||
? 'text-yellow-600 bg-yellow-50 border-yellow-200'
|
||||
: validacion === 'inalcanzable'
|
||||
? 'text-red-600 bg-red-50 border-red-200'
|
||||
: 'text-gray-600 bg-gray-50 border-gray-200';
|
||||
|
||||
const mensajeValidacion = validacion === 'eficiente'
|
||||
? '¡Excelente! Estás sobre la FPP (Asignación eficiente)'
|
||||
: validacion === 'ineficiente'
|
||||
? 'Punto ineficiente: Estás dentro de la FPP, hay recursos sin usar'
|
||||
: validacion === 'inalcanzable'
|
||||
? 'Punto inalcanzable: No tienes suficientes recursos'
|
||||
: 'Ajusta los sliders para explorar la FPP';
|
||||
|
||||
const puntoColor = validacion === 'eficiente'
|
||||
? '#10b981'
|
||||
: validacion === 'ineficiente'
|
||||
? '#f59e0b'
|
||||
: validacion === 'inalcanzable'
|
||||
? '#ef4444'
|
||||
: '#6b7280';
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-4xl mx-auto bg-white rounded-lg shadow-lg p-6">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Simulador de Disyuntivas Económicas</h3>
|
||||
<p className="text-sm text-gray-500">Explora la Frontera de Posibilidades de Producción (FPP)</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="space-y-6">
|
||||
{/* Slider X */}
|
||||
<div>
|
||||
<label className="flex justify-between text-sm font-medium text-gray-700 mb-2">
|
||||
<span>Alimentos (X)</span>
|
||||
<span className="text-blue-600 font-bold">{bienX} millones de toneladas</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={MAX_X}
|
||||
value={bienX}
|
||||
onChange={(e) => setBienX(Number(e.target.value))}
|
||||
className="w-full h-2 bg-gray-200 rounded-lg cursor-pointer"
|
||||
style={{ accentColor: '#2563eb' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Slider Y */}
|
||||
<div>
|
||||
<label className="flex justify-between text-sm font-medium text-gray-700 mb-2">
|
||||
<span>Tecnología (Y)</span>
|
||||
<span className="text-green-600 font-bold">{bienY} millones de unidades</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={MAX_Y}
|
||||
value={bienY}
|
||||
onChange={(e) => setBienY(Number(e.target.value))}
|
||||
className="w-full h-2 bg-gray-200 rounded-lg cursor-pointer"
|
||||
style={{ accentColor: '#16a34a' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Mensaje de validación */}
|
||||
<div className={`p-4 rounded-lg border-2 ${colorValidacion}`}>
|
||||
<span className="font-semibold capitalize">{validacion || 'Selecciona'}:</span>
|
||||
<p className="text-sm mt-1">{mensajeValidacion}</p>
|
||||
</div>
|
||||
|
||||
{/* Botones */}
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handleValidar}
|
||||
disabled={completado}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 transition-colors"
|
||||
>
|
||||
Validar Posición
|
||||
</button>
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Reiniciar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mensaje de éxito */}
|
||||
{completado && (
|
||||
<div className="bg-green-100 border border-green-300 rounded-lg p-4 text-center">
|
||||
<p className="text-green-800 font-semibold">¡Ejercicio Completado!</p>
|
||||
<p className="text-green-700 text-2xl font-bold mt-1">100 puntos</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Gráfico SVG */}
|
||||
<div className="bg-gray-50 rounded-lg p-4 border-2 border-gray-200">
|
||||
<svg viewBox="0 0 340 240" className="w-full" style={{ minHeight: '300px' }}>
|
||||
{/* Grid */}
|
||||
<defs>
|
||||
<pattern id="grid" width="30" height="27" patternUnits="userSpaceOnUse">
|
||||
<path d="M 30 0 L 0 0 0 27" fill="none" stroke="#e5e7eb" strokeWidth="0.5"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="340" height="240" fill="url(#grid)" />
|
||||
|
||||
{/* Ejes */}
|
||||
<line x1="40" y1="210" x2="320" y2="210" stroke="#374151" strokeWidth="2" />
|
||||
<line x1="40" y1="210" x2="40" y2="30" stroke="#374151" strokeWidth="2" />
|
||||
|
||||
{/* Flechas */}
|
||||
<polygon points="320,210 315,207 315,213" fill="#374151" />
|
||||
<polygon points="40,30 37,35 43,35" fill="#374151" />
|
||||
|
||||
{/* Etiquetas */}
|
||||
<text x="180" y="235" textAnchor="middle" fill="#6b7280" fontSize="12">
|
||||
Alimentos (millones de toneladas)
|
||||
</text>
|
||||
<text x="15" y="120" textAnchor="middle" fill="#6b7280" fontSize="12" transform="rotate(-90, 15, 120)">
|
||||
Tecnología (millones de unidades)
|
||||
</text>
|
||||
|
||||
{/* Marcas X */}
|
||||
{[0, 25, 50, 75, 100].map((val, i) => (
|
||||
<g key={`x-${val}`}>
|
||||
<line x1={40 + i * 70} y1="210" x2={40 + i * 70} y2="215" stroke="#374151" />
|
||||
<text x={40 + i * 70} y="228" textAnchor="middle" fill="#6b7280" fontSize="11">{val}</text>
|
||||
</g>
|
||||
))}
|
||||
|
||||
{/* Marcas Y */}
|
||||
{[0, 20, 40, 60, 80].map((val, i) => (
|
||||
<g key={`y-${val}`}>
|
||||
<line x1="35" y1={210 - i * 45} x2="40" y2={210 - i * 45} stroke="#374151" />
|
||||
<text x="30" y={210 - i * 45 + 4} textAnchor="end" fill="#6b7280" fontSize="11">{val}</text>
|
||||
</g>
|
||||
))}
|
||||
|
||||
{/* Curva FPP */}
|
||||
{pathData && (
|
||||
<path
|
||||
d={pathData}
|
||||
fill="none"
|
||||
stroke="#2563eb"
|
||||
strokeWidth="3"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Punto actual */}
|
||||
<circle
|
||||
cx={40 + (bienX / MAX_X) * 280}
|
||||
cy={210 - (bienY / MAX_Y) * 180}
|
||||
r="8"
|
||||
fill={puntoColor}
|
||||
stroke="white"
|
||||
strokeWidth="3"
|
||||
/>
|
||||
|
||||
{/* Coordenadas */}
|
||||
<text
|
||||
x={40 + (bienX / MAX_X) * 280}
|
||||
y={210 - (bienY / MAX_Y) * 180 - 15}
|
||||
textAnchor="middle"
|
||||
fill="#1f2937"
|
||||
fontSize="12"
|
||||
fontWeight="bold"
|
||||
>
|
||||
({bienX}, {bienY})
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SimuladorDisyuntivas;
|
||||
3
frontend/src/components/exercises/modulo1/index.ts
Normal file
3
frontend/src/components/exercises/modulo1/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { SimuladorDisyuntivas } from './SimuladorDisyuntivas';
|
||||
export { QuizBienes } from './QuizBienes';
|
||||
export { FlujoCircular } from './FlujoCircular';
|
||||
505
frontend/src/components/exercises/modulo2/ConstructorCurvas.tsx
Normal file
505
frontend/src/components/exercises/modulo2/ConstructorCurvas.tsx
Normal file
@@ -0,0 +1,505 @@
|
||||
import React, { useState, useRef, useCallback } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { LineChart, Check, X, RotateCcw, Trophy, ArrowRight, HelpCircle } from 'lucide-react';
|
||||
|
||||
interface Punto {
|
||||
x: number;
|
||||
y: number;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface ConstructorCurvasProps {
|
||||
ejercicioId: string;
|
||||
onComplete?: (puntuacion: number) => void;
|
||||
}
|
||||
|
||||
type Nivel = 'demanda' | 'oferta' | 'equilibrio';
|
||||
type TipoCurva = 'demanda' | 'oferta';
|
||||
|
||||
interface NivelConfig {
|
||||
tipo: Nivel;
|
||||
titulo: string;
|
||||
descripcion: string;
|
||||
tipoCurvaEsperada: TipoCurva | 'ambas';
|
||||
mensajeExito: string;
|
||||
}
|
||||
|
||||
const niveles: NivelConfig[] = [
|
||||
{
|
||||
tipo: 'demanda',
|
||||
titulo: 'Nivel 1: Curva de Demanda',
|
||||
descripcion: 'La demanda tiene pendiente negativa (cuando el precio sube, la cantidad demandada baja). Coloca al menos 2 puntos y traza la línea.',
|
||||
tipoCurvaEsperada: 'demanda',
|
||||
mensajeExito: '¡Correcto! La curva de demanda tiene pendiente negativa.'
|
||||
},
|
||||
{
|
||||
tipo: 'oferta',
|
||||
titulo: 'Nivel 2: Curva de Oferta',
|
||||
descripcion: 'La oferta tiene pendiente positiva (cuando el precio sube, los productores quieren vender más). Coloca al menos 2 puntos.',
|
||||
tipoCurvaEsperada: 'oferta',
|
||||
mensajeExito: '¡Correcto! La curva de oferta tiene pendiente positiva.'
|
||||
},
|
||||
{
|
||||
tipo: 'equilibrio',
|
||||
titulo: 'Nivel 3: Equilibrio de Mercado',
|
||||
descripcion: 'Dibuja ambas curvas para encontrar el punto de equilibrio donde se cruzan.',
|
||||
tipoCurvaEsperada: 'ambas',
|
||||
mensajeExito: '¡Excelente! Has encontrado el equilibrio de mercado.'
|
||||
}
|
||||
];
|
||||
|
||||
const GRID_SIZE = 300;
|
||||
const PADDING = 40;
|
||||
const MAX_PRECIO = 100;
|
||||
const MAX_CANTIDAD = 100;
|
||||
|
||||
export const ConstructorCurvas: React.FC<ConstructorCurvasProps> = ({ onComplete, ejercicioId: _ejercicioId }) => {
|
||||
const [nivelActual, setNivelActual] = useState(0);
|
||||
const [puntosDemanda, setPuntosDemanda] = useState<Punto[]>([]);
|
||||
const [puntosOferta, setPuntosOferta] = useState<Punto[]>([]);
|
||||
const [modoActivo, setModoActivo] = useState<TipoCurva>('demanda');
|
||||
const [mensaje, setMensaje] = useState<string>('');
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
const [score, setScore] = useState(0);
|
||||
const [_startTime] = useState(Date.now());
|
||||
const [, setPuntosDibujados] = useState<{ demanda: boolean; oferta: boolean }>({ demanda: false, oferta: false });
|
||||
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
const [draggedPoint, setDraggedPoint] = useState<string | null>(null);
|
||||
|
||||
const nivel = niveles[nivelActual];
|
||||
|
||||
const cartesianToSvg = useCallback((x: number, y: number) => {
|
||||
const svgX = PADDING + (x / MAX_CANTIDAD) * GRID_SIZE;
|
||||
const svgY = PADDING + GRID_SIZE - (y / MAX_PRECIO) * GRID_SIZE;
|
||||
return { x: svgX, y: svgY };
|
||||
}, []);
|
||||
|
||||
const svgToCartesian = useCallback((svgX: number, svgY: number) => {
|
||||
const x = ((svgX - PADDING) / GRID_SIZE) * MAX_CANTIDAD;
|
||||
const y = ((PADDING + GRID_SIZE - svgY) / GRID_SIZE) * MAX_PRECIO;
|
||||
return {
|
||||
x: Math.max(0, Math.min(MAX_CANTIDAD, Math.round(x))),
|
||||
y: Math.max(0, Math.min(MAX_PRECIO, Math.round(y)))
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSvgClick = (e: React.MouseEvent<SVGSVGElement>) => {
|
||||
if (draggedPoint || nivelActual === 2) return;
|
||||
|
||||
const rect = svgRef.current?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
|
||||
const svgX = e.clientX - rect.left;
|
||||
const svgY = e.clientY - rect.top;
|
||||
const cartesian = svgToCartesian(svgX, svgY);
|
||||
|
||||
if (nivelActual === 0 && puntosDemanda.length >= 4) {
|
||||
setMensaje('Máximo 4 puntos para la demanda');
|
||||
return;
|
||||
}
|
||||
if (nivelActual === 1 && puntosOferta.length >= 4) {
|
||||
setMensaje('Máximo 4 puntos para la oferta');
|
||||
return;
|
||||
}
|
||||
|
||||
const newPoint: Punto = {
|
||||
x: cartesian.x,
|
||||
y: cartesian.y,
|
||||
id: `point-${Date.now()}-${Math.random()}`
|
||||
};
|
||||
|
||||
if (modoActivo === 'demanda') {
|
||||
setPuntosDemanda(prev => [...prev, newPoint]);
|
||||
} else {
|
||||
setPuntosOferta(prev => [...prev, newPoint]);
|
||||
}
|
||||
setMensaje('');
|
||||
};
|
||||
|
||||
const handlePointDrag = (pointId: string, _tipo: TipoCurva) => {
|
||||
setDraggedPoint(pointId);
|
||||
};
|
||||
|
||||
const handlePointMove = (e: React.MouseEvent<SVGSVGElement>) => {
|
||||
if (!draggedPoint) return;
|
||||
|
||||
const rect = svgRef.current?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
|
||||
const svgX = e.clientX - rect.left;
|
||||
const svgY = e.clientY - rect.top;
|
||||
const cartesian = svgToCartesian(svgX, svgY);
|
||||
|
||||
const updatePoint = (puntos: Punto[]) =>
|
||||
puntos.map(p => p.id === draggedPoint ? { ...p, x: cartesian.x, y: cartesian.y } : p);
|
||||
|
||||
if (puntosDemanda.some(p => p.id === draggedPoint)) {
|
||||
setPuntosDemanda(updatePoint);
|
||||
} else if (puntosOferta.some(p => p.id === draggedPoint)) {
|
||||
setPuntosOferta(updatePoint);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePointUp = () => {
|
||||
setDraggedPoint(null);
|
||||
};
|
||||
|
||||
const calcularPendiente = (puntos: Punto[]): number | null => {
|
||||
if (puntos.length < 2) return null;
|
||||
const sorted = [...puntos].sort((a, b) => a.x - b.x);
|
||||
const first = sorted[0];
|
||||
const last = sorted[sorted.length - 1];
|
||||
if (last.x === first.x) return 0;
|
||||
return (last.y - first.y) / (last.x - first.x);
|
||||
};
|
||||
|
||||
const validarCurva = () => {
|
||||
const puntos = modoActivo === 'demanda' ? puntosDemanda : puntosOferta;
|
||||
|
||||
if (puntos.length < 2) {
|
||||
setMensaje('Necesitas al menos 2 puntos para trazar una curva');
|
||||
return;
|
||||
}
|
||||
|
||||
const pendiente = calcularPendiente(puntos);
|
||||
if (pendiente === null) return;
|
||||
|
||||
if (modoActivo === 'demanda') {
|
||||
if (pendiente >= 0) {
|
||||
setMensaje('La demanda debe tener pendiente negativa (bajar de izquierda a derecha)');
|
||||
return;
|
||||
}
|
||||
setPuntosDibujados(prev => ({ ...prev, demanda: true }));
|
||||
} else {
|
||||
if (pendiente <= 0) {
|
||||
setMensaje('La oferta debe tener pendiente positiva (subir de izquierda a derecha)');
|
||||
return;
|
||||
}
|
||||
setPuntosDibujados(prev => ({ ...prev, oferta: true }));
|
||||
}
|
||||
|
||||
setMensaje('');
|
||||
setShowSuccess(true);
|
||||
setScore(prev => prev + 33);
|
||||
|
||||
setTimeout(() => {
|
||||
if (nivelActual < 2) {
|
||||
setNivelActual(prev => prev + 1);
|
||||
setShowSuccess(false);
|
||||
setMensaje('');
|
||||
if (nivelActual === 0) setModoActivo('oferta');
|
||||
}
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const validarEquilibrio = () => {
|
||||
if (puntosDemanda.length < 2 || puntosOferta.length < 2) {
|
||||
setMensaje('Necesitas trazar ambas curvas con al menos 2 puntos cada una');
|
||||
return;
|
||||
}
|
||||
|
||||
setShowSuccess(true);
|
||||
setScore(100);
|
||||
|
||||
setTimeout(() => {
|
||||
if (onComplete) {
|
||||
onComplete(100);
|
||||
}
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const reiniciar = () => {
|
||||
setPuntosDemanda([]);
|
||||
setPuntosOferta([]);
|
||||
setNivelActual(0);
|
||||
setModoActivo('demanda');
|
||||
setMensaje('');
|
||||
setShowSuccess(false);
|
||||
setScore(0);
|
||||
setPuntosDibujados({ demanda: false, oferta: false });
|
||||
};
|
||||
|
||||
const eliminarPunto = (id: string, tipo: TipoCurva) => {
|
||||
if (tipo === 'demanda') {
|
||||
setPuntosDemanda(prev => prev.filter(p => p.id !== id));
|
||||
} else {
|
||||
setPuntosOferta(prev => prev.filter(p => p.id !== id));
|
||||
}
|
||||
};
|
||||
|
||||
const renderLineaCurva = (puntos: Punto[], color: string) => {
|
||||
if (puntos.length < 2) return null;
|
||||
const sorted = [...puntos].sort((a, b) => a.x - b.x);
|
||||
const points = sorted.map(p => {
|
||||
const svg = cartesianToSvg(p.x, p.y);
|
||||
return `${svg.x},${svg.y}`;
|
||||
}).join(' ');
|
||||
|
||||
return (
|
||||
<polyline
|
||||
points={points}
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-4xl mx-auto p-6 bg-white rounded-xl shadow-lg">
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<LineChart className="w-8 h-8 text-blue-600" />
|
||||
<h2 className="text-2xl font-bold text-gray-800">{nivel.titulo}</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-gray-500">Nivel {nivelActual + 1} de 3</span>
|
||||
<div className="w-32 h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<motion.div
|
||||
className="h-full bg-blue-600"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${score}%` }}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={reiniciar}
|
||||
className="p-2 text-gray-500 hover:text-blue-600 transition-colors"
|
||||
>
|
||||
<RotateCcw className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-600">{nivel.descripcion}</p>
|
||||
</div>
|
||||
|
||||
{nivelActual === 2 && (
|
||||
<div className="mb-4 p-3 bg-blue-50 rounded-lg flex items-center gap-2">
|
||||
<HelpCircle className="w-5 h-5 text-blue-600" />
|
||||
<span className="text-sm text-blue-700">
|
||||
Nivel Avanzado: Dibuja ambas curvas. La demanda (azul) con pendiente negativa,
|
||||
y la oferta (verde) con pendiente positiva.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-6">
|
||||
<div className="flex-1">
|
||||
<svg
|
||||
ref={svgRef}
|
||||
width={GRID_SIZE + 2 * PADDING}
|
||||
height={GRID_SIZE + 2 * PADDING}
|
||||
className="border-2 border-gray-300 rounded-lg bg-white cursor-crosshair"
|
||||
onClick={handleSvgClick}
|
||||
onMouseMove={handlePointMove}
|
||||
onMouseUp={handlePointUp}
|
||||
onMouseLeave={handlePointUp}
|
||||
>
|
||||
{/* Grid */}
|
||||
{Array.from({ length: 11 }).map((_, i) => (
|
||||
<g key={i}>
|
||||
<line
|
||||
x1={PADDING + (i * GRID_SIZE) / 10}
|
||||
y1={PADDING}
|
||||
x2={PADDING + (i * GRID_SIZE) / 10}
|
||||
y2={PADDING + GRID_SIZE}
|
||||
stroke="#e5e7eb"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<line
|
||||
x1={PADDING}
|
||||
y1={PADDING + (i * GRID_SIZE) / 10}
|
||||
x2={PADDING + GRID_SIZE}
|
||||
y2={PADDING + (i * GRID_SIZE) / 10}
|
||||
stroke="#e5e7eb"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
</g>
|
||||
))}
|
||||
|
||||
{/* Ejes */}
|
||||
<line
|
||||
x1={PADDING}
|
||||
y1={PADDING + GRID_SIZE}
|
||||
x2={PADDING + GRID_SIZE}
|
||||
y2={PADDING + GRID_SIZE}
|
||||
stroke="#374151"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<line
|
||||
x1={PADDING}
|
||||
y1={PADDING}
|
||||
x2={PADDING}
|
||||
y2={PADDING + GRID_SIZE}
|
||||
stroke="#374151"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
|
||||
{/* Labels ejes */}
|
||||
<text x={PADDING + GRID_SIZE / 2} y={PADDING + GRID_SIZE + 25} textAnchor="middle" className="text-sm fill-gray-600">
|
||||
Cantidad
|
||||
</text>
|
||||
<text x={15} y={PADDING + GRID_SIZE / 2} textAnchor="middle" transform={`rotate(-90, 15, ${PADDING + GRID_SIZE / 2})`} className="text-sm fill-gray-600">
|
||||
Precio
|
||||
</text>
|
||||
|
||||
{/* Curvas */}
|
||||
{(nivelActual === 0 || nivelActual === 2) && renderLineaCurva(puntosDemanda, '#3b82f6')}
|
||||
{(nivelActual === 1 || nivelActual === 2) && renderLineaCurva(puntosOferta, '#22c55e')}
|
||||
|
||||
{/* Puntos Demanda */}
|
||||
{(nivelActual === 0 || nivelActual === 2) && puntosDemanda.map(punto => {
|
||||
const svg = cartesianToSvg(punto.x, punto.y);
|
||||
return (
|
||||
<motion.g key={punto.id}>
|
||||
<circle
|
||||
cx={svg.x}
|
||||
cy={svg.y}
|
||||
r="8"
|
||||
fill="#3b82f6"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
className="cursor-move hover:r-10"
|
||||
onMouseDown={() => handlePointDrag(punto.id, 'demanda')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
eliminarPunto(punto.id, 'demanda');
|
||||
}}
|
||||
/>
|
||||
<text x={svg.x} y={svg.y - 12} textAnchor="middle" className="text-xs fill-gray-500">
|
||||
({punto.x}, {punto.y})
|
||||
</text>
|
||||
</motion.g>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Puntos Oferta */}
|
||||
{(nivelActual === 1 || nivelActual === 2) && puntosOferta.map(punto => {
|
||||
const svg = cartesianToSvg(punto.x, punto.y);
|
||||
return (
|
||||
<motion.g key={punto.id}>
|
||||
<circle
|
||||
cx={svg.x}
|
||||
cy={svg.y}
|
||||
r="8"
|
||||
fill="#22c55e"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
className="cursor-move"
|
||||
onMouseDown={() => handlePointDrag(punto.id, 'oferta')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
eliminarPunto(punto.id, 'oferta');
|
||||
}}
|
||||
/>
|
||||
<text x={svg.x} y={svg.y - 12} textAnchor="middle" className="text-xs fill-gray-500">
|
||||
({punto.x}, {punto.y})
|
||||
</text>
|
||||
</motion.g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="w-full md:w-64 space-y-4">
|
||||
{nivelActual === 2 && (
|
||||
<div className="flex gap-2 p-3 bg-gray-100 rounded-lg">
|
||||
<button
|
||||
onClick={() => setModoActivo('demanda')}
|
||||
className={`flex-1 py-2 px-3 rounded text-sm font-medium transition-colors ${
|
||||
modoActivo === 'demanda'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-white text-gray-700 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
Demanda
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setModoActivo('oferta')}
|
||||
className={`flex-1 py-2 px-3 rounded text-sm font-medium transition-colors ${
|
||||
modoActivo === 'oferta'
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-white text-gray-700 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
Oferta
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-4 bg-gray-50 rounded-lg">
|
||||
<h3 className="font-semibold text-gray-700 mb-2">Puntos colocados:</h3>
|
||||
{modoActivo === 'demanda' || nivelActual === 2 ? (
|
||||
<div className="mb-2">
|
||||
<span className="text-sm text-blue-600 font-medium">Demanda: </span>
|
||||
<span className="text-sm text-gray-600">{puntosDemanda.length} puntos</span>
|
||||
</div>
|
||||
) : null}
|
||||
{(modoActivo === 'oferta' || nivelActual === 2) && (
|
||||
<div>
|
||||
<span className="text-sm text-green-600 font-medium">Oferta: </span>
|
||||
<span className="text-sm text-gray-600">{puntosOferta.length} puntos</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{mensaje && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="p-3 bg-red-50 border border-red-200 rounded-lg flex items-start gap-2"
|
||||
>
|
||||
<X className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-red-700">{mensaje}</p>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{nivelActual < 2 ? (
|
||||
<button
|
||||
onClick={validarCurva}
|
||||
disabled={showSuccess}
|
||||
className="w-full py-3 px-4 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
<Check className="w-5 h-5" />
|
||||
Validar Curva
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={validarEquilibrio}
|
||||
disabled={showSuccess}
|
||||
className="w-full py-3 px-4 bg-purple-600 text-white rounded-lg font-medium hover:bg-purple-700 disabled:opacity-50 transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
<Check className="w-5 h-5" />
|
||||
Validar Equilibrio
|
||||
</button>
|
||||
)}
|
||||
|
||||
<AnimatePresence>
|
||||
{showSuccess && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.9 }}
|
||||
className="p-4 bg-green-50 border border-green-200 rounded-lg text-center"
|
||||
>
|
||||
<Trophy className="w-8 h-8 text-green-500 mx-auto mb-2" />
|
||||
<p className="font-semibold text-green-700">{nivel.mensajeExito}</p>
|
||||
{nivelActual === 2 && (
|
||||
<div className="mt-3 flex items-center justify-center gap-2 text-green-600">
|
||||
<span>Completado</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConstructorCurvas;
|
||||
467
frontend/src/components/exercises/modulo2/IdentificarShocks.tsx
Normal file
467
frontend/src/components/exercises/modulo2/IdentificarShocks.tsx
Normal file
@@ -0,0 +1,467 @@
|
||||
import React, { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Brain, ArrowRight, ArrowLeft, TrendingUp, TrendingDown, CheckCircle2, XCircle, Trophy, RotateCcw, BookOpen } from 'lucide-react';
|
||||
|
||||
interface IdentificarShocksProps {
|
||||
ejercicioId: string;
|
||||
onComplete?: (puntuacion: number) => void;
|
||||
}
|
||||
|
||||
type DireccionShock = 'oferta-up' | 'oferta-down' | 'demanda-up' | 'demanda-down';
|
||||
type CurvaTipo = 'oferta' | 'demanda';
|
||||
type Direccion = 'arriba' | 'abajo';
|
||||
|
||||
interface Escenario {
|
||||
id: number;
|
||||
descripcion: string;
|
||||
respuesta: DireccionShock;
|
||||
curva: CurvaTipo;
|
||||
direccion: Direccion;
|
||||
explicacion: string;
|
||||
dificultad: 'facil' | 'medio' | 'dificil';
|
||||
}
|
||||
|
||||
const escenarios: Escenario[] = [
|
||||
{
|
||||
id: 1,
|
||||
descripcion: 'Una nueva tecnología permite producir smartphones más rápido y barato.',
|
||||
respuesta: 'oferta-up',
|
||||
curva: 'oferta',
|
||||
direccion: 'arriba',
|
||||
explicacion: 'La tecnología mejora la productividad, reduciendo costos. Esto aumenta la oferta (la curva se desplaza a la derecha).',
|
||||
dificultad: 'facil'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
descripcion: 'Se anuncia que el café causa cáncer y la demanda disminuye drásticamente.',
|
||||
respuesta: 'demanda-down',
|
||||
curva: 'demanda',
|
||||
direccion: 'abajo',
|
||||
explicacion: 'Las preferencias de los consumidores cambian negativamente. La demanda disminuye (la curva se desplaza a la izquierda).',
|
||||
dificultad: 'facil'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
descripcion: 'Una sequía severa destruye el 40% de la cosecha de trigo.',
|
||||
respuesta: 'oferta-down',
|
||||
curva: 'oferta',
|
||||
direccion: 'abajo',
|
||||
explicacion: 'La sequía reduce la cantidad disponible de trigo. La oferta disminuye (la curva se desplaza a la izquierda).',
|
||||
dificultad: 'facil'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
descripcion: 'El ingreso promedio de la población aumenta un 15% (bien normal).',
|
||||
respuesta: 'demanda-up',
|
||||
curva: 'demanda',
|
||||
direccion: 'arriba',
|
||||
explicacion: 'Para bienes normales, al aumentar el ingreso, aumenta la demanda (la curva se desplaza a la derecha).',
|
||||
dificultad: 'medio'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
descripcion: 'El precio del petróleo (insumo) sube un 30%.',
|
||||
respuesta: 'oferta-down',
|
||||
curva: 'oferta',
|
||||
direccion: 'abajo',
|
||||
explicacion: 'Al subir el costo de los insumos, producir es más caro. La oferta disminuye (la curva se desplaza a la izquierda).',
|
||||
dificultad: 'medio'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
descripcion: 'El gobierno subsidia la compra de autos eléctricos con $5,000.',
|
||||
respuesta: 'demanda-up',
|
||||
curva: 'demanda',
|
||||
direccion: 'arriba',
|
||||
explicacion: 'El subsidio reduce el precio efectivo para consumidores. La demanda aumenta (la curva se desplaza a la derecha).',
|
||||
dificultad: 'dificil'
|
||||
}
|
||||
];
|
||||
|
||||
interface Opcion {
|
||||
value: DireccionShock;
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
color: string;
|
||||
}
|
||||
|
||||
const opciones: Opcion[] = [
|
||||
{ value: 'oferta-up', label: 'Oferta ↑', icon: <TrendingUp className="w-5 h-5" />, color: 'green' },
|
||||
{ value: 'oferta-down', label: 'Oferta ↓', icon: <TrendingDown className="w-5 h-5" />, color: 'red' },
|
||||
{ value: 'demanda-up', label: 'Demanda ↑', icon: <TrendingUp className="w-5 h-5" />, color: 'blue' },
|
||||
{ value: 'demanda-down', label: 'Demanda ↓', icon: <TrendingDown className="w-5 h-5" />, color: 'orange' },
|
||||
];
|
||||
|
||||
export const IdentificarShocks: React.FC<IdentificarShocksProps> = ({ onComplete, ejercicioId: _ejercicioId }) => {
|
||||
const [escenarioActual, setEscenarioActual] = useState(0);
|
||||
const [respuestaSeleccionada, setRespuestaSeleccionada] = useState<DireccionShock | null>(null);
|
||||
const [mostrarResultado, setMostrarResultado] = useState(false);
|
||||
const [score, setScore] = useState(0);
|
||||
const [respuestasCorrectas, setRespuestasCorrectas] = useState(0);
|
||||
const [_startTime] = useState(Date.now());
|
||||
const [completado, setCompletado] = useState(false);
|
||||
|
||||
const escenario = escenarios[escenarioActual];
|
||||
|
||||
const handleSeleccionar = (respuesta: DireccionShock) => {
|
||||
if (mostrarResultado) return;
|
||||
setRespuestaSeleccionada(respuesta);
|
||||
};
|
||||
|
||||
const handleVerificar = () => {
|
||||
if (!respuestaSeleccionada) return;
|
||||
|
||||
const esCorrecta = respuestaSeleccionada === escenario.respuesta;
|
||||
setMostrarResultado(true);
|
||||
|
||||
if (esCorrecta) {
|
||||
setScore(prev => prev + Math.round(100 / escenarios.length));
|
||||
setRespuestasCorrectas(prev => prev + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSiguiente = () => {
|
||||
if (escenarioActual < escenarios.length - 1) {
|
||||
setEscenarioActual(prev => prev + 1);
|
||||
setRespuestaSeleccionada(null);
|
||||
setMostrarResultado(false);
|
||||
} else {
|
||||
setCompletado(true);
|
||||
if (onComplete) {
|
||||
onComplete(score);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleReiniciar = () => {
|
||||
setEscenarioActual(0);
|
||||
setRespuestaSeleccionada(null);
|
||||
setMostrarResultado(false);
|
||||
setScore(0);
|
||||
setRespuestasCorrectas(0);
|
||||
setCompletado(false);
|
||||
};
|
||||
|
||||
const getDificultadColor = (dificultad: string) => {
|
||||
switch (dificultad) {
|
||||
case 'facil': return 'bg-green-100 text-green-700';
|
||||
case 'medio': return 'bg-yellow-100 text-yellow-700';
|
||||
case 'dificil': return 'bg-red-100 text-red-700';
|
||||
default: return 'bg-gray-100 text-gray-700';
|
||||
}
|
||||
};
|
||||
|
||||
const renderGraficoShock = () => {
|
||||
const { curva, direccion } = escenario;
|
||||
const isOferta = curva === 'oferta';
|
||||
const isUp = direccion === 'arriba';
|
||||
|
||||
return (
|
||||
<svg width="300" height="250" className="mx-auto">
|
||||
{/* Grid */}
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<g key={i}>
|
||||
<line x1={50 + i * 40} y1="30" x2={50 + i * 40} y2="210" stroke="#e5e7eb" strokeWidth="1" />
|
||||
<line x1="50" y1={30 + i * 36} x2="250" y2={30 + i * 36} stroke="#e5e7eb" strokeWidth="1" />
|
||||
</g>
|
||||
))}
|
||||
|
||||
{/* Ejes */}
|
||||
<line x1="50" y1="210" x2="250" y2="210" stroke="#374151" strokeWidth="2" />
|
||||
<line x1="50" y1="30" x2="50" y2="210" stroke="#374151" strokeWidth="2" />
|
||||
|
||||
{/* Curva original */}
|
||||
{isOferta ? (
|
||||
<line x1="80" y1="180" x2="220" y2="80" stroke="#22c55e" strokeWidth="3" />
|
||||
) : (
|
||||
<line x1="80" y1="80" x2="220" y2="180" stroke="#3b82f6" strokeWidth="3" />
|
||||
)}
|
||||
<text x={isOferta ? 230 : 230} y={isOferta ? 75 : 190} className={`text-sm ${isOferta ? 'fill-green-600' : 'fill-blue-600'}`}>
|
||||
{isOferta ? 'S₁' : 'D₁'}
|
||||
</text>
|
||||
|
||||
{/* Curva desplazada */}
|
||||
<motion.g
|
||||
initial={{ opacity: 0, x: isUp ? 30 : -30 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{isOferta ? (
|
||||
<line x1={isUp ? 110 : 50} y1="180" x2={isUp ? 250 : 190} y2="80" stroke="#22c55e" strokeWidth="3" strokeDasharray="5,5" />
|
||||
) : (
|
||||
<line x1={isUp ? 110 : 50} y1="80" x2={isUp ? 250 : 190} y2="180" stroke="#3b82f6" strokeWidth="3" strokeDasharray="5,5" />
|
||||
)}
|
||||
<text x={isUp ? 260 : 200} y={isOferta ? 75 : 190} className={`text-sm ${isOferta ? 'fill-green-600' : 'fill-blue-600'}`}>
|
||||
{isOferta ? 'S₂' : 'D₂'}
|
||||
</text>
|
||||
</motion.g>
|
||||
|
||||
{/* Flecha de dirección */}
|
||||
<motion.path
|
||||
d={isUp ? 'M 280 130 L 300 130' : 'M 300 130 L 280 130'}
|
||||
stroke={isOferta ? '#22c55e' : '#3b82f6'}
|
||||
strokeWidth="3"
|
||||
markerEnd={`url(#arrowhead-${isOferta ? 'green' : 'blue'})`}
|
||||
initial={{ pathLength: 0 }}
|
||||
animate={{ pathLength: 1 }}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
/>
|
||||
|
||||
{/* Defs para flechas */}
|
||||
<defs>
|
||||
<marker id="arrowhead-green" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
||||
<polygon points="0 0, 10 3.5, 0 7" fill="#22c55e" />
|
||||
</marker>
|
||||
<marker id="arrowhead-blue" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
||||
<polygon points="0 0, 10 3.5, 0 7" fill="#3b82f6" />
|
||||
</marker>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
if (completado) {
|
||||
const porcentaje = Math.round((respuestasCorrectas / escenarios.length) * 100);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="w-full max-w-2xl mx-auto p-8 bg-white rounded-xl shadow-lg text-center"
|
||||
>
|
||||
<Trophy className="w-16 h-16 text-yellow-500 mx-auto mb-4" />
|
||||
<h2 className="text-3xl font-bold text-gray-800 mb-2">¡Ejercicio Completado!</h2>
|
||||
<p className="text-gray-600 mb-6">Has identificado shocks del mercado</p>
|
||||
|
||||
<div className="bg-gray-50 rounded-lg p-6 mb-6">
|
||||
<div className="text-5xl font-bold text-purple-600 mb-2">{porcentaje}%</div>
|
||||
<p className="text-gray-600">
|
||||
{respuestasCorrectas} de {escenarios.length} respuestas correctas
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleReiniciar}
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-purple-600 text-white rounded-lg font-medium hover:bg-purple-700 transition-colors"
|
||||
>
|
||||
<RotateCcw className="w-5 h-5" />
|
||||
Intentar de nuevo
|
||||
</button>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-4xl mx-auto p-6 bg-white rounded-xl shadow-lg">
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Brain className="w-8 h-8 text-purple-600" />
|
||||
<h2 className="text-2xl font-bold text-gray-800">Identificar Shocks del Mercado</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getDificultadColor(escenario.dificultad)}`}>
|
||||
{escenario.dificultad.toUpperCase()}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
{escenarioActual + 1} de {escenarios.length}
|
||||
</span>
|
||||
<div className="w-32 h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<motion.div
|
||||
className="h-full bg-purple-600"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${((escenarioActual + 1) / escenarios.length) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-600">
|
||||
Lee cada escenario e identifica qué curva se desplaza y en qué dirección.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<div className="bg-gradient-to-br from-purple-50 to-blue-50 rounded-lg p-6 mb-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<BookOpen className="w-6 h-6 text-purple-600 flex-shrink-0 mt-1" />
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-800 mb-2">Escenario {escenario.id}</h3>
|
||||
<p className="text-gray-700 text-lg">{escenario.descripcion}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{opciones.map((opcion) => {
|
||||
const isSelected = respuestaSeleccionada === opcion.value;
|
||||
const isCorrect = mostrarResultado && opcion.value === escenario.respuesta;
|
||||
const isWrong = mostrarResultado && isSelected && opcion.value !== escenario.respuesta;
|
||||
|
||||
let buttonClass = 'p-4 rounded-lg border-2 transition-all flex flex-col items-center gap-2 ';
|
||||
|
||||
if (isCorrect) {
|
||||
buttonClass += 'border-green-500 bg-green-50';
|
||||
} else if (isWrong) {
|
||||
buttonClass += 'border-red-500 bg-red-50';
|
||||
} else if (isSelected) {
|
||||
buttonClass += `border-${opcion.color}-500 bg-${opcion.color}-50`;
|
||||
} else {
|
||||
buttonClass += 'border-gray-200 hover:border-gray-300 hover:bg-gray-50';
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
key={opcion.value}
|
||||
onClick={() => handleSeleccionar(opcion.value)}
|
||||
disabled={mostrarResultado}
|
||||
whileHover={!mostrarResultado ? { scale: 1.02 } : {}}
|
||||
whileTap={!mostrarResultado ? { scale: 0.98 } : {}}
|
||||
className={buttonClass}
|
||||
>
|
||||
{opcion.icon}
|
||||
<span className={`font-semibold ${
|
||||
isCorrect ? 'text-green-700' :
|
||||
isWrong ? 'text-red-700' :
|
||||
isSelected ? `text-${opcion.color}-700` : 'text-gray-700'
|
||||
}`}>
|
||||
{opcion.label}
|
||||
</span>
|
||||
{isCorrect && <CheckCircle2 className="w-5 h-5 text-green-600" />}
|
||||
{isWrong && <XCircle className="w-5 h-5 text-red-600" />}
|
||||
</motion.button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{mostrarResultado && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
className={`mt-4 p-4 rounded-lg ${
|
||||
respuestaSeleccionada === escenario.respuesta
|
||||
? 'bg-green-50 border border-green-200'
|
||||
: 'bg-red-50 border border-red-200'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
{respuestaSeleccionada === escenario.respuesta ? (
|
||||
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
|
||||
) : (
|
||||
<XCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
|
||||
)}
|
||||
<div>
|
||||
<p className={`font-semibold ${
|
||||
respuestaSeleccionada === escenario.respuesta ? 'text-green-800' : 'text-red-800'
|
||||
}`}>
|
||||
{respuestaSeleccionada === escenario.respuesta ? '¡Correcto!' : 'Incorrecto'}
|
||||
</p>
|
||||
<p className="text-sm mt-1 text-gray-700">{escenario.explicacion}</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className="mt-4 flex gap-3">
|
||||
{!mostrarResultado ? (
|
||||
<button
|
||||
onClick={handleVerificar}
|
||||
disabled={!respuestaSeleccionada}
|
||||
className="flex-1 py-3 px-4 bg-purple-600 text-white rounded-lg font-medium hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Verificar respuesta
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleSiguiente}
|
||||
className="flex-1 py-3 px-4 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
{escenarioActual < escenarios.length - 1 ? (
|
||||
<>
|
||||
Siguiente
|
||||
<ArrowRight className="w-5 h-5" />
|
||||
</>
|
||||
) : (
|
||||
'Finalizar'
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<h3 className="font-semibold text-gray-700 mb-4 text-center">Visualización del Shock</h3>
|
||||
{renderGraficoShock()}
|
||||
|
||||
<div className="mt-4 p-3 bg-white rounded-lg">
|
||||
<h4 className="font-medium text-gray-700 mb-2">Leyenda:</h4>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-0.5 bg-blue-600"></div>
|
||||
<span className="text-gray-600">Curva de Demanda (D)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-0.5 bg-green-600"></div>
|
||||
<span className="text-gray-600">Curva de Oferta (S)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-0.5 border-t-2 border-dashed border-gray-400"></div>
|
||||
<span className="text-gray-600">Curva después del shock</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-3 bg-yellow-50 rounded-lg border border-yellow-200">
|
||||
<p className="text-sm text-yellow-800">
|
||||
<strong>Tip:</strong> Recuerda que:
|
||||
</p>
|
||||
<ul className="text-sm text-yellow-700 mt-1 space-y-1">
|
||||
<li>• Factores de oferta: tecnología, insumos, número de vendedores</li>
|
||||
<li>• Factores de demanda: ingreso, preferencias, precios relacionados</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-between items-center">
|
||||
<button
|
||||
onClick={() => setEscenarioActual(Math.max(0, escenarioActual - 1))}
|
||||
disabled={escenarioActual === 0}
|
||||
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Anterior
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
{escenarios.map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
index === escenarioActual
|
||||
? 'bg-purple-600'
|
||||
: index < escenarioActual
|
||||
? 'bg-green-500'
|
||||
: 'bg-gray-300'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setEscenarioActual(Math.min(escenarios.length - 1, escenarioActual + 1))}
|
||||
disabled={escenarioActual === escenarios.length - 1}
|
||||
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Siguiente
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IdentificarShocks;
|
||||
454
frontend/src/components/exercises/modulo2/SimuladorPrecios.tsx
Normal file
454
frontend/src/components/exercises/modulo2/SimuladorPrecios.tsx
Normal file
@@ -0,0 +1,454 @@
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { TrendingUp, TrendingDown, AlertTriangle, Calculator, RotateCcw, Info } from 'lucide-react';
|
||||
|
||||
interface SimuladorPreciosProps {
|
||||
ejercicioId: string;
|
||||
onComplete?: (puntuacion: number) => void;
|
||||
}
|
||||
|
||||
interface CurvaParams {
|
||||
pendienteDemanda: number;
|
||||
interceptoDemanda: number;
|
||||
pendienteOferta: number;
|
||||
interceptoOferta: number;
|
||||
}
|
||||
|
||||
const DEFAULT_PARAMS: CurvaParams = {
|
||||
pendienteDemanda: -1.5,
|
||||
interceptoDemanda: 90,
|
||||
pendienteOferta: 1.2,
|
||||
interceptoOferta: 10
|
||||
};
|
||||
|
||||
const calcularEquilibrio = (params: CurvaParams) => {
|
||||
const { pendienteDemanda, interceptoDemanda, pendienteOferta, interceptoOferta } = params;
|
||||
// Pd = Po => a + b*Q = c + d*Q
|
||||
const Q = (interceptoOferta - interceptoDemanda) / (pendienteDemanda - pendienteOferta);
|
||||
const P = interceptoDemanda + pendienteDemanda * Q;
|
||||
return { Q: Math.max(0, Q), P: Math.max(0, P) };
|
||||
};
|
||||
|
||||
const calcularCantidadEnPrecio = (precio: number, params: CurvaParams) => {
|
||||
// P = a + b*Q => Q = (P - a) / b
|
||||
const Qd = (precio - params.interceptoDemanda) / params.pendienteDemanda;
|
||||
const Qo = (precio - params.interceptoOferta) / params.pendienteOferta;
|
||||
return { Qd: Math.max(0, Qd), Qo: Math.max(0, Qo) };
|
||||
};
|
||||
|
||||
export const SimuladorPrecios: React.FC<SimuladorPreciosProps> = ({ onComplete, ejercicioId: _ejercicioId }) => {
|
||||
const [params, _setParams] = useState<CurvaParams>(DEFAULT_PARAMS);
|
||||
const [precioMaximo, setPrecioMaximo] = useState<number | null>(null);
|
||||
const [precioMinimo, setPrecioMinimo] = useState<number | null>(null);
|
||||
const [showInfo, setShowInfo] = useState(true);
|
||||
const [startTime] = useState(Date.now());
|
||||
const [hasInteracted, setHasInteracted] = useState(false);
|
||||
|
||||
const equilibrio = useMemo(() => calcularEquilibrio(params), [params]);
|
||||
|
||||
const analisis = useMemo(() => {
|
||||
if (precioMaximo !== null && precioMaximo < equilibrio.P) {
|
||||
const { Qd, Qo } = calcularCantidadEnPrecio(precioMaximo, params);
|
||||
const excesoDemanda = Qd - Qo;
|
||||
const cantidadTransada = Qo;
|
||||
|
||||
// Pérdida de peso muerto: área del triángulo
|
||||
const base = equilibrio.Q - cantidadTransada;
|
||||
const altura = precioMaximo - (params.interceptoOferta + params.pendienteOferta * cantidadTransada);
|
||||
const deadweightLoss = 0.5 * base * altura;
|
||||
|
||||
return {
|
||||
tipo: 'precio-maximo' as const,
|
||||
excesoDemanda: Math.max(0, excesoDemanda),
|
||||
excesoOferta: 0,
|
||||
cantidadTransada,
|
||||
deadweightLoss: Math.max(0, deadweightLoss),
|
||||
mensaje: 'Precio máximo crea escasez (exceso de demanda)'
|
||||
};
|
||||
}
|
||||
|
||||
if (precioMinimo !== null && precioMinimo > equilibrio.P) {
|
||||
const { Qd, Qo } = calcularCantidadEnPrecio(precioMinimo, params);
|
||||
const excesoOferta = Qo - Qd;
|
||||
const cantidadTransada = Qd;
|
||||
|
||||
const base = equilibrio.Q - cantidadTransada;
|
||||
const altura = (params.interceptoDemanda + params.pendienteDemanda * cantidadTransada) - precioMinimo;
|
||||
const deadweightLoss = 0.5 * base * altura;
|
||||
|
||||
return {
|
||||
tipo: 'precio-minimo' as const,
|
||||
excesoDemanda: 0,
|
||||
excesoOferta: Math.max(0, excesoOferta),
|
||||
cantidadTransada,
|
||||
deadweightLoss: Math.max(0, deadweightLoss),
|
||||
mensaje: 'Precio mínimo crea superávit (exceso de oferta)'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
tipo: 'equilibrio' as const,
|
||||
excesoDemanda: 0,
|
||||
excesoOferta: 0,
|
||||
cantidadTransada: equilibrio.Q,
|
||||
deadweightLoss: 0,
|
||||
mensaje: 'Mercado en equilibrio'
|
||||
};
|
||||
}, [precioMaximo, precioMinimo, equilibrio, params]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasInteracted && (precioMaximo !== null || precioMinimo !== null)) {
|
||||
const timer = setTimeout(() => {
|
||||
if (onComplete) {
|
||||
onComplete(100);
|
||||
}
|
||||
}, 5000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [hasInteracted, precioMaximo, precioMinimo, startTime, onComplete]);
|
||||
|
||||
const reset = () => {
|
||||
setPrecioMaximo(null);
|
||||
setPrecioMinimo(null);
|
||||
setHasInteracted(false);
|
||||
};
|
||||
|
||||
// Generar puntos para las curvas
|
||||
const generateCurvePoints = () => {
|
||||
const points = [];
|
||||
for (let Q = 0; Q <= 60; Q += 2) {
|
||||
const Pd = params.interceptoDemanda + params.pendienteDemanda * Q;
|
||||
const Po = params.interceptoOferta + params.pendienteOferta * Q;
|
||||
points.push({ Q, Pd: Math.max(0, Pd), Po: Math.max(0, Po) });
|
||||
}
|
||||
return points;
|
||||
};
|
||||
|
||||
const curvePoints = generateCurvePoints();
|
||||
|
||||
// Escalar para SVG
|
||||
const scaleX = (Q: number) => 50 + (Q / 60) * 400;
|
||||
const scaleY = (P: number) => 350 - (P / 100) * 300;
|
||||
|
||||
const demandaPath = curvePoints.map((p, i) =>
|
||||
`${i === 0 ? 'M' : 'L'} ${scaleX(p.Q)} ${scaleY(p.Pd)}`
|
||||
).join(' ');
|
||||
|
||||
const ofertaPath = curvePoints.map((p, i) =>
|
||||
`${i === 0 ? 'M' : 'L'} ${scaleX(p.Q)} ${scaleY(p.Po)}`
|
||||
).join(' ');
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-5xl mx-auto p-6 bg-white rounded-xl shadow-lg">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-2xl font-bold text-gray-800 flex items-center gap-3">
|
||||
<Calculator className="w-8 h-8 text-purple-600" />
|
||||
Simulador de Precios Intervenidos
|
||||
</h2>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Experimenta con precios máximos y mínimos para ver cómo afectan el equilibrio de mercado.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{showInfo && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg flex items-start gap-3"
|
||||
>
|
||||
<Info className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-blue-800 mb-1">Cómo usar:</h4>
|
||||
<ul className="text-sm text-blue-700 space-y-1">
|
||||
<li>• Ajusta los sliders para establecer un precio máximo o mínimo</li>
|
||||
<li>• Observa cómo cambian las cantidades demandadas y ofrecidas</li>
|
||||
<li>• Identifica escasez (exceso de demanda) o superávit (exceso de oferta)</li>
|
||||
<li>• La pérdida de peso muerto representa la ineficiencia creada</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowInfo(false)}
|
||||
className="text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="font-semibold text-gray-700">Gráfico de Mercado</h3>
|
||||
<button
|
||||
onClick={reset}
|
||||
className="p-2 text-gray-500 hover:text-purple-600 transition-colors"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<svg width="500" height="400" className="w-full">
|
||||
{/* Grid */}
|
||||
{Array.from({ length: 11 }).map((_, i) => (
|
||||
<g key={i}>
|
||||
<line
|
||||
x1={50 + (i * 400) / 10}
|
||||
y1={50}
|
||||
x2={50 + (i * 400) / 10}
|
||||
y2={350}
|
||||
stroke="#e5e7eb"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<line
|
||||
x1={50}
|
||||
y1={50 + (i * 300) / 10}
|
||||
x2={450}
|
||||
y2={50 + (i * 300) / 10}
|
||||
stroke="#e5e7eb"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
</g>
|
||||
))}
|
||||
|
||||
{/* Ejes */}
|
||||
<line x1="50" y1="350" x2="450" y2="350" stroke="#374151" strokeWidth="2" />
|
||||
<line x1="50" y1="50" x2="50" y2="350" stroke="#374151" strokeWidth="2" />
|
||||
|
||||
{/* Labels */}
|
||||
<text x="250" y="385" textAnchor="middle" className="text-sm fill-gray-600">Cantidad</text>
|
||||
<text x="15" y="200" textAnchor="middle" transform="rotate(-90, 15, 200)" className="text-sm fill-gray-600">Precio</text>
|
||||
|
||||
{/* Curva de Demanda */}
|
||||
<path d={demandaPath} fill="none" stroke="#3b82f6" strokeWidth="3" />
|
||||
<text x="420" y={scaleY(15)} className="text-sm fill-blue-600 font-medium">D</text>
|
||||
|
||||
{/* Curva de Oferta */}
|
||||
<path d={ofertaPath} fill="none" stroke="#22c55e" strokeWidth="3" />
|
||||
<text x="420" y={scaleY(80)} className="text-sm fill-green-600 font-medium">S</text>
|
||||
|
||||
{/* Punto de equilibrio */}
|
||||
<circle
|
||||
cx={scaleX(equilibrio.Q)}
|
||||
cy={scaleY(equilibrio.P)}
|
||||
r="6"
|
||||
fill="#8b5cf6"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<text x={scaleX(equilibrio.Q) + 10} y={scaleY(equilibrio.P)} className="text-xs fill-purple-600">
|
||||
E
|
||||
</text>
|
||||
|
||||
{/* Línea de precio máximo */}
|
||||
{precioMaximo !== null && (
|
||||
<g>
|
||||
<line
|
||||
x1="50"
|
||||
y1={scaleY(precioMaximo)}
|
||||
x2="450"
|
||||
y2={scaleY(precioMaximo)}
|
||||
stroke="#ef4444"
|
||||
strokeWidth="2"
|
||||
strokeDasharray="5,5"
|
||||
/>
|
||||
<text x="460" y={scaleY(precioMaximo)} className="text-xs fill-red-500">Pmáx</text>
|
||||
|
||||
{/* Zona de escasez */}
|
||||
{analisis.excesoDemanda > 0 && (
|
||||
<polygon
|
||||
points={`
|
||||
${scaleX(analisis.cantidadTransada)} ${scaleY(precioMaximo)}
|
||||
${scaleX(analisis.cantidadTransada + analisis.excesoDemanda)} ${scaleY(precioMaximo)}
|
||||
`}
|
||||
fill="#fef3c7"
|
||||
opacity="0.5"
|
||||
/>
|
||||
)}
|
||||
</g>
|
||||
)}
|
||||
|
||||
{/* Línea de precio mínimo */}
|
||||
{precioMinimo !== null && (
|
||||
<g>
|
||||
<line
|
||||
x1="50"
|
||||
y1={scaleY(precioMinimo)}
|
||||
x2="450"
|
||||
y2={scaleY(precioMinimo)}
|
||||
stroke="#f59e0b"
|
||||
strokeWidth="2"
|
||||
strokeDasharray="5,5"
|
||||
/>
|
||||
<text x="460" y={scaleY(precioMinimo)} className="text-xs fill-amber-500">Pmín</text>
|
||||
</g>
|
||||
)}
|
||||
|
||||
{/* Indicador de pérdida de peso muerto */}
|
||||
{analisis.deadweightLoss > 0 && (
|
||||
<motion.text
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
x={scaleX(equilibrio.Q / 2)}
|
||||
y={scaleY((equilibrio.P + (precioMaximo || precioMinimo || 0)) / 2)}
|
||||
textAnchor="middle"
|
||||
className="text-xs fill-red-600 font-medium"
|
||||
>
|
||||
Pérdida de peso muerto
|
||||
</motion.text>
|
||||
)}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<h3 className="font-semibold text-gray-700 mb-4">Controles de Precio</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Precio Máximo (Pmáx)
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={Math.round(equilibrio.P)}
|
||||
value={precioMaximo ?? Math.round(equilibrio.P)}
|
||||
onChange={(e) => {
|
||||
setPrecioMaximo(Number(e.target.value) || null);
|
||||
setPrecioMinimo(null);
|
||||
setHasInteracted(true);
|
||||
}}
|
||||
className="w-full accent-red-500"
|
||||
/>
|
||||
<div className="flex justify-between text-sm text-gray-500 mt-1">
|
||||
<span>$0</span>
|
||||
<span className="font-medium text-red-600">
|
||||
{precioMaximo !== null ? `$${precioMaximo}` : 'Desactivado'}
|
||||
</span>
|
||||
<span>${Math.round(equilibrio.P)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 pt-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Precio Mínimo (Pmín)
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min={Math.round(equilibrio.P)}
|
||||
max="100"
|
||||
value={precioMinimo ?? Math.round(equilibrio.P)}
|
||||
onChange={(e) => {
|
||||
setPrecioMinimo(Number(e.target.value) || null);
|
||||
setPrecioMaximo(null);
|
||||
setHasInteracted(true);
|
||||
}}
|
||||
className="w-full accent-amber-500"
|
||||
/>
|
||||
<div className="flex justify-between text-sm text-gray-500 mt-1">
|
||||
<span>${Math.round(equilibrio.P)}</span>
|
||||
<span className="font-medium text-amber-600">
|
||||
{precioMinimo !== null ? `$${precioMinimo}` : 'Desactivado'}
|
||||
</span>
|
||||
<span>$100</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={analisis.tipo}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
className={`p-4 rounded-lg border ${
|
||||
analisis.tipo === 'equilibrio'
|
||||
? 'bg-green-50 border-green-200'
|
||||
: analisis.tipo === 'precio-maximo'
|
||||
? 'bg-red-50 border-red-200'
|
||||
: 'bg-amber-50 border-amber-200'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
{analisis.tipo === 'equilibrio' ? (
|
||||
<TrendingUp className="w-5 h-5 text-green-600" />
|
||||
) : analisis.tipo === 'precio-maximo' ? (
|
||||
<TrendingDown className="w-5 h-5 text-red-600" />
|
||||
) : (
|
||||
<AlertTriangle className="w-5 h-5 text-amber-600" />
|
||||
)}
|
||||
<h4 className={`font-semibold ${
|
||||
analisis.tipo === 'equilibrio'
|
||||
? 'text-green-800'
|
||||
: analisis.tipo === 'precio-maximo'
|
||||
? 'text-red-800'
|
||||
: 'text-amber-800'
|
||||
}`}>
|
||||
{analisis.mensaje}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div className="bg-white rounded p-2">
|
||||
<span className="text-gray-500">Precio de equilibrio:</span>
|
||||
<p className="font-semibold text-gray-800">${equilibrio.P.toFixed(1)}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded p-2">
|
||||
<span className="text-gray-500">Cantidad de equilibrio:</span>
|
||||
<p className="font-semibold text-gray-800">{equilibrio.Q.toFixed(1)} unidades</p>
|
||||
</div>
|
||||
{precioMaximo !== null && (
|
||||
<div className="bg-white rounded p-2">
|
||||
<span className="text-red-500">Precio máximo:</span>
|
||||
<p className="font-semibold text-gray-800">${precioMaximo}</p>
|
||||
</div>
|
||||
)}
|
||||
{precioMinimo !== null && (
|
||||
<div className="bg-white rounded p-2">
|
||||
<span className="text-amber-500">Precio mínimo:</span>
|
||||
<p className="font-semibold text-gray-800">${precioMinimo}</p>
|
||||
</div>
|
||||
)}
|
||||
{analisis.excesoDemanda > 0 && (
|
||||
<div className="bg-red-100 rounded p-2 col-span-2">
|
||||
<span className="text-red-700">Exceso de demanda (escasez):</span>
|
||||
<p className="font-semibold text-red-800">{analisis.excesoDemanda.toFixed(1)} unidades</p>
|
||||
</div>
|
||||
)}
|
||||
{analisis.excesoOferta > 0 && (
|
||||
<div className="bg-amber-100 rounded p-2 col-span-2">
|
||||
<span className="text-amber-700">Exceso de oferta (superávit):</span>
|
||||
<p className="font-semibold text-amber-800">{analisis.excesoOferta.toFixed(1)} unidades</p>
|
||||
</div>
|
||||
)}
|
||||
{analisis.deadweightLoss > 0 && (
|
||||
<div className="bg-red-100 rounded p-2 col-span-2">
|
||||
<span className="text-red-700">Pérdida de peso muerto:</span>
|
||||
<p className="font-semibold text-red-800">${analisis.deadweightLoss.toFixed(1)}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
<div className="p-4 bg-purple-50 rounded-lg">
|
||||
<h4 className="font-semibold text-purple-800 mb-2">Resultado</h4>
|
||||
<p className="text-sm text-purple-700">
|
||||
Cantidad transada: <span className="font-bold">{analisis.cantidadTransada.toFixed(1)} unidades</span>
|
||||
</p>
|
||||
<p className="text-xs text-purple-600 mt-1">
|
||||
{analisis.tipo === 'precio-maximo'
|
||||
? 'Con precio máximo, los vendedores quieren vender menos cantidad.'
|
||||
: analisis.tipo === 'precio-minimo'
|
||||
? 'Con precio mínimo, los compradores quieren comprar menos cantidad.'
|
||||
: 'En equilibrio, la cantidad demandada = cantidad ofrecida.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SimuladorPrecios;
|
||||
3
frontend/src/components/exercises/modulo2/index.ts
Normal file
3
frontend/src/components/exercises/modulo2/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { ConstructorCurvas } from './ConstructorCurvas';
|
||||
export { SimuladorPrecios } from './SimuladorPrecios';
|
||||
export { IdentificarShocks } from './IdentificarShocks';
|
||||
@@ -0,0 +1,266 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Button } from '../../ui/Button';
|
||||
import { Input } from '../../ui/Input';
|
||||
import { Card, CardHeader } from '../../ui/Card';
|
||||
|
||||
type ElasticidadTipo = 'precio' | 'ingreso' | 'cruzada';
|
||||
|
||||
interface CalculadoraElasticidadProps {
|
||||
ejercicioId: string;
|
||||
onComplete?: (puntuacion: number) => void;
|
||||
}
|
||||
|
||||
export function CalculadoraElasticidad({ ejercicioId: _ejercicioId, onComplete }: CalculadoraElasticidadProps) {
|
||||
const [tipo, setTipo] = useState<ElasticidadTipo>('precio');
|
||||
const [p1, setP1] = useState('');
|
||||
const [p2, setP2] = useState('');
|
||||
const [q1, setQ1] = useState('');
|
||||
const [q2, setQ2] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const [resultado, setResultado] = useState<{
|
||||
deltaQ: number;
|
||||
deltaP: number;
|
||||
qPromedio: number;
|
||||
pPromedio: number;
|
||||
porcentajeQ: number;
|
||||
porcentajeP: number;
|
||||
elasticidad: number;
|
||||
interpretacion: string;
|
||||
} | null>(null);
|
||||
|
||||
const calcular = useCallback(() => {
|
||||
const numP1 = parseFloat(p1);
|
||||
const numP2 = parseFloat(p2);
|
||||
const numQ1 = parseFloat(q1);
|
||||
const numQ2 = parseFloat(q2);
|
||||
|
||||
if (isNaN(numP1) || isNaN(numP2) || isNaN(numQ1) || isNaN(numQ2)) {
|
||||
setError('Todos los valores deben ser numéricos');
|
||||
setResultado(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (numP1 === numP2 && tipo === 'precio') {
|
||||
setError('P1 y P2 no pueden ser iguales para calcular elasticidad');
|
||||
setResultado(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setError('');
|
||||
|
||||
// Método del punto medio
|
||||
const deltaQ = numQ2 - numQ1;
|
||||
const deltaP = numP2 - numP1;
|
||||
const qPromedio = (numQ1 + numQ2) / 2;
|
||||
const pPromedio = (numP1 + numP2) / 2;
|
||||
|
||||
const porcentajeQ = (deltaQ / qPromedio) * 100;
|
||||
const porcentajeP = (deltaP / pPromedio) * 100;
|
||||
const elasticidad = porcentajeQ / porcentajeP;
|
||||
|
||||
let interpretacion = '';
|
||||
const absE = Math.abs(elasticidad);
|
||||
|
||||
if (tipo === 'precio') {
|
||||
if (absE > 1) interpretacion = 'Demanda ELÁSTICA: |E| > 1. El consumo responde más que proporcionalmente al cambio de precio.';
|
||||
else if (absE < 1) interpretacion = 'Demanda INELÁSTICA: |E| < 1. El consumo responde menos que proporcionalmente al cambio de precio.';
|
||||
else interpretacion = 'Demanda UNITARIA: |E| = 1. El consumo responde exactamente proporcional al cambio de precio.';
|
||||
} else if (tipo === 'ingreso') {
|
||||
if (elasticidad > 1) interpretacion = 'Bien de LUJO: Ei > 1. El gasto en el bien aumenta más que proporcionalmente al ingreso.';
|
||||
else if (elasticidad > 0 && elasticidad < 1) interpretacion = 'Bien NECESARIO: 0 < Ei < 1. El gasto aumenta menos que proporcionalmente al ingreso.';
|
||||
else if (elasticidad < 0) interpretacion = 'Bien INFERIOR: Ei < 0. El consumo disminuye cuando aumenta el ingreso.';
|
||||
else interpretacion = 'Bien NEUTRO: Ei = 0. El consumo no cambia con el ingreso.';
|
||||
} else {
|
||||
if (elasticidad > 0) interpretacion = 'BIENES SUSTITUTOS: Ecr > 0. El aumento del precio de Y aumenta la demanda de X.';
|
||||
else if (elasticidad < 0) interpretacion = 'BIENES COMPLEMENTARIOS: Ecr < 0. El aumento del precio de Y disminuye la demanda de X.';
|
||||
else interpretacion = 'BIENES INDEPENDIENTES: Ecr = 0. No existe relación entre los bienes.';
|
||||
}
|
||||
|
||||
setResultado({
|
||||
deltaQ,
|
||||
deltaP,
|
||||
qPromedio,
|
||||
pPromedio,
|
||||
porcentajeQ,
|
||||
porcentajeP,
|
||||
elasticidad,
|
||||
interpretacion
|
||||
});
|
||||
|
||||
if (onComplete) {
|
||||
onComplete(100);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [p1, p2, q1, q2, tipo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (p1 && p2 && q1 && q2) {
|
||||
calcular();
|
||||
}
|
||||
}, [p1, p2, q1, q2, tipo, calcular]);
|
||||
|
||||
const getLabelPrecio = () => {
|
||||
if (tipo === 'cruzada') return 'P1/Py1 (Precio del otro bien)';
|
||||
return 'P1 (Precio inicial)';
|
||||
};
|
||||
|
||||
const getLabelCantidad = () => {
|
||||
if (tipo === 'ingreso') return 'Q1 (Cantidad con ingreso I1)';
|
||||
return 'Q1 (Cantidad inicial)';
|
||||
};
|
||||
|
||||
const tipoLabels: Record<ElasticidadTipo, string> = {
|
||||
precio: 'Elasticidad Precio de la Demanda',
|
||||
ingreso: 'Elasticidad Ingreso',
|
||||
cruzada: 'Elasticidad Cruzada'
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="max-w-2xl mx-auto">
|
||||
<CardHeader
|
||||
title="Calculadora de Elasticidad"
|
||||
subtitle="Método del punto medio (Arco)"
|
||||
/>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(['precio', 'ingreso', 'cruzada'] as ElasticidadTipo[]).map((t) => (
|
||||
<Button
|
||||
key={t}
|
||||
variant={tipo === t ? 'primary' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setTipo(t)}
|
||||
>
|
||||
{tipoLabels[t]}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<p className="text-sm text-blue-800 font-medium">{tipoLabels[tipo]}</p>
|
||||
<p className="text-xs text-blue-600 mt-1">
|
||||
{tipo === 'precio' && 'Mide la sensibilidad de la cantidad demandada ante cambios en el precio del propio bien'}
|
||||
{tipo === 'ingreso' && 'Mide la sensibilidad de la cantidad demandada ante cambios en el ingreso del consumidor'}
|
||||
{tipo === 'cruzada' && 'Mide la sensibilidad de la cantidad demandada de X ante cambios en el precio de Y'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Input
|
||||
label={getLabelCantidad()}
|
||||
type="number"
|
||||
value={q1}
|
||||
onChange={(e) => setQ1(e.target.value)}
|
||||
placeholder="Ej: 100"
|
||||
/>
|
||||
<Input
|
||||
label={tipo === 'ingreso' ? 'Q2 (Cantidad con ingreso I2)' : 'Q2 (Cantidad final)'}
|
||||
type="number"
|
||||
value={q2}
|
||||
onChange={(e) => setQ2(e.target.value)}
|
||||
placeholder="Ej: 80"
|
||||
/>
|
||||
<Input
|
||||
label={getLabelPrecio()}
|
||||
type="number"
|
||||
value={p1}
|
||||
onChange={(e) => setP1(e.target.value)}
|
||||
placeholder="Ej: 10"
|
||||
/>
|
||||
<Input
|
||||
label={tipo === 'cruzada' ? 'P2/Py2 (Precio final del otro bien)' : 'P2 (Precio final)'}
|
||||
type="number"
|
||||
value={p2}
|
||||
onChange={(e) => setP2(e.target.value)}
|
||||
placeholder="Ej: 12"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="text-red-500 text-sm bg-red-50 p-3 rounded-lg">{error}</p>
|
||||
)}
|
||||
|
||||
{resultado && (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-gray-50 p-4 rounded-lg space-y-3">
|
||||
<h4 className="font-semibold text-gray-700">Desarrollo paso a paso:</h4>
|
||||
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="bg-white p-3 rounded border">
|
||||
<p className="font-medium text-gray-600">Paso 1: Calcular cambios</p>
|
||||
<p className="font-mono text-gray-800">
|
||||
ΔQ = Q2 - Q1 = {q2} - {q1} = {resultado.deltaQ.toFixed(2)}
|
||||
</p>
|
||||
<p className="font-mono text-gray-800">
|
||||
ΔP = P2 - P1 = {p2} - {p1} = {resultado.deltaP.toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-3 rounded border">
|
||||
<p className="font-medium text-gray-600">Paso 2: Calcular promedios</p>
|
||||
<p className="font-mono text-gray-800">
|
||||
Q̄ = (Q1 + Q2) / 2 = ({q1} + {q2}) / 2 = {resultado.qPromedio.toFixed(2)}
|
||||
</p>
|
||||
<p className="font-mono text-gray-800">
|
||||
P̄ = (P1 + P2) / 2 = ({p1} + {p2}) / 2 = {resultado.pPromedio.toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-3 rounded border">
|
||||
<p className="font-medium text-gray-600">Paso 3: Calcular variaciones porcentuales</p>
|
||||
<p className="font-mono text-gray-800">
|
||||
%ΔQ = (ΔQ / Q̄) × 100 = ({resultado.deltaQ.toFixed(2)} / {resultado.qPromedio.toFixed(2)}) × 100 = {resultado.porcentajeQ.toFixed(2)}%
|
||||
</p>
|
||||
<p className="font-mono text-gray-800">
|
||||
%ΔP = (ΔP / P̄) × 100 = ({resultado.deltaP.toFixed(2)} / {resultado.pPromedio.toFixed(2)}) × 100 = {resultado.porcentajeP.toFixed(2)}%
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-100 p-3 rounded border border-blue-300">
|
||||
<p className="font-medium text-blue-800">Paso 4: Calcular elasticidad</p>
|
||||
<p className="font-mono text-blue-900 text-lg">
|
||||
E = %ΔQ / %ΔP = {resultado.porcentajeQ.toFixed(2)} / {resultado.porcentajeP.toFixed(2)} = <strong>{resultado.elasticidad.toFixed(4)}</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`p-4 rounded-lg ${
|
||||
tipo === 'precio'
|
||||
? (Math.abs(resultado.elasticidad) > 1
|
||||
? 'bg-green-100 border border-green-300'
|
||||
: Math.abs(resultado.elasticidad) < 1
|
||||
? 'bg-orange-100 border border-orange-300'
|
||||
: 'bg-blue-100 border border-blue-300')
|
||||
: tipo === 'ingreso'
|
||||
? (resultado.elasticidad > 1
|
||||
? 'bg-purple-100 border border-purple-300'
|
||||
: resultado.elasticidad > 0
|
||||
? 'bg-yellow-100 border border-yellow-300'
|
||||
: 'bg-red-100 border border-red-300')
|
||||
: (resultado.elasticidad > 0
|
||||
? 'bg-green-100 border border-green-300'
|
||||
: 'bg-red-100 border border-red-300')
|
||||
}`}>
|
||||
<p className="font-semibold text-gray-800">Interpretación:</p>
|
||||
<p className="text-gray-700 mt-1">{resultado.interpretacion}</p>
|
||||
{tipo === 'precio' && Math.abs(resultado.elasticidad) > 1 && (
|
||||
<p className="text-sm text-gray-600 mt-2">
|
||||
El ingreso total aumentará si se reduce el precio (efecto cantidad domina).
|
||||
</p>
|
||||
)}
|
||||
{tipo === 'precio' && Math.abs(resultado.elasticidad) < 1 && (
|
||||
<p className="text-sm text-gray-600 mt-2">
|
||||
El ingreso total aumentará si se aumenta el precio (efecto precio domina).
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default CalculadoraElasticidad;
|
||||
228
frontend/src/components/exercises/modulo3/ClasificadorBienes.tsx
Normal file
228
frontend/src/components/exercises/modulo3/ClasificadorBienes.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '../../ui/Button';
|
||||
import { Card, CardHeader } from '../../ui/Card';
|
||||
|
||||
interface Bien {
|
||||
id: string;
|
||||
nombre: string;
|
||||
descripcion: string;
|
||||
elasticidad: number;
|
||||
categoriaCorrecta: Categoria;
|
||||
}
|
||||
|
||||
type Categoria = 'lujo' | 'necesario' | 'inferior';
|
||||
|
||||
interface ClasificadorBienesProps {
|
||||
ejercicioId: string;
|
||||
onComplete?: (puntuacion: number) => void;
|
||||
}
|
||||
|
||||
const bienes: Bien[] = [
|
||||
{ id: '1', nombre: 'Caviar', descripcion: 'Alimento de lujo', elasticidad: 3.5, categoriaCorrecta: 'lujo' },
|
||||
{ id: '2', nombre: 'Arroz', descripcion: 'Grano básico de consumo', elasticidad: 0.3, categoriaCorrecta: 'necesario' },
|
||||
{ id: '3', nombre: 'Viajes en primera clase', descripcion: 'Transporte de lujo', elasticidad: 2.8, categoriaCorrecta: 'lujo' },
|
||||
{ id: '4', nombre: 'Pasta de dientes', descripcion: 'Higiene personal básica', elasticidad: 0.15, categoriaCorrecta: 'necesario' },
|
||||
{ id: '5', nombre: 'Autobuses', descripcion: 'Transporte público urbano', elasticidad: -0.5, categoriaCorrecta: 'inferior' },
|
||||
{ id: '6', nombre: 'Frijoles', descripcion: 'Proteína básica', elasticidad: 0.4, categoriaCorrecta: 'necesario' },
|
||||
{ id: '7', nombre: 'Yates privados', descripcion: 'Embarcaciones recreativas', elasticidad: 4.2, categoriaCorrecta: 'lujo' },
|
||||
{ id: '8', nombre: 'Pan de bagazo', descripcion: 'Pan económico de baja calidad', elasticidad: -0.8, categoriaCorrecta: 'inferior' },
|
||||
{ id: '9', nombre: 'Sal', descripcion: 'Condimento esencial', elasticidad: 0.05, categoriaCorrecta: 'necesario' },
|
||||
{ id: '10', nombre: 'Joyería fina', descripcion: 'Accesorios de oro/plata', elasticidad: 2.2, categoriaCorrecta: 'lujo' },
|
||||
{ id: '11', nombre: 'Comida rápida barata', descripcion: 'Hamburguesas de bajo costo', elasticidad: -0.3, categoriaCorrecta: 'inferior' },
|
||||
{ id: '12', nombre: 'Medicinas genéricas', descripcion: 'Productos farmacéuticos básicos', elasticidad: 0.2, categoriaCorrecta: 'necesario' }
|
||||
];
|
||||
|
||||
const categorias: { id: Categoria; nombre: string; descripcion: string; rango: string; color: string }[] = [
|
||||
{
|
||||
id: 'lujo',
|
||||
nombre: 'Bienes de Lujo',
|
||||
descripcion: 'El gasto aumenta más que proporcionalmente al ingreso',
|
||||
rango: 'Ei > 1',
|
||||
color: 'bg-purple-100 border-purple-300 text-purple-800'
|
||||
},
|
||||
{
|
||||
id: 'necesario',
|
||||
nombre: 'Bienes Necesarios',
|
||||
descripcion: 'El gasto aumenta menos que proporcionalmente al ingreso',
|
||||
rango: '0 < Ei < 1',
|
||||
color: 'bg-blue-100 border-blue-300 text-blue-800'
|
||||
},
|
||||
{
|
||||
id: 'inferior',
|
||||
nombre: 'Bienes Inferiores',
|
||||
descripcion: 'El consumo disminuye cuando aumenta el ingreso',
|
||||
rango: 'Ei < 0',
|
||||
color: 'bg-red-100 border-red-300 text-red-800'
|
||||
}
|
||||
];
|
||||
|
||||
export function ClasificadorBienes({ ejercicioId: _ejercicioId, onComplete }: ClasificadorBienesProps) {
|
||||
const [clasificaciones, setClasificaciones] = useState<Record<string, Categoria | null>>({});
|
||||
const [mostrarResultados, setMostrarResultados] = useState(false);
|
||||
const [bienesActuales] = useState(() =>
|
||||
[...bienes].sort(() => Math.random() - 0.5).slice(0, 8)
|
||||
);
|
||||
|
||||
const seleccionarCategoria = (bienId: string, categoria: Categoria) => {
|
||||
setClasificaciones(prev => ({
|
||||
...prev,
|
||||
[bienId]: categoria
|
||||
}));
|
||||
};
|
||||
|
||||
const verificarResultados = () => {
|
||||
setMostrarResultados(true);
|
||||
|
||||
const correctas = bienesActuales.filter(
|
||||
bien => clasificaciones[bien.id] === bien.categoriaCorrecta
|
||||
).length;
|
||||
|
||||
const score = Math.round((correctas / bienesActuales.length) * 100);
|
||||
|
||||
if (onComplete) {
|
||||
onComplete(score);
|
||||
}
|
||||
|
||||
return score;
|
||||
};
|
||||
|
||||
const reiniciar = () => {
|
||||
setClasificaciones({});
|
||||
setMostrarResultados(false);
|
||||
};
|
||||
|
||||
const getEstadoBien = (bien: Bien) => {
|
||||
if (!mostrarResultados || !clasificaciones[bien.id]) {
|
||||
return 'bg-white border-gray-200';
|
||||
}
|
||||
|
||||
const esCorrecto = clasificaciones[bien.id] === bien.categoriaCorrecta;
|
||||
return esCorrecto
|
||||
? 'bg-green-50 border-green-400'
|
||||
: 'bg-red-50 border-red-400';
|
||||
};
|
||||
|
||||
const getIconoEstado = (bien: Bien) => {
|
||||
if (!mostrarResultados || !clasificaciones[bien.id]) return null;
|
||||
|
||||
const esCorrecto = clasificaciones[bien.id] === bien.categoriaCorrecta;
|
||||
return esCorrecto ? (
|
||||
<span className="text-green-600 text-xl">✓</span>
|
||||
) : (
|
||||
<span className="text-red-600 text-xl">✗</span>
|
||||
);
|
||||
};
|
||||
|
||||
const puntuacion = mostrarResultados
|
||||
? bienesActuales.filter(bien => clasificaciones[bien.id] === bien.categoriaCorrecta).length
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<Card className="max-w-4xl mx-auto">
|
||||
<CardHeader
|
||||
title="Clasificador de Bienes"
|
||||
subtitle="Clasifica cada bien según su elasticidad ingreso"
|
||||
/>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
{categorias.map((cat) => (
|
||||
<div key={cat.id} className={`p-4 rounded-lg border-2 ${cat.color}`}>
|
||||
<h4 className="font-bold text-lg">{cat.nombre}</h4>
|
||||
<p className="text-sm font-mono font-semibold">{cat.rango}</p>
|
||||
<p className="text-xs mt-1 opacity-80">{cat.descripcion}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{bienesActuales.map((bien) => (
|
||||
<div
|
||||
key={bien.id}
|
||||
className={`p-4 rounded-lg border-2 transition-all ${getEstadoBien(bien)}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-semibold text-gray-800">{bien.nombre}</h4>
|
||||
{getIconoEstado(bien)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">{bien.descripcion}</p>
|
||||
{mostrarResultados && (
|
||||
<p className="text-xs font-mono mt-1 text-gray-600">
|
||||
Ei = {bien.elasticidad} → {categorias.find(c => c.id === bien.categoriaCorrecta)?.nombre}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{categorias.map((cat) => (
|
||||
<button
|
||||
key={cat.id}
|
||||
onClick={() => !mostrarResultados && seleccionarCategoria(bien.id, cat.id)}
|
||||
disabled={mostrarResultados}
|
||||
className={`px-3 py-2 rounded text-xs font-medium transition-all ${
|
||||
clasificaciones[bien.id] === cat.id
|
||||
? cat.color + ' border-2'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 border-2 border-transparent'
|
||||
} ${mostrarResultados ? 'cursor-default' : 'cursor-pointer'}`}
|
||||
>
|
||||
{cat.nombre.split(' ')[1]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-4 border-t">
|
||||
<div className="text-sm text-gray-600">
|
||||
Progreso: {Object.keys(clasificaciones).length} / {bienesActuales.length}
|
||||
</div>
|
||||
|
||||
{!mostrarResultados ? (
|
||||
<Button
|
||||
onClick={verificarResultados}
|
||||
disabled={Object.keys(clasificaciones).length < bienesActuales.length}
|
||||
>
|
||||
Verificar Resultados
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-gray-600">Puntuación</p>
|
||||
<p className="text-2xl font-bold text-gray-800">
|
||||
{puntuacion} / {bienesActuales.length}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={reiniciar}>
|
||||
Reiniciar
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{mostrarResultados && (
|
||||
<div className={`p-4 rounded-lg ${
|
||||
puntuacion === bienesActuales.length
|
||||
? 'bg-green-100 border border-green-300'
|
||||
: puntuacion >= bienesActuales.length / 2
|
||||
? 'bg-yellow-100 border border-yellow-300'
|
||||
: 'bg-red-100 border border-red-300'
|
||||
}`}>
|
||||
<p className="font-semibold text-gray-800">
|
||||
{puntuacion === bienesActuales.length
|
||||
? '¡Perfecto! Has clasificado todos los bienes correctamente.'
|
||||
: puntuacion >= bienesActuales.length / 2
|
||||
? '¡Buen trabajo! Sigue practicando para mejorar.'
|
||||
: 'Necesitas más práctica. Revisa las categorías y vuelve a intentar.'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClasificadorBienes;
|
||||
404
frontend/src/components/exercises/modulo3/EjerciciosExamen.tsx
Normal file
404
frontend/src/components/exercises/modulo3/EjerciciosExamen.tsx
Normal file
@@ -0,0 +1,404 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '../../ui/Button';
|
||||
import { Input } from '../../ui/Input';
|
||||
import { Card, CardHeader } from '../../ui/Card';
|
||||
|
||||
interface Problema {
|
||||
id: number;
|
||||
titulo: string;
|
||||
descripcion: string;
|
||||
datos: { etiqueta: string; valor: string }[];
|
||||
preguntas: {
|
||||
id: string;
|
||||
texto: string;
|
||||
tipo: 'numero' | 'seleccion';
|
||||
opciones?: string[];
|
||||
respuestaCorrecta: number | string;
|
||||
tolerancia?: number;
|
||||
solucion: string[];
|
||||
}[];
|
||||
}
|
||||
|
||||
const problemas: Problema[] = [
|
||||
{
|
||||
id: 1,
|
||||
titulo: "Elasticidad Precio de la Demanda",
|
||||
descripcion: "Una tienda de electrónica observa que cuando el precio de un modelo de laptop aumenta de $800 a $900, la cantidad demandada disminuye de 500 a 400 unidades por mes.",
|
||||
datos: [
|
||||
{ etiqueta: "P1", valor: "$800" },
|
||||
{ etiqueta: "P2", valor: "$900" },
|
||||
{ etiqueta: "Q1", valor: "500 unidades" },
|
||||
{ etiqueta: "Q2", valor: "400 unidades" }
|
||||
],
|
||||
preguntas: [
|
||||
{
|
||||
id: "p1_a",
|
||||
texto: "Calcule la elasticidad precio de la demanda usando el método del punto medio.",
|
||||
tipo: "numero",
|
||||
respuestaCorrecta: -1.89,
|
||||
tolerancia: 0.05,
|
||||
solucion: [
|
||||
"Paso 1: ΔQ = 400 - 500 = -100",
|
||||
"Paso 2: ΔP = 900 - 800 = 100",
|
||||
"Paso 3: Q̄ = (500 + 400) / 2 = 450",
|
||||
"Paso 4: P̄ = (800 + 900) / 2 = 850",
|
||||
"Paso 5: %ΔQ = (-100 / 450) × 100 = -22.22%",
|
||||
"Paso 6: %ΔP = (100 / 850) × 100 = 11.76%",
|
||||
"Paso 7: Ed = -22.22% / 11.76% = -1.89"
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "p1_b",
|
||||
texto: "¿Qué tipo de demanda presenta este producto?",
|
||||
tipo: "seleccion",
|
||||
opciones: [
|
||||
"Elástica (|Ed| > 1)",
|
||||
"Inelástica (|Ed| < 1)",
|
||||
"Unitaria (|Ed| = 1)"
|
||||
],
|
||||
respuestaCorrecta: "Elástica (|Ed| > 1)",
|
||||
solucion: [
|
||||
"Como |Ed| = 1.89 > 1, la demanda es ELÁSTICA.",
|
||||
"Esto significa que los consumidores son sensibles al cambio de precio.",
|
||||
"Un aumento de precio del 1% reduce la cantidad demandada en aproximadamente 1.89%"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
titulo: "Elasticidad Ingreso",
|
||||
descripcion: "En una economía, cuando el ingreso promedio de los hogares aumenta de $2,000 a $2,500 mensuales, el consumo de restaurantes de alta categoría aumenta de 2 a 4 visitas mensuales por hogar.",
|
||||
datos: [
|
||||
{ etiqueta: "I1", valor: "$2,000" },
|
||||
{ etiqueta: "I2", valor: "$2,500" },
|
||||
{ etiqueta: "Q1", valor: "2 visitas" },
|
||||
{ etiqueta: "Q2", valor: "4 visitas" }
|
||||
],
|
||||
preguntas: [
|
||||
{
|
||||
id: "p2_a",
|
||||
texto: "Calcule la elasticidad ingreso.",
|
||||
tipo: "numero",
|
||||
respuestaCorrecta: 2.33,
|
||||
tolerancia: 0.05,
|
||||
solucion: [
|
||||
"Paso 1: ΔQ = 4 - 2 = 2",
|
||||
"Paso 2: ΔI = 2500 - 2000 = 500",
|
||||
"Paso 3: Q̄ = (2 + 4) / 2 = 3",
|
||||
"Paso 4: Ī = (2000 + 2500) / 2 = 2250",
|
||||
"Paso 5: %ΔQ = (2 / 3) × 100 = 66.67%",
|
||||
"Paso 6: %ΔI = (500 / 2250) × 100 = 22.22%",
|
||||
"Paso 7: Ei = 66.67% / 22.22% = 3.00"
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "p2_b",
|
||||
texto: "¿Qué tipo de bien representa los restaurantes de alta categoría?",
|
||||
tipo: "seleccion",
|
||||
opciones: [
|
||||
"Bien necesario (0 < Ei < 1)",
|
||||
"Bien de lujo (Ei > 1)",
|
||||
"Bien inferior (Ei < 0)"
|
||||
],
|
||||
respuestaCorrecta: "Bien de lujo (Ei > 1)",
|
||||
solucion: [
|
||||
"Como Ei = 3.00 > 1, se trata de un BIEN DE LUJO.",
|
||||
"El gasto en este bien aumenta más que proporcionalmente al ingreso.",
|
||||
"Cuando el ingreso crece 10%, el consumo de restaurantes crece 30%"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
titulo: "Elasticidad Cruzada",
|
||||
descripcion: "Cuando el precio del café aumenta de $4 a $6 por libra, la cantidad demandada de té aumenta de 100 a 150 libras mensuales en el mismo mercado.",
|
||||
datos: [
|
||||
{ etiqueta: "Pcafé1", valor: "$4/libra" },
|
||||
{ etiqueta: "Pcafé2", valor: "$6/libra" },
|
||||
{ etiqueta: "Qté1", valor: "100 libras" },
|
||||
{ etiqueta: "Qté2", valor: "150 libras" }
|
||||
],
|
||||
preguntas: [
|
||||
{
|
||||
id: "p3_a",
|
||||
texto: "Calcule la elasticidad cruzada entre café y té.",
|
||||
tipo: "numero",
|
||||
respuestaCorrecta: 1.0,
|
||||
tolerancia: 0.05,
|
||||
solucion: [
|
||||
"Paso 1: ΔQté = 150 - 100 = 50",
|
||||
"Paso 2: ΔPcafé = 6 - 4 = 2",
|
||||
"Paso 3: Qté̄ = (100 + 150) / 2 = 125",
|
||||
"Paso 4: Pcafé̄ = (4 + 6) / 2 = 5",
|
||||
"Paso 5: %ΔQté = (50 / 125) × 100 = 40%",
|
||||
"Paso 6: %ΔPcafé = (2 / 5) × 100 = 40%",
|
||||
"Paso 7: Ecr = 40% / 40% = 1.0"
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "p3_b",
|
||||
texto: "¿Qué relación existe entre café y té?",
|
||||
tipo: "seleccion",
|
||||
opciones: [
|
||||
"Son bienes sustitutos (Ecr > 0)",
|
||||
"Son bienes complementarios (Ecr < 0)",
|
||||
"Son bienes independientes (Ecr = 0)"
|
||||
],
|
||||
respuestaCorrecta: "Son bienes sustitutos (Ecr > 0)",
|
||||
solucion: [
|
||||
"Como Ecr = 1.0 > 0, café y té son BIENES SUSTITUTOS.",
|
||||
"Cuando sube el precio del café, los consumidores compran más té.",
|
||||
"Los consumidores pueden sustituir uno por otro según los precios"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
interface Respuesta {
|
||||
valor: string;
|
||||
esCorrecta: boolean | null;
|
||||
}
|
||||
|
||||
interface EjerciciosExamenProps {
|
||||
ejercicioId: string;
|
||||
onComplete?: (puntuacion: number) => void;
|
||||
}
|
||||
|
||||
export function EjerciciosExamen({ ejercicioId: _ejercicioId, onComplete }: EjerciciosExamenProps) {
|
||||
const [respuestas, setRespuestas] = useState<Record<string, Respuesta>>({});
|
||||
const [mostrarSolucion, setMostrarSolucion] = useState<Record<number, boolean>>({});
|
||||
const [problemaActual, setProblemaActual] = useState(0);
|
||||
|
||||
const handleRespuesta = (preguntaId: string, valor: string) => {
|
||||
setRespuestas(prev => ({
|
||||
...prev,
|
||||
[preguntaId]: { valor, esCorrecta: null }
|
||||
}));
|
||||
};
|
||||
|
||||
const verificarRespuesta = (pregunta: Problema['preguntas'][0]) => {
|
||||
const respuesta = respuestas[pregunta.id];
|
||||
if (!respuesta) return;
|
||||
|
||||
let esCorrecta = false;
|
||||
|
||||
if (pregunta.tipo === 'numero') {
|
||||
const valorNum = parseFloat(respuesta.valor);
|
||||
const tolerancia = pregunta.tolerancia || 0.05;
|
||||
esCorrecta = Math.abs(valorNum - (pregunta.respuestaCorrecta as number)) <= tolerancia;
|
||||
} else {
|
||||
esCorrecta = respuesta.valor === pregunta.respuestaCorrecta;
|
||||
}
|
||||
|
||||
setRespuestas(prev => ({
|
||||
...prev,
|
||||
[pregunta.id]: { ...respuesta, esCorrecta }
|
||||
}));
|
||||
};
|
||||
|
||||
const toggleSolucion = (problemaId: number) => {
|
||||
setMostrarSolucion(prev => ({
|
||||
...prev,
|
||||
[problemaId]: !prev[problemaId]
|
||||
}));
|
||||
};
|
||||
|
||||
const calcularPuntuacion = () => {
|
||||
let correctas = 0;
|
||||
let total = 0;
|
||||
|
||||
problemas.forEach(problema => {
|
||||
problema.preguntas.forEach(pregunta => {
|
||||
total++;
|
||||
if (respuestas[pregunta.id]?.esCorrecta) {
|
||||
correctas++;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return Math.round((correctas / total) * 100);
|
||||
};
|
||||
|
||||
const finalizarExamen = () => {
|
||||
const score = calcularPuntuacion();
|
||||
if (onComplete) {
|
||||
onComplete(score);
|
||||
}
|
||||
return score;
|
||||
};
|
||||
|
||||
const problema = problemas[problemaActual];
|
||||
const progreso = ((problemaActual + 1) / problemas.length) * 100;
|
||||
|
||||
return (
|
||||
<Card className="max-w-3xl mx-auto">
|
||||
<CardHeader
|
||||
title="Ejercicios Tipo Examen"
|
||||
subtitle={`Problema ${problemaActual + 1} de ${problemas.length}`}
|
||||
/>
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-primary h-2 rounded-full transition-all"
|
||||
style={{ width: `${progreso}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<h3 className="font-bold text-lg text-blue-900">{problema.titulo}</h3>
|
||||
<p className="text-gray-700 mt-2">{problema.descripcion}</p>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mt-4">
|
||||
{problema.datos.map((dato, idx) => (
|
||||
<div key={idx} className="bg-white p-2 rounded text-center">
|
||||
<span className="font-mono text-sm font-bold">{dato.etiqueta}</span>
|
||||
<p className="text-xs text-gray-600">{dato.valor}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{problema.preguntas.map((pregunta, idx) => {
|
||||
const respuesta = respuestas[pregunta.id];
|
||||
const estado = respuesta?.esCorrecta;
|
||||
|
||||
return (
|
||||
<div key={pregunta.id} className="border rounded-lg p-4">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="bg-primary text-white rounded-full w-6 h-6 flex items-center justify-center text-sm flex-shrink-0">
|
||||
{idx + 1}
|
||||
</span>
|
||||
<p className="text-gray-800 font-medium">{pregunta.texto}</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 ml-8">
|
||||
{pregunta.tipo === 'numero' ? (
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={respuesta?.valor || ''}
|
||||
onChange={(e) => handleRespuesta(pregunta.id, e.target.value)}
|
||||
className="w-48"
|
||||
placeholder="Respuesta numérica"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => verificarRespuesta(pregunta)}
|
||||
disabled={!respuesta?.valor}
|
||||
>
|
||||
Verificar
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{pregunta.opciones?.map((opcion) => (
|
||||
<label
|
||||
key={opcion}
|
||||
className={`flex items-center gap-2 p-2 rounded cursor-pointer transition-colors ${
|
||||
respuesta?.valor === opcion
|
||||
? 'bg-blue-100 border-blue-300 border'
|
||||
: 'hover:bg-gray-50 border border-transparent'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name={pregunta.id}
|
||||
value={opcion}
|
||||
checked={respuesta?.valor === opcion}
|
||||
onChange={(e) => handleRespuesta(pregunta.id, e.target.value)}
|
||||
className="text-primary"
|
||||
/>
|
||||
<span className="text-sm">{opcion}</span>
|
||||
</label>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => verificarRespuesta(pregunta)}
|
||||
disabled={!respuesta?.valor}
|
||||
>
|
||||
Verificar
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{estado !== null && (
|
||||
<div className={`mt-3 p-3 rounded ${
|
||||
estado
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{estado ? '¡Correcto!' : 'Incorrecto. Intenta de nuevo.'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => toggleSolucion(parseInt(pregunta.id.split('_')[0]))}
|
||||
className="mt-2"
|
||||
>
|
||||
{mostrarSolucion[parseInt(pregunta.id.split('_')[0])] ? 'Ocultar' : 'Ver'} solución
|
||||
</Button>
|
||||
|
||||
{mostrarSolucion[parseInt(pregunta.id.split('_')[0])] && (
|
||||
<div className="mt-2 bg-gray-50 p-3 rounded text-sm">
|
||||
<p className="font-semibold mb-2">Solución paso a paso:</p>
|
||||
<ul className="space-y-1 text-gray-700">
|
||||
{pregunta.solucion.map((paso, i) => (
|
||||
<li key={i} className="font-mono text-xs">{paso}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setProblemaActual(Math.max(0, problemaActual - 1))}
|
||||
disabled={problemaActual === 0}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
|
||||
{problemaActual < problemas.length - 1 ? (
|
||||
<Button
|
||||
onClick={() => setProblemaActual(problemaActual + 1)}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={finalizarExamen}
|
||||
variant="primary"
|
||||
>
|
||||
Finalizar Examen
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{problemaActual === problemas.length - 1 && (
|
||||
<div className="bg-gray-50 p-4 rounded-lg text-center">
|
||||
<p className="text-gray-600">Puntuación actual: <strong>{calcularPuntuacion()}%</strong></p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default EjerciciosExamen;
|
||||
3
frontend/src/components/exercises/modulo3/index.ts
Normal file
3
frontend/src/components/exercises/modulo3/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { CalculadoraElasticidad } from './CalculadoraElasticidad';
|
||||
export { ClasificadorBienes } from './ClasificadorBienes';
|
||||
export { EjerciciosExamen } from './EjerciciosExamen';
|
||||
328
frontend/src/components/exercises/modulo4/CalculadoraCostos.tsx
Normal file
328
frontend/src/components/exercises/modulo4/CalculadoraCostos.tsx
Normal file
@@ -0,0 +1,328 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Card, CardHeader } from '../../ui/Card';
|
||||
import { Button } from '../../ui/Button';
|
||||
import { CheckCircle, RotateCcw, Calculator } from 'lucide-react';
|
||||
|
||||
interface FilaCostos {
|
||||
q: number;
|
||||
cv: number;
|
||||
}
|
||||
|
||||
interface FilaCalculada extends FilaCostos {
|
||||
cf: number;
|
||||
ct: number;
|
||||
cfme: number;
|
||||
cvme: number;
|
||||
cme: number;
|
||||
cmg: number | null;
|
||||
}
|
||||
|
||||
interface CalculadoraCostosProps {
|
||||
ejercicioId: string;
|
||||
onComplete?: (puntuacion: number) => void;
|
||||
}
|
||||
|
||||
export function CalculadoraCostos({ ejercicioId: _ejercicioId, onComplete }: CalculadoraCostosProps) {
|
||||
const CF_BASE = 200;
|
||||
|
||||
const [filas, setFilas] = useState<FilaCostos[]>([
|
||||
{ q: 0, cv: 0 },
|
||||
{ q: 1, cv: 50 },
|
||||
{ q: 2, cv: 90 },
|
||||
{ q: 3, cv: 120 },
|
||||
{ q: 4, cv: 160 },
|
||||
{ q: 5, cv: 220 },
|
||||
{ q: 6, cv: 300 },
|
||||
{ q: 7, cv: 400 },
|
||||
{ q: 8, cv: 520 },
|
||||
]);
|
||||
|
||||
const [validado, setValidado] = useState(false);
|
||||
const [errores, setErrores] = useState<string[]>([]);
|
||||
|
||||
const datosCalculados: FilaCalculada[] = useMemo(() => {
|
||||
return filas.map((fila, index) => {
|
||||
const ct = CF_BASE + fila.cv;
|
||||
const cfme = fila.q > 0 ? CF_BASE / fila.q : 0;
|
||||
const cvme = fila.q > 0 ? fila.cv / fila.q : 0;
|
||||
const cme = fila.q > 0 ? ct / fila.q : 0;
|
||||
const cmg = index > 0 ? ct - (CF_BASE + filas[index - 1].cv) : null;
|
||||
|
||||
return {
|
||||
...fila,
|
||||
cf: CF_BASE,
|
||||
ct,
|
||||
cfme,
|
||||
cvme,
|
||||
cme,
|
||||
cmg,
|
||||
};
|
||||
});
|
||||
}, [filas]);
|
||||
|
||||
const handleCvChange = (index: number, valor: string) => {
|
||||
const numValor = parseFloat(valor) || 0;
|
||||
const nuevasFilas = [...filas];
|
||||
nuevasFilas[index] = { ...nuevasFilas[index], cv: numValor };
|
||||
setFilas(nuevasFilas);
|
||||
setValidado(false);
|
||||
};
|
||||
|
||||
const validarCalculos = () => {
|
||||
const nuevosErrores: string[] = [];
|
||||
|
||||
datosCalculados.forEach((fila, index) => {
|
||||
if (fila.ct !== fila.cf + fila.cv) {
|
||||
nuevosErrores.push(`Fila ${index + 1}: CT no coincide con CF + CV`);
|
||||
}
|
||||
if (fila.q > 0 && Math.abs(fila.cme - fila.ct / fila.q) > 0.01) {
|
||||
nuevosErrores.push(`Fila ${index + 1}: CMe calculado incorrectamente`);
|
||||
}
|
||||
});
|
||||
|
||||
setErrores(nuevosErrores);
|
||||
setValidado(true);
|
||||
|
||||
if (nuevosErrores.length === 0) {
|
||||
if (onComplete) {
|
||||
onComplete(100);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const reiniciar = () => {
|
||||
setFilas([
|
||||
{ q: 0, cv: 0 },
|
||||
{ q: 1, cv: 50 },
|
||||
{ q: 2, cv: 90 },
|
||||
{ q: 3, cv: 120 },
|
||||
{ q: 4, cv: 160 },
|
||||
{ q: 5, cv: 220 },
|
||||
{ q: 6, cv: 300 },
|
||||
{ q: 7, cv: 400 },
|
||||
{ q: 8, cv: 520 },
|
||||
]);
|
||||
setValidado(false);
|
||||
setErrores([]);
|
||||
};
|
||||
|
||||
const maxCT = Math.max(...datosCalculados.map(d => d.ct));
|
||||
const maxCMe = Math.max(...datosCalculados.filter(d => d.q > 0).map(d => d.cme));
|
||||
const maxCMg = Math.max(...datosCalculados.filter(d => d.cmg !== null).map(d => d.cmg || 0));
|
||||
const escalaCT = maxCT > 0 ? 150 / maxCT : 1;
|
||||
const escalaCMe = maxCMe > 0 ? 150 / maxCMe : 1;
|
||||
const escalaCMg = maxCMg > 0 ? 150 / maxCMg : 1;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Calculadora de Costos"
|
||||
subtitle="Ingresa los Costos Variables (CV) y observa los cálculos automáticos"
|
||||
/>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-gray-50 border-b">
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-700">Q</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-700">CF</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-700 bg-blue-50">CV</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-700">CT</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-700">CFMe</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-700">CVMe</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-700">CMe</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-700">CMg</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{datosCalculados.map((fila, index) => (
|
||||
<tr key={index} className="border-b hover:bg-gray-50">
|
||||
<td className="px-3 py-2 font-medium">{fila.q}</td>
|
||||
<td className="px-3 py-2 text-gray-600">{fila.cf}</td>
|
||||
<td className="px-3 py-2 bg-blue-50">
|
||||
<input
|
||||
type="number"
|
||||
value={fila.cv}
|
||||
onChange={(e) => handleCvChange(index, e.target.value)}
|
||||
className="w-20 px-2 py-1 border rounded text-sm focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
min="0"
|
||||
disabled={fila.q === 0}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-3 py-2 font-medium text-primary">{fila.ct}</td>
|
||||
<td className="px-3 py-2 text-gray-600">
|
||||
{fila.q > 0 ? fila.cfme.toFixed(2) : '-'}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-gray-600">
|
||||
{fila.q > 0 ? fila.cvme.toFixed(2) : '-'}
|
||||
</td>
|
||||
<td className="px-3 py-2 font-medium text-secondary">
|
||||
{fila.q > 0 ? fila.cme.toFixed(2) : '-'}
|
||||
</td>
|
||||
<td className="px-3 py-2 font-medium text-success">
|
||||
{fila.cmg !== null ? fila.cmg : '-'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex gap-3">
|
||||
<Button onClick={validarCalculos} variant="primary">
|
||||
<Calculator className="w-4 h-4 mr-2" />
|
||||
Validar Cálculos
|
||||
</Button>
|
||||
<Button onClick={reiniciar} variant="outline">
|
||||
<RotateCcw className="w-4 h-4 mr-2" />
|
||||
Reiniciar
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{validado && errores.length === 0 && (
|
||||
<div className="mt-4 p-4 bg-success/10 border border-success rounded-lg">
|
||||
<div className="flex items-center gap-2 text-success">
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
<span className="font-medium">¡Todos los cálculos son correctos!</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{validado && errores.length > 0 && (
|
||||
<div className="mt-4 p-4 bg-error/10 border border-error rounded-lg">
|
||||
<p className="font-medium text-error mb-2">Se encontraron errores:</p>
|
||||
<ul className="list-disc list-inside text-sm text-error">
|
||||
{errores.map((error, i) => (
|
||||
<li key={i}>{error}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Visualización de Curvas de Costos"
|
||||
subtitle="Gráfico de CT, CMe y CMg"
|
||||
/>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-2">Costo Total (CT)</h4>
|
||||
<div className="h-40 bg-gray-50 rounded-lg p-4 relative">
|
||||
<svg className="w-full h-full" viewBox="0 0 400 160">
|
||||
<line x1="30" y1="140" x2="380" y2="140" stroke="#374151" strokeWidth="1" />
|
||||
<line x1="30" y1="140" x2="30" y2="10" stroke="#374151" strokeWidth="1" />
|
||||
<text x="200" y="155" textAnchor="middle" className="text-xs fill-gray-500">Cantidad (Q)</text>
|
||||
<text x="10" y="75" textAnchor="middle" className="text-xs fill-gray-500" transform="rotate(-90 10 75)">CT</text>
|
||||
|
||||
{datosCalculados.map((d, i) => (
|
||||
<text key={i} x={30 + i * 40} y="150" textAnchor="middle" className="text-xs fill-gray-500">
|
||||
{d.q}
|
||||
</text>
|
||||
))}
|
||||
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#2563eb"
|
||||
strokeWidth="2"
|
||||
points={datosCalculados.map((d, i) => `${30 + i * 40},${140 - d.ct * escalaCT}`).join(' ')}
|
||||
/>
|
||||
|
||||
{datosCalculados.map((d, i) => (
|
||||
<circle
|
||||
key={i}
|
||||
cx={30 + i * 40}
|
||||
cy={140 - d.ct * escalaCT}
|
||||
r="4"
|
||||
fill="#2563eb"
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-2">Costo Medio (CMe) vs Costo Marginal (CMg)</h4>
|
||||
<div className="h-40 bg-gray-50 rounded-lg p-4 relative">
|
||||
<svg className="w-full h-full" viewBox="0 0 400 160">
|
||||
<line x1="30" y1="140" x2="380" y2="140" stroke="#374151" strokeWidth="1" />
|
||||
<line x1="30" y1="140" x2="30" y2="10" stroke="#374151" strokeWidth="1" />
|
||||
<text x="200" y="155" textAnchor="middle" className="text-xs fill-gray-500">Cantidad (Q)</text>
|
||||
<text x="10" y="75" textAnchor="middle" className="text-xs fill-gray-500" transform="rotate(-90 10 75)">Costo</text>
|
||||
|
||||
{datosCalculados.filter(d => d.q > 0).map((d, i) => (
|
||||
<text key={i} x={70 + i * 40} y="150" textAnchor="middle" className="text-xs fill-gray-500">
|
||||
{d.q}
|
||||
</text>
|
||||
))}
|
||||
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#7c3aed"
|
||||
strokeWidth="2"
|
||||
points={datosCalculados
|
||||
.filter(d => d.q > 0)
|
||||
.map((d, i) => `${70 + i * 40},${140 - d.cme * escalaCMe}`)
|
||||
.join(' ')}
|
||||
/>
|
||||
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#16a34a"
|
||||
strokeWidth="2"
|
||||
strokeDasharray="4"
|
||||
points={datosCalculados
|
||||
.filter(d => d.cmg !== null)
|
||||
.map((d, i) => `${70 + i * 40},${140 - (d.cmg || 0) * escalaCMg}`)
|
||||
.join(' ')}
|
||||
/>
|
||||
|
||||
{datosCalculados.filter(d => d.q > 0).map((d, i) => (
|
||||
<circle
|
||||
key={`cme-${i}`}
|
||||
cx={70 + i * 40}
|
||||
cy={140 - d.cme * escalaCMe}
|
||||
r="4"
|
||||
fill="#7c3aed"
|
||||
/>
|
||||
))}
|
||||
|
||||
{datosCalculados.filter(d => d.cmg !== null).map((d, i) => (
|
||||
<circle
|
||||
key={`cmg-${i}`}
|
||||
cx={70 + i * 40}
|
||||
cy={140 - (d.cmg || 0) * escalaCMg}
|
||||
r="4"
|
||||
fill="#16a34a"
|
||||
/>
|
||||
))}
|
||||
|
||||
<g transform="translate(280, 30)">
|
||||
<line x1="0" y1="0" x2="20" y2="0" stroke="#7c3aed" strokeWidth="2" />
|
||||
<text x="25" y="4" className="text-xs fill-gray-600">CMe</text>
|
||||
<line x1="0" y1="15" x2="20" y2="15" stroke="#16a34a" strokeWidth="2" strokeDasharray="4" />
|
||||
<text x="25" y="19" className="text-xs fill-gray-600">CMg</text>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-blue-50 border-blue-200">
|
||||
<h4 className="font-semibold text-blue-900 mb-2">Fórmulas utilizadas:</h4>
|
||||
<ul className="space-y-1 text-sm text-blue-800">
|
||||
<li><strong>CT</strong> = CF + CV (Costo Total)</li>
|
||||
<li><strong>CFMe</strong> = CF / Q (Costo Fijo Medio)</li>
|
||||
<li><strong>CVMe</strong> = CV / Q (Costo Variable Medio)</li>
|
||||
<li><strong>CMe</strong> = CT / Q (Costo Medio)</li>
|
||||
<li><strong>CMg</strong> = ΔCT / ΔQ (Costo Marginal)</li>
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CalculadoraCostos;
|
||||
@@ -0,0 +1,318 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Card, CardHeader } from '../../ui/Card';
|
||||
import { Button } from '../../ui/Button';
|
||||
import { Input } from '../../ui/Input';
|
||||
import { CheckCircle, Target, TrendingUp, DollarSign } from 'lucide-react';
|
||||
|
||||
interface FilaProduccion {
|
||||
q: number;
|
||||
ct: number;
|
||||
}
|
||||
|
||||
interface FilaCalculada {
|
||||
q: number;
|
||||
precio: number;
|
||||
it: number;
|
||||
ct: number;
|
||||
bt: number;
|
||||
img: number | null;
|
||||
cmg: number | null;
|
||||
}
|
||||
|
||||
interface SimuladorProduccionProps {
|
||||
ejercicioId: string;
|
||||
onComplete?: (puntuacion: number) => void;
|
||||
}
|
||||
|
||||
export function SimuladorProduccion({ ejercicioId: _ejercicioId, onComplete }: SimuladorProduccionProps) {
|
||||
const [precio, setPrecio] = useState(80);
|
||||
|
||||
const datosBase: FilaProduccion[] = [
|
||||
{ q: 0, ct: 200 },
|
||||
{ q: 1, ct: 250 },
|
||||
{ q: 2, ct: 290 },
|
||||
{ q: 3, ct: 320 },
|
||||
{ q: 4, ct: 360 },
|
||||
{ q: 5, ct: 420 },
|
||||
{ q: 6, ct: 500 },
|
||||
{ q: 7, ct: 600 },
|
||||
{ q: 8, ct: 720 },
|
||||
];
|
||||
|
||||
const datosCalculados: FilaCalculada[] = useMemo(() => {
|
||||
return datosBase.map((fila, index) => {
|
||||
const it = precio * fila.q;
|
||||
const bt = it - fila.ct;
|
||||
const img = index > 0 ? precio : null;
|
||||
const cmg = index > 0 ? fila.ct - datosBase[index - 1].ct : null;
|
||||
|
||||
return {
|
||||
q: fila.q,
|
||||
precio,
|
||||
it,
|
||||
ct: fila.ct,
|
||||
bt,
|
||||
img,
|
||||
cmg,
|
||||
};
|
||||
});
|
||||
}, [precio]);
|
||||
|
||||
const qOptima = useMemo(() => {
|
||||
let maxBT = -Infinity;
|
||||
let qOpt = 0;
|
||||
|
||||
datosCalculados.forEach((fila) => {
|
||||
if (fila.bt > maxBT) {
|
||||
maxBT = fila.bt;
|
||||
qOpt = fila.q;
|
||||
}
|
||||
});
|
||||
|
||||
return qOpt;
|
||||
}, [datosCalculados]);
|
||||
|
||||
const verificacionIMgCMg = useMemo(() => {
|
||||
const filasValidas = datosCalculados.filter(f => f.img !== null && f.cmg !== null);
|
||||
const filaOptima = filasValidas.find(f => f.q === qOptima);
|
||||
|
||||
if (!filaOptima) return null;
|
||||
|
||||
return {
|
||||
img: filaOptima.img,
|
||||
cmg: filaOptima.cmg,
|
||||
diferencia: Math.abs((filaOptima.img || 0) - (filaOptima.cmg || 0)),
|
||||
cumple: Math.abs((filaOptima.img || 0) - (filaOptima.cmg || 0)) < 5,
|
||||
};
|
||||
}, [datosCalculados, qOptima]);
|
||||
|
||||
const maxValor = Math.max(
|
||||
...datosCalculados.map(d => Math.max(d.it, d.ct, d.bt > 0 ? d.bt : 0))
|
||||
);
|
||||
const escala = maxValor > 0 ? 140 / maxValor : 1;
|
||||
|
||||
const handleCompletar = () => {
|
||||
if (onComplete) {
|
||||
onComplete(100);
|
||||
}
|
||||
return 100;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Simulador de Decisión de Producción"
|
||||
subtitle="Encuentra la cantidad óptima que maximiza el beneficio"
|
||||
/>
|
||||
|
||||
<div className="mb-6 p-4 bg-blue-50 rounded-lg">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Precio de Mercado (P)
|
||||
</label>
|
||||
<div className="flex items-center gap-4">
|
||||
<DollarSign className="w-5 h-5 text-gray-400" />
|
||||
<Input
|
||||
type="number"
|
||||
value={precio}
|
||||
onChange={(e) => setPrecio(parseFloat(e.target.value) || 0)}
|
||||
className="w-32"
|
||||
min="0"
|
||||
/>
|
||||
<span className="text-sm text-gray-500">
|
||||
Ajusta el precio para ver cómo cambia la decisión óptima
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-gray-50 border-b">
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-700">Q</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-700">Precio (P)</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-primary">IT = P × Q</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-700">CT</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-success">BT = IT - CT</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-600">IMg</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-600">CMg</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{datosCalculados.map((fila) => (
|
||||
<tr
|
||||
key={fila.q}
|
||||
className={`border-b hover:bg-gray-50 ${
|
||||
fila.q === qOptima ? 'bg-green-50' : ''
|
||||
}`}
|
||||
>
|
||||
<td className="px-3 py-2 font-medium">{fila.q}</td>
|
||||
<td className="px-3 py-2">{fila.precio}</td>
|
||||
<td className="px-3 py-2 font-medium text-primary">{fila.it}</td>
|
||||
<td className="px-3 py-2">{fila.ct}</td>
|
||||
<td className={`px-3 py-2 font-bold ${fila.bt >= 0 ? 'text-success' : 'text-error'}`}>
|
||||
{fila.bt}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-gray-600">
|
||||
{fila.img !== null ? fila.img : '-'}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-gray-600">
|
||||
{fila.cmg !== null ? fila.cmg : '-'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<Target className="w-6 h-6 text-green-600" />
|
||||
<div>
|
||||
<p className="font-semibold text-green-800">
|
||||
Cantidad Óptima: Q = {qOptima}
|
||||
</p>
|
||||
<p className="text-sm text-green-700">
|
||||
Beneficio Máximo: BT = {datosCalculados.find(d => d.q === qOptima)?.bt}
|
||||
{' '}(${precio} × {qOptima} - {datosCalculados.find(d => d.q === qOptima)?.ct})
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{verificacionIMgCMg && (
|
||||
<div className={`mt-4 p-4 rounded-lg ${
|
||||
verificacionIMgCMg.cumple
|
||||
? 'bg-success/10 border border-success'
|
||||
: 'bg-yellow-50 border border-yellow-200'
|
||||
}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
{verificacionIMgCMg.cumple ? (
|
||||
<CheckCircle className="w-5 h-5 text-success" />
|
||||
) : (
|
||||
<TrendingUp className="w-5 h-5 text-yellow-600" />
|
||||
)}
|
||||
<span className={`font-medium ${
|
||||
verificacionIMgCMg.cumple ? 'text-success' : 'text-yellow-800'
|
||||
}`}>
|
||||
Verificación IMg ≈ CMg:
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-gray-600">
|
||||
IMg = {verificacionIMgCMg.img}, CMg = {verificacionIMgCMg.cmg}
|
||||
{' '}(Diferencia: {verificacionIMgCMg.diferencia.toFixed(1)})
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
{verificacionIMgCMg.cumple
|
||||
? '✓ La condición de optimalidad se cumple: IMg ≈ CMg'
|
||||
: 'La diferencia es significativa, pero el beneficio sigue siendo máximo en Q = ' + qOptima}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Gráfico de IT y CT"
|
||||
subtitle="Visualiza el punto donde la distancia entre IT y CT es máxima"
|
||||
/>
|
||||
|
||||
<div className="h-64 bg-gray-50 rounded-lg p-4">
|
||||
<svg className="w-full h-full" viewBox="0 0 500 220">
|
||||
<line x1="50" y1="190" x2="480" y2="190" stroke="#374151" strokeWidth="1" />
|
||||
<line x1="50" y1="190" x2="50" y2="20" stroke="#374151" strokeWidth="1" />
|
||||
<text x="265" y="210" textAnchor="middle" className="text-sm fill-gray-600">Cantidad (Q)</text>
|
||||
<text x="20" y="105" textAnchor="middle" className="text-sm fill-gray-600" transform="rotate(-90 20 105)">$</text>
|
||||
|
||||
{datosCalculados.map((d, i) => (
|
||||
<text key={i} x={80 + i * 45} y="205" textAnchor="middle" className="text-xs fill-gray-500">
|
||||
{d.q}
|
||||
</text>
|
||||
))}
|
||||
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#2563eb"
|
||||
strokeWidth="3"
|
||||
points={datosCalculados.map((d, i) => `${80 + i * 45},${190 - d.it * escala}`).join(' ')}
|
||||
/>
|
||||
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#dc2626"
|
||||
strokeWidth="3"
|
||||
points={datosCalculados.map((d, i) => `${80 + i * 45},${190 - d.ct * escala}`).join(' ')}
|
||||
/>
|
||||
|
||||
{datosCalculados.map((d, i) => (
|
||||
<g key={i}>
|
||||
<circle
|
||||
cx={80 + i * 45}
|
||||
cy={190 - d.it * escala}
|
||||
r="5"
|
||||
fill="#2563eb"
|
||||
/>
|
||||
<circle
|
||||
cx={80 + i * 45}
|
||||
cy={190 - d.ct * escala}
|
||||
r="5"
|
||||
fill="#dc2626"
|
||||
/>
|
||||
</g>
|
||||
))}
|
||||
|
||||
<g transform={`translate(${80 + datosCalculados.findIndex(d => d.q === qOptima) * 45}, ${
|
||||
190 - (datosCalculados.find(d => d.q === qOptima)?.it || 0) * escala - 20
|
||||
})`}>
|
||||
<polygon points="0,0 -8,-15 8,-15" fill="#16a34a" />
|
||||
<text x="0" y="-20" textAnchor="middle" className="text-xs fill-green-600 font-bold">
|
||||
Óptimo Q={qOptima}
|
||||
</text>
|
||||
</g>
|
||||
|
||||
<g transform="translate(380, 40)">
|
||||
<line x1="0" y1="0" x2="30" y2="0" stroke="#2563eb" strokeWidth="3" />
|
||||
<text x="40" y="5" className="text-sm fill-gray-700">IT (Ingreso Total)</text>
|
||||
<line x1="0" y1="20" x2="30" y2="20" stroke="#dc2626" strokeWidth="3" />
|
||||
<text x="40" y="25" className="text-sm fill-gray-700">CT (Costo Total)</text>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-r from-green-50 to-blue-50">
|
||||
<h4 className="font-semibold text-gray-900 mb-3 flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
Conceptos Clave
|
||||
</h4>
|
||||
<div className="grid md:grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="font-medium text-gray-700 mb-1">Ingreso Total (IT)</p>
|
||||
<p className="text-gray-600">IT = P × Q</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-700 mb-1">Beneficio Total (BT)</p>
|
||||
<p className="text-gray-600">BT = IT - CT</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-700 mb-1">Ingreso Marginal (IMg)</p>
|
||||
<p className="text-gray-600">IMg = ΔIT / ΔQ = P (en competencia perfecta)</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-700 mb-1">Condición de Optimalidad</p>
|
||||
<p className="text-gray-600">IMg = CMg (producir hasta que el ingreso marginal iguale al costo marginal)</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleCompletar} size="lg">
|
||||
<CheckCircle className="w-5 h-5 mr-2" />
|
||||
Marcar como Completado
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SimuladorProduccion;
|
||||
@@ -0,0 +1,344 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Card, CardHeader } from '../../ui/Card';
|
||||
import { Button } from '../../ui/Button';
|
||||
import { Input } from '../../ui/Input';
|
||||
import { CheckCircle, Info, TrendingUp } from 'lucide-react';
|
||||
|
||||
interface VisualizadorExcedentesProps {
|
||||
ejercicioId: string;
|
||||
onComplete?: (puntuacion: number) => void;
|
||||
}
|
||||
|
||||
export function VisualizadorExcedentes({ ejercicioId: _ejercicioId, onComplete }: VisualizadorExcedentesProps) {
|
||||
const [precio, setPrecio] = useState(50);
|
||||
|
||||
const demandaParams = { a: 100, b: 1 };
|
||||
const ofertaParams = { c: 10, d: 0.8 };
|
||||
|
||||
const puntoEquilibrio = useMemo(() => {
|
||||
const { a, b } = demandaParams;
|
||||
const { c, d } = ofertaParams;
|
||||
const pEq = (a - c) / (b + d);
|
||||
const qEq = a - b * pEq;
|
||||
return { pEq, qEq };
|
||||
}, []);
|
||||
|
||||
const datosCurvas = useMemo(() => {
|
||||
const puntos = [];
|
||||
const { a, b } = demandaParams;
|
||||
const { c, d } = ofertaParams;
|
||||
|
||||
for (let q = 0; q <= 100; q += 5) {
|
||||
const pDemanda = (a - q) / b;
|
||||
const pOferta = q > 0 ? (q - c) / d : 0;
|
||||
puntos.push({ q, pDemanda: Math.max(0, pDemanda), pOferta: Math.max(0, pOferta) });
|
||||
}
|
||||
|
||||
return puntos;
|
||||
}, []);
|
||||
|
||||
const excedentes = useMemo(() => {
|
||||
const { a } = demandaParams;
|
||||
const { c } = ofertaParams;
|
||||
|
||||
const qAlPrecio = Math.max(0, a - demandaParams.b * precio);
|
||||
const qOfrecida = Math.max(0, c + ofertaParams.d * precio);
|
||||
|
||||
const excedenteConsumidor = 0.5 * qAlPrecio * (a - precio);
|
||||
const excedenteProductor = 0.5 * qOfrecida * (precio - c);
|
||||
|
||||
return {
|
||||
ec: excedenteConsumidor,
|
||||
ep: excedenteProductor,
|
||||
total: excedenteConsumidor + excedenteProductor,
|
||||
qAlPrecio,
|
||||
qOfrecida,
|
||||
};
|
||||
}, [precio]);
|
||||
|
||||
const excedentesEquilibrio = useMemo(() => {
|
||||
const { pEq, qEq } = puntoEquilibrio;
|
||||
const { a } = demandaParams;
|
||||
const { c } = ofertaParams;
|
||||
|
||||
const ec = 0.5 * qEq * (a - pEq);
|
||||
const ep = 0.5 * qEq * (pEq - c);
|
||||
|
||||
return { ec, ep, total: ec + ep };
|
||||
}, [puntoEquilibrio]);
|
||||
|
||||
const maxP = 100;
|
||||
const maxQ = 100;
|
||||
const escalaX = 350 / maxQ;
|
||||
const escalaY = 180 / maxP;
|
||||
|
||||
const handleCompletar = () => {
|
||||
if (onComplete) {
|
||||
onComplete(100);
|
||||
}
|
||||
return 100;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Visualizador de Excedentes"
|
||||
subtitle="Ajusta el precio para ver cómo cambian los excedentes del consumidor y productor"
|
||||
/>
|
||||
|
||||
<div className="mb-6 p-4 bg-blue-50 rounded-lg">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Precio de Mercado (P)
|
||||
</label>
|
||||
<div className="flex items-center gap-4">
|
||||
<Input
|
||||
type="range"
|
||||
min="20"
|
||||
max="90"
|
||||
value={precio}
|
||||
onChange={(e) => setPrecio(parseFloat(e.target.value))}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="text-2xl font-bold text-primary min-w-[80px]">${precio}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-gray-500 mt-1">
|
||||
<span>$20</span>
|
||||
<span>Precio de equilibrio: ${puntoEquilibrio.pEq.toFixed(1)}</span>
|
||||
<span>$90</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative h-80 bg-gray-50 rounded-lg p-4 overflow-hidden">
|
||||
<svg className="w-full h-full" viewBox="0 0 400 220" preserveAspectRatio="xMidYMid meet">
|
||||
<line x1="50" y1="200" x2="380" y2="200" stroke="#374151" strokeWidth="2" />
|
||||
<line x1="50" y1="200" x2="50" y2="20" stroke="#374151" strokeWidth="2" />
|
||||
|
||||
<text x="215" y="215" textAnchor="middle" className="text-sm fill-gray-700 font-medium">Cantidad (Q)</text>
|
||||
<text x="15" y="110" textAnchor="middle" className="text-sm fill-gray-700 font-medium" transform="rotate(-90 15 110)">Precio (P)</text>
|
||||
|
||||
{[0, 25, 50, 75, 100].map((q) => (
|
||||
<g key={q}>
|
||||
<line
|
||||
x1={50 + q * escalaX}
|
||||
y1="200"
|
||||
x2={50 + q * escalaX}
|
||||
y2="205"
|
||||
stroke="#374151"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<text
|
||||
x={50 + q * escalaX}
|
||||
y="215"
|
||||
textAnchor="middle"
|
||||
className="text-xs fill-gray-500"
|
||||
>
|
||||
{q}
|
||||
</text>
|
||||
</g>
|
||||
))}
|
||||
|
||||
{[0, 25, 50, 75, 100].map((p) => (
|
||||
<g key={p}>
|
||||
<line
|
||||
x1="45"
|
||||
y1={200 - p * escalaY}
|
||||
x2="50"
|
||||
y2={200 - p * escalaY}
|
||||
stroke="#374151"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<text
|
||||
x="40"
|
||||
y={200 - p * escalaY + 4}
|
||||
textAnchor="end"
|
||||
className="text-xs fill-gray-500"
|
||||
>
|
||||
{p}
|
||||
</text>
|
||||
</g>
|
||||
))}
|
||||
|
||||
<line
|
||||
x1="50"
|
||||
y1={200 - precio * escalaY}
|
||||
x2="380"
|
||||
y2={200 - precio * escalaY}
|
||||
stroke="#7c3aed"
|
||||
strokeWidth="2"
|
||||
strokeDasharray="5,5"
|
||||
opacity="0.7"
|
||||
/>
|
||||
<text x="385" y={200 - precio * escalaY + 4} className="text-xs fill-purple-600 font-medium">
|
||||
P = {precio}
|
||||
</text>
|
||||
|
||||
{precio > puntoEquilibrio.pEq && (
|
||||
<polygon
|
||||
points={`
|
||||
50,${200 - precio * escalaY}
|
||||
${50 + excedentes.qAlPrecio * escalaX},${200 - precio * escalaY}
|
||||
${50 + excedentes.qAlPrecio * escalaX},${200 - ((100 - excedentes.qAlPrecio)) * escalaY}
|
||||
`}
|
||||
fill="rgba(37, 99, 235, 0.3)"
|
||||
stroke="#2563eb"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
)}
|
||||
|
||||
{precio < puntoEquilibrio.pEq && (
|
||||
<polygon
|
||||
points={`
|
||||
50,${200 - 12.5 * escalaY}
|
||||
${50 + excedentes.qOfrecida * escalaX},${200 - precio * escalaY}
|
||||
50,${200 - precio * escalaY}
|
||||
`}
|
||||
fill="rgba(22, 163, 74, 0.3)"
|
||||
stroke="#16a34a"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
)}
|
||||
|
||||
{Math.abs(precio - puntoEquilibrio.pEq) < 2 && (
|
||||
<>
|
||||
<polygon
|
||||
points={`
|
||||
50,${200 - puntoEquilibrio.pEq * escalaY}
|
||||
${50 + puntoEquilibrio.qEq * escalaX},${200 - puntoEquilibrio.pEq * escalaY}
|
||||
${50 + puntoEquilibrio.qEq * escalaX},${200 - 100 * escalaY}
|
||||
`}
|
||||
fill="rgba(37, 99, 235, 0.3)"
|
||||
stroke="#2563eb"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<polygon
|
||||
points={`
|
||||
50,${200 - 12.5 * escalaY}
|
||||
${50 + puntoEquilibrio.qEq * escalaX},${200 - puntoEquilibrio.pEq * escalaY}
|
||||
50,${200 - puntoEquilibrio.pEq * escalaY}
|
||||
`}
|
||||
fill="rgba(22, 163, 74, 0.3)"
|
||||
stroke="#16a34a"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#2563eb"
|
||||
strokeWidth="3"
|
||||
points={datosCurvas.map(d => `${50 + d.q * escalaX},${200 - d.pDemanda * escalaY}`).join(' ')}
|
||||
/>
|
||||
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#16a34a"
|
||||
strokeWidth="3"
|
||||
points={datosCurvas.filter(d => d.pOferta >= 0).map(d => `${50 + d.q * escalaX},${200 - d.pOferta * escalaY}`).join(' ')}
|
||||
/>
|
||||
|
||||
<circle
|
||||
cx={50 + puntoEquilibrio.qEq * escalaX}
|
||||
cy={200 - puntoEquilibrio.pEq * escalaY}
|
||||
r="6"
|
||||
fill="#dc2626"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<text
|
||||
x={50 + puntoEquilibrio.qEq * escalaX}
|
||||
y={200 - puntoEquilibrio.pEq * escalaY - 12}
|
||||
textAnchor="middle"
|
||||
className="text-xs fill-red-600 font-bold"
|
||||
>
|
||||
E
|
||||
</text>
|
||||
|
||||
<g transform="translate(320, 40)">
|
||||
<line x1="0" y1="0" x2="25" y2="0" stroke="#2563eb" strokeWidth="3" />
|
||||
<text x="30" y="5" className="text-xs fill-gray-700">Demanda</text>
|
||||
<line x1="0" y1="15" x2="25" y2="15" stroke="#16a34a" strokeWidth="3" />
|
||||
<text x="30" y="20" className="text-xs fill-gray-700">Oferta</text>
|
||||
<rect x="0" y="30" width="15" height="15" fill="rgba(37, 99, 235, 0.3)" stroke="#2563eb" />
|
||||
<text x="20" y="42" className="text-xs fill-gray-700">EC</text>
|
||||
<rect x="0" y="50" width="15" height="15" fill="rgba(22, 163, 74, 0.3)" stroke="#16a34a" />
|
||||
<text x="20" y="62" className="text-xs fill-gray-700">EP</text>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-4">
|
||||
<Card className="bg-blue-50 border-blue-200">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-4 h-4 bg-blue-500/30 border border-blue-500 rounded" />
|
||||
<h4 className="font-semibold text-blue-900">Excedente del Consumidor</h4>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-blue-700">${excedentes.ec.toFixed(0)}</p>
|
||||
<p className="text-sm text-blue-600 mt-1">
|
||||
Área bajo la curva de demanda y sobre el precio
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-green-50 border-green-200">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-4 h-4 bg-green-500/30 border border-green-500 rounded" />
|
||||
<h4 className="font-semibold text-green-900">Excedente del Productor</h4>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-green-700">${excedentes.ep.toFixed(0)}</p>
|
||||
<p className="text-sm text-green-600 mt-1">
|
||||
Área sobre la curva de oferta y bajo el precio
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-purple-50 border-purple-200">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<TrendingUp className="w-5 h-5 text-purple-600" />
|
||||
<h4 className="font-semibold text-purple-900">Excedente Total</h4>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-purple-700">${excedentes.total.toFixed(0)}</p>
|
||||
<p className="text-sm text-purple-600 mt-1">
|
||||
EC + EP = Bienestar social total
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="bg-yellow-50 border-yellow-200">
|
||||
<div className="flex items-start gap-3">
|
||||
<Info className="w-5 h-5 text-yellow-600 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-semibold text-yellow-900 mb-2">En el Equilibrio de Mercado:</h4>
|
||||
<div className="grid md:grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-yellow-800">Precio: </span>
|
||||
<span className="font-bold">${puntoEquilibrio.pEq.toFixed(1)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-yellow-800">Cantidad: </span>
|
||||
<span className="font-bold">{puntoEquilibrio.qEq.toFixed(1)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-yellow-800">Excedente Total: </span>
|
||||
<span className="font-bold">${excedentesEquilibrio.total.toFixed(0)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-3 text-yellow-800 text-sm">
|
||||
El equilibrio de mercado maximiza el bienestar social (excedente total).
|
||||
Cualquier desviación del precio de equilibrio genera pérdida de eficiencia.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleCompletar} size="lg">
|
||||
<CheckCircle className="w-5 h-5 mr-2" />
|
||||
Marcar como Completado
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default VisualizadorExcedentes;
|
||||
3
frontend/src/components/exercises/modulo4/index.ts
Normal file
3
frontend/src/components/exercises/modulo4/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { CalculadoraCostos } from './CalculadoraCostos';
|
||||
export { SimuladorProduccion } from './SimuladorProduccion';
|
||||
export { VisualizadorExcedentes } from './VisualizadorExcedentes';
|
||||
225
frontend/src/components/progress/Badges.tsx
Normal file
225
frontend/src/components/progress/Badges.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Footprints,
|
||||
BookOpen,
|
||||
Scale,
|
||||
StretchHorizontal,
|
||||
Factory,
|
||||
GraduationCap,
|
||||
Target,
|
||||
Award,
|
||||
Lock,
|
||||
Unlock,
|
||||
Trophy
|
||||
} from 'lucide-react';
|
||||
import type { Badge } from '../../types';
|
||||
|
||||
const ICON_MAP: Record<string, React.ComponentType<{ size?: number | string; className?: string }>> = {
|
||||
Footprints,
|
||||
BookOpen,
|
||||
Scale,
|
||||
StretchHorizontal,
|
||||
Factory,
|
||||
GraduationCap,
|
||||
Target,
|
||||
Award,
|
||||
};
|
||||
|
||||
interface BadgeCardProps {
|
||||
badge: Badge;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
export function BadgeCard({ badge, size = 'md' }: BadgeCardProps) {
|
||||
const Icon = ICON_MAP[badge.icono] || Trophy;
|
||||
|
||||
const sizeClasses = {
|
||||
sm: {
|
||||
container: 'p-3',
|
||||
icon: 20,
|
||||
title: 'text-xs',
|
||||
desc: 'text-[10px]',
|
||||
},
|
||||
md: {
|
||||
container: 'p-4',
|
||||
icon: 28,
|
||||
title: 'text-sm',
|
||||
desc: 'text-xs',
|
||||
},
|
||||
lg: {
|
||||
container: 'p-5',
|
||||
icon: 36,
|
||||
title: 'text-base',
|
||||
desc: 'text-sm',
|
||||
},
|
||||
};
|
||||
|
||||
if (badge.desbloqueado) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
className={`${sizeClasses[size].container} bg-gradient-to-br from-yellow-50 to-orange-50 rounded-xl border-2 border-yellow-300 shadow-sm hover:shadow-md transition-all`}
|
||||
>
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<div className="relative">
|
||||
<motion.div
|
||||
animate={{
|
||||
rotate: [0, -5, 5, 0],
|
||||
scale: [1, 1.1, 1]
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
repeatDelay: 3
|
||||
}}
|
||||
className="w-12 h-12 bg-gradient-to-br from-yellow-400 to-orange-500 rounded-full flex items-center justify-center mb-2"
|
||||
>
|
||||
<Icon size={sizeClasses[size].icon} className="text-white" />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ delay: 0.3, type: "spring" }}
|
||||
className="absolute -top-1 -right-1 w-5 h-5 bg-green-500 rounded-full flex items-center justify-center"
|
||||
>
|
||||
<Unlock size={12} className="text-white" />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<h4 className={`${sizeClasses[size].title} font-bold text-gray-800 mb-1`}>
|
||||
{badge.titulo}
|
||||
</h4>
|
||||
<p className={`${sizeClasses[size].desc} text-gray-600 line-clamp-2`}>
|
||||
{badge.descripcion}
|
||||
</p>
|
||||
|
||||
{badge.fechaDesbloqueo && (
|
||||
<p className="text-[10px] text-gray-400 mt-2">
|
||||
Desbloqueado: {new Date(badge.fechaDesbloqueo).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${sizeClasses[size].container} bg-gray-50 rounded-xl border-2 border-gray-200 opacity-70`}>
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<div className="relative">
|
||||
<div className="w-12 h-12 bg-gray-300 rounded-full flex items-center justify-center mb-2">
|
||||
<Icon size={sizeClasses[size].icon} className="text-gray-500" />
|
||||
</div>
|
||||
<div className="absolute -top-1 -right-1 w-5 h-5 bg-gray-400 rounded-full flex items-center justify-center">
|
||||
<Lock size={12} className="text-white" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 className={`${sizeClasses[size].title} font-bold text-gray-500 mb-1`}>
|
||||
{badge.titulo}
|
||||
</h4>
|
||||
|
||||
<p className={`${sizeClasses[size].desc} text-gray-400 line-clamp-2`}>
|
||||
{badge.descripcion}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface BadgesGridProps {
|
||||
badges: Badge[];
|
||||
columns?: 2 | 3 | 4;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
export function BadgesGrid({ badges, columns = 4, size = 'md' }: BadgesGridProps) {
|
||||
const columnClasses = {
|
||||
2: 'grid-cols-2',
|
||||
3: 'grid-cols-2 md:grid-cols-3',
|
||||
4: 'grid-cols-2 md:grid-cols-3 lg:grid-cols-4',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`grid ${columnClasses[columns]} gap-4`}>
|
||||
{badges.map((badge, index) => (
|
||||
<motion.div
|
||||
key={badge.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
>
|
||||
<BadgeCard badge={badge} size={size} />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface BadgesSectionProps {
|
||||
badgesDesbloqueados: Badge[];
|
||||
badgesBloqueados: Badge[];
|
||||
}
|
||||
|
||||
export function BadgesSection({ badgesDesbloqueados, badgesBloqueados }: BadgesSectionProps) {
|
||||
const totalBadges = badgesDesbloqueados.length + badgesBloqueados.length;
|
||||
const porcentaje = totalBadges > 0 ? Math.round((badgesDesbloqueados.length / totalBadges) * 100) : 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Resumen */}
|
||||
<div className="bg-white rounded-xl p-4 border border-gray-200">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-yellow-100 rounded-lg flex items-center justify-center">
|
||||
<Trophy size={20} className="text-yellow-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-gray-900">Logros</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
{badgesDesbloqueados.length} de {totalBadges} desbloqueados
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-yellow-600">
|
||||
{porcentaje}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-100 rounded-full h-3 overflow-hidden">
|
||||
<motion.div
|
||||
className="h-full bg-gradient-to-r from-yellow-400 to-orange-500 rounded-full"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${porcentaje}%` }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Badges Desbloqueados */}
|
||||
{badgesDesbloqueados.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
|
||||
<Unlock size={16} className="text-green-500" />
|
||||
Desbloqueados ({badgesDesbloqueados.length})
|
||||
</h4>
|
||||
<BadgesGrid badges={badgesDesbloqueados} columns={4} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Badges Bloqueados */}
|
||||
{badgesBloqueados.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-gray-500 mb-3 flex items-center gap-2">
|
||||
<Lock size={16} className="text-gray-400" />
|
||||
Por desbloquear ({badgesBloqueados.length})
|
||||
</h4>
|
||||
<BadgesGrid badges={badgesBloqueados} columns={4} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BadgesGrid;
|
||||
81
frontend/src/components/progress/ProgressBar.tsx
Normal file
81
frontend/src/components/progress/ProgressBar.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface ProgressBarProps {
|
||||
porcentaje: number;
|
||||
moduloNumero: number;
|
||||
showLabel?: boolean;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
export function ProgressBar({
|
||||
porcentaje,
|
||||
moduloNumero,
|
||||
showLabel = true,
|
||||
size = 'md'
|
||||
}: ProgressBarProps) {
|
||||
// Determinar color según progreso
|
||||
const getColor = () => {
|
||||
if (porcentaje < 30) return 'bg-red-500';
|
||||
if (porcentaje < 70) return 'bg-yellow-500';
|
||||
return 'bg-green-500';
|
||||
};
|
||||
|
||||
// Determinar color del borde/fondo según progreso
|
||||
const getBgColor = () => {
|
||||
if (porcentaje < 30) return 'bg-red-100';
|
||||
if (porcentaje < 70) return 'bg-yellow-100';
|
||||
return 'bg-green-100';
|
||||
};
|
||||
|
||||
const getTextColor = () => {
|
||||
if (porcentaje < 30) return 'text-red-700';
|
||||
if (porcentaje < 70) return 'text-yellow-700';
|
||||
return 'text-green-700';
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'h-2',
|
||||
md: 'h-4',
|
||||
lg: 'h-6',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{showLabel && (
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Módulo {moduloNumero}
|
||||
</span>
|
||||
<span className={`text-sm font-bold ${getTextColor()}`}>
|
||||
{porcentaje}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`w-full ${getBgColor()} rounded-full overflow-hidden ${sizeClasses[size]}`}>
|
||||
<motion.div
|
||||
className={`${getColor()} ${sizeClasses[size]} rounded-full`}
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${porcentaje}%` }}
|
||||
transition={{
|
||||
duration: 0.8,
|
||||
ease: "easeOut",
|
||||
delay: 0.2
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{porcentaje === 100 && (
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="text-xs text-green-600 mt-1 font-medium"
|
||||
>
|
||||
¡Módulo completado!
|
||||
</motion.p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProgressBar;
|
||||
212
frontend/src/components/progress/ScoreDisplay.tsx
Normal file
212
frontend/src/components/progress/ScoreDisplay.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Star } from 'lucide-react';
|
||||
import type { NivelUsuario } from '../../types';
|
||||
|
||||
interface ScoreDisplayProps {
|
||||
puntos: number;
|
||||
animar?: boolean;
|
||||
showNivel?: boolean;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
const NIVELES_CONFIG: Record<NivelUsuario, { color: string; bgColor: string; icon: string }> = {
|
||||
Novato: {
|
||||
color: 'text-gray-600',
|
||||
bgColor: 'bg-gray-100',
|
||||
icon: '🌱'
|
||||
},
|
||||
Aprendiz: {
|
||||
color: 'text-blue-600',
|
||||
bgColor: 'bg-blue-100',
|
||||
icon: '📚'
|
||||
},
|
||||
Experto: {
|
||||
color: 'text-purple-600',
|
||||
bgColor: 'bg-purple-100',
|
||||
icon: '🏆'
|
||||
},
|
||||
Maestro: {
|
||||
color: 'text-yellow-600',
|
||||
bgColor: 'bg-yellow-100',
|
||||
icon: '👑'
|
||||
},
|
||||
};
|
||||
|
||||
function calcularNivel(puntuacion: number): NivelUsuario {
|
||||
if (puntuacion >= 2000) return 'Maestro';
|
||||
if (puntuacion >= 1000) return 'Experto';
|
||||
if (puntuacion >= 300) return 'Aprendiz';
|
||||
return 'Novato';
|
||||
}
|
||||
|
||||
function calcularProgresoNivel(puntuacion: number): { actual: number; siguiente: number; porcentaje: number } {
|
||||
if (puntuacion >= 2000) {
|
||||
return { actual: 2000, siguiente: 2000, porcentaje: 100 };
|
||||
} else if (puntuacion >= 1000) {
|
||||
return { actual: puntuacion, siguiente: 2000, porcentaje: ((puntuacion - 1000) / 1000) * 100 };
|
||||
} else if (puntuacion >= 300) {
|
||||
return { actual: puntuacion, siguiente: 1000, porcentaje: ((puntuacion - 300) / 700) * 100 };
|
||||
} else {
|
||||
return { actual: puntuacion, siguiente: 300, porcentaje: (puntuacion / 300) * 100 };
|
||||
}
|
||||
}
|
||||
|
||||
export function ScoreDisplay({
|
||||
puntos,
|
||||
animar = false,
|
||||
showNivel = true,
|
||||
size = 'md'
|
||||
}: ScoreDisplayProps) {
|
||||
const [puntosAnimados, setPuntosAnimados] = useState(0);
|
||||
const [puntosPrevios, setPuntosPrevios] = useState(puntos);
|
||||
const [cambioReciente, setCambioReciente] = useState(0);
|
||||
|
||||
const nivel = calcularNivel(puntos);
|
||||
const configNivel = NIVELES_CONFIG[nivel];
|
||||
const progresoNivel = calcularProgresoNivel(puntos);
|
||||
|
||||
useEffect(() => {
|
||||
if (animar && puntos !== puntosPrevios) {
|
||||
const diferencia = puntos - puntosPrevios;
|
||||
setCambioReciente(diferencia);
|
||||
|
||||
// Animar contador
|
||||
const duracion = 1500;
|
||||
const pasos = 60;
|
||||
let pasoActual = 0;
|
||||
|
||||
const intervalo = setInterval(() => {
|
||||
pasoActual++;
|
||||
const progreso = pasoActual / pasos;
|
||||
// Función de easing
|
||||
const easeOutQuart = 1 - Math.pow(1 - progreso, 4);
|
||||
|
||||
setPuntosAnimados(Math.round(puntosPrevios + (diferencia * easeOutQuart)));
|
||||
|
||||
if (pasoActual >= pasos) {
|
||||
clearInterval(intervalo);
|
||||
setPuntosAnimados(puntos);
|
||||
setPuntosPrevios(puntos);
|
||||
|
||||
// Ocultar el cambio después de 3 segundos
|
||||
setTimeout(() => setCambioReciente(0), 3000);
|
||||
}
|
||||
}, duracion / pasos);
|
||||
|
||||
return () => clearInterval(intervalo);
|
||||
} else {
|
||||
setPuntosAnimados(puntos);
|
||||
setPuntosPrevios(puntos);
|
||||
}
|
||||
}, [puntos, animar, puntosPrevios]);
|
||||
|
||||
const sizeClasses = {
|
||||
sm: {
|
||||
container: 'p-2',
|
||||
puntos: 'text-xl',
|
||||
label: 'text-xs',
|
||||
icon: 16,
|
||||
},
|
||||
md: {
|
||||
container: 'p-4',
|
||||
puntos: 'text-3xl',
|
||||
label: 'text-sm',
|
||||
icon: 24,
|
||||
},
|
||||
lg: {
|
||||
container: 'p-6',
|
||||
puntos: 'text-5xl',
|
||||
label: 'text-base',
|
||||
icon: 32,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${sizeClasses[size].container} bg-white rounded-xl border border-gray-200 shadow-sm`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-lg ${configNivel.bgColor}`}>
|
||||
<Star size={sizeClasses[size].icon} className={configNivel.color} />
|
||||
</div>
|
||||
<div>
|
||||
<p className={`${sizeClasses[size].label} text-gray-500 font-medium`}>Puntuación Total</p>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<motion.span
|
||||
className={`${sizeClasses[size].puntos} font-bold text-gray-900`}
|
||||
key={puntosAnimados}
|
||||
initial={animar ? { scale: 1.2 } : false}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ type: "spring", stiffness: 300 }}
|
||||
>
|
||||
{puntosAnimados.toLocaleString()}
|
||||
</motion.span>
|
||||
<span className="text-sm text-gray-400">pts</span>
|
||||
|
||||
<AnimatePresence>
|
||||
{cambioReciente !== 0 && (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 20, scale: 0.5 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
className={`text-sm font-bold ${cambioReciente > 0 ? 'text-green-600' : 'text-red-600'}`}
|
||||
>
|
||||
{cambioReciente > 0 ? '+' : ''}{cambioReciente}
|
||||
</motion.span>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showNivel && (
|
||||
<div className="text-right">
|
||||
<motion.div
|
||||
key={nivel}
|
||||
initial={animar ? { rotate: -10 } : false}
|
||||
animate={{ rotate: 0 }}
|
||||
className={`inline-flex items-center gap-1 px-3 py-1 rounded-full ${configNivel.bgColor}`}
|
||||
>
|
||||
<span className="text-lg">{configNivel.icon}</span>
|
||||
<span className={`text-sm font-bold ${configNivel.color}`}>{nivel}</span>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showNivel && progresoNivel.siguiente > progresoNivel.actual && (
|
||||
<div className="mt-4">
|
||||
<div className="flex justify-between text-xs text-gray-500 mb-1">
|
||||
<span>Progreso hacia {calcularNivel(progresoNivel.siguiente)}</span>
|
||||
<span>{Math.round(progresoNivel.porcentaje)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-100 rounded-full h-2 overflow-hidden">
|
||||
<motion.div
|
||||
className={`h-full rounded-full ${configNivel.color.replace('text-', 'bg-')}`}
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${progresoNivel.porcentaje}%` }}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
{progresoNivel.siguiente - progresoNivel.actual} puntos para el siguiente nivel
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{progresoNivel.porcentaje === 100 && nivel === 'Maestro' && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="mt-3 p-2 bg-yellow-50 rounded-lg border border-yellow-200"
|
||||
>
|
||||
<p className="text-xs text-yellow-700 text-center font-medium">
|
||||
🎉 ¡Has alcanzado el nivel máximo!
|
||||
</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ScoreDisplay;
|
||||
3
frontend/src/components/progress/index.ts
Normal file
3
frontend/src/components/progress/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { ProgressBar } from './ProgressBar';
|
||||
export { ScoreDisplay } from './ScoreDisplay';
|
||||
export { BadgesGrid, BadgesSection, BadgeCard } from './Badges';
|
||||
@@ -3,11 +3,17 @@ import { ReactNode } from 'react';
|
||||
interface CardProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export function Card({ children, className = '' }: CardProps) {
|
||||
export function Card({ children, className = '', onClick }: CardProps) {
|
||||
return (
|
||||
<div className={`bg-white rounded-xl shadow-md p-6 ${className}`}>
|
||||
<div
|
||||
className={`bg-white rounded-xl shadow-md p-6 ${className}`}
|
||||
onClick={onClick}
|
||||
role={onClick ? "button" : undefined}
|
||||
tabIndex={onClick ? 0 : undefined}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
20
frontend/src/components/ui/Loader.tsx
Normal file
20
frontend/src/components/ui/Loader.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
interface LoaderProps {
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'w-4 h-4',
|
||||
md: 'w-8 h-8',
|
||||
lg: 'w-12 h-12',
|
||||
};
|
||||
|
||||
export function Loader({ size = 'md', className = '' }: LoaderProps) {
|
||||
return (
|
||||
<Loader2
|
||||
className={`animate-spin text-blue-600 ${sizeClasses[size]} ${className}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
141
frontend/src/content/modulo1/agentes.ts
Normal file
141
frontend/src/content/modulo1/agentes.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import type { ModuloContenido } from './introduccion';
|
||||
|
||||
export const agentes: ModuloContenido = {
|
||||
titulo: 'Agentes Económicos',
|
||||
contenido: [
|
||||
{
|
||||
titulo: 'Las Familias o Hogares',
|
||||
contenido: `Las familias constituyen la unidad básica de consumo en la economía. Sus funciones principales son:
|
||||
|
||||
**Como consumidores:**
|
||||
- Adquieren bienes y servicios para satisfacer sus necesidades
|
||||
- Toman decisiones sobre qué comprar, cuánto y a qué precio
|
||||
- Maximizan su utilidad (satisfacción) dado su presupuesto
|
||||
|
||||
**Como oferentes de factores productivos:**
|
||||
- Proporcionan trabajo a cambio de salarios
|
||||
- Ofrecen capital (ahorros) a cambio de intereses
|
||||
- Entregan tierra/naturaleza a cambio de renta
|
||||
- Aportan habilidades empresariales a cambio de beneficios
|
||||
|
||||
Las decisiones de las familias están influenciadas por sus ingresos, preferencias, precios y expectativas futuras.`
|
||||
},
|
||||
{
|
||||
titulo: 'Las Empresas',
|
||||
contenido: `Las empresas son las unidades productivas que transforman insumos en bienes y servicios. Sus características principales:
|
||||
|
||||
**Funciones:**
|
||||
- Compran factores de producción (trabajo, capital, materias primas)
|
||||
- Organizan el proceso productivo
|
||||
- Venden bienes y servicios en los mercados
|
||||
|
||||
**Objetivo principal:**
|
||||
Maximizar beneficios (diferencia entre ingresos y costos)
|
||||
|
||||
**Clasificación por tamaño:**
|
||||
- Microempresas: menos de 10 trabajadores
|
||||
- Pequeñas empresas: 10-50 trabajadores
|
||||
- Medianas empresas: 50-250 trabajadores
|
||||
- Grandes empresas: más de 250 trabajadores
|
||||
|
||||
**Clasificación por sector:**
|
||||
- Sector primario: extracción de recursos naturales
|
||||
- Sector secundario: industria y manufactura
|
||||
- Sector terciario: servicios`
|
||||
},
|
||||
{
|
||||
titulo: 'El Estado o Gobierno',
|
||||
contenido: `El Estado interviene en la economía para corregir fallas del mercado, redistribuir ingresos y estabilizar la economía:
|
||||
|
||||
**Funciones económicas:**
|
||||
|
||||
1. **Función rectora o reguladora:**
|
||||
- Establece normas y leyes (ley de competencia, protección al consumidor)
|
||||
- Regula sectores estratégicos
|
||||
- Protege la propiedad intelectual
|
||||
|
||||
2. **Función productiva:**
|
||||
- Produce bienes y servicios públicos (educación, salud, defensa)
|
||||
- Gestiona empresas públicas
|
||||
|
||||
3. **Función redistributiva:**
|
||||
- Recauda impuestos
|
||||
- Transfiere recursos a quienes más lo necesitan
|
||||
- Proporciona seguridad social
|
||||
|
||||
4. **Función estabilizadora:**
|
||||
- Política fiscal (gasto e impuestos)
|
||||
- Política monetaria (a través del banco central)
|
||||
- Control de inflación y desempleo`
|
||||
},
|
||||
{
|
||||
titulo: 'El Sector Externo',
|
||||
contenido: `El sector externo comprende todas las transacciones económicas con el resto del mundo:
|
||||
|
||||
**Intercambios principales:**
|
||||
- **Exportaciones:** Bienes y servicios vendidos al exterior (generan entrada de divisas)
|
||||
- **Importaciones:** Bienes y servicios comprados del exterior (generan salida de divisas)
|
||||
|
||||
**Agentes del sector externo:**
|
||||
- Empresas multinacionales
|
||||
- Inversionistas extranjeros
|
||||
- Turistas
|
||||
- Organismos internacionales (FMI, Banco Mundial)
|
||||
|
||||
**Impacto económico:**
|
||||
- Aporta divisas necesarias para importaciones
|
||||
- Genera competencia para empresas locales
|
||||
- Transfiere tecnología y conocimiento
|
||||
- Crea empleo (zonas francas, exportaciones)
|
||||
|
||||
**Balanza comercial:**
|
||||
- Superávit: Exportaciones > Importaciones
|
||||
- Déficit: Importaciones > Exportaciones`
|
||||
},
|
||||
{
|
||||
titulo: 'El Flujo Circular de la Renta',
|
||||
contenido: `El flujo circular de la renta es un modelo que muestra cómo interactúan los agentes económicos y cómo circulan bienes, servicios y dinero en la economía.
|
||||
|
||||
**Flujos reales (bienes y servicios):**
|
||||
1. Familias → Empresas: Factores de producción (trabajo, capital, tierra)
|
||||
2. Empresas → Familias: Bienes y servicios para consumo
|
||||
|
||||
**Flujos monetarios (dinero):**
|
||||
1. Empresas → Familias: Pagos por factores (salarios, rentas, intereses, beneficios)
|
||||
2. Familias → Empresas: Gasto en consumo
|
||||
|
||||
**Inclusión del Estado:**
|
||||
- El Estado recauda impuestos de familias y empresas
|
||||
- El Estado gasta en bienes públicos y transferencias
|
||||
|
||||
**Inclusión del sector externo:**
|
||||
- Exportaciones: Dinero entra al país
|
||||
- Importaciones: Dinero sale del país
|
||||
|
||||
**Identidad macroeconómica básica:**
|
||||
Ingreso = Producción = Gasto`
|
||||
}
|
||||
],
|
||||
ejercicios: [
|
||||
{
|
||||
id: 'flujo-circular-juego',
|
||||
tipo: 'juego',
|
||||
titulo: 'Juego del Flujo Circular',
|
||||
descripcion: 'Arrastra cada elemento a su lugar correcto en el diagrama del flujo circular de la renta',
|
||||
config: {
|
||||
agentes: ['Familias', 'Empresas', 'Estado', 'Sector Externo'],
|
||||
flujos: [
|
||||
{ origen: 'Familias', destino: 'Empresas', tipo: 'factor', nombre: 'Trabajo' },
|
||||
{ origen: 'Empresas', destino: 'Familias', tipo: 'monetario', nombre: 'Salarios' },
|
||||
{ origen: 'Empresas', destino: 'Familias', tipo: 'real', nombre: 'Bienes' },
|
||||
{ origen: 'Familias', destino: 'Empresas', tipo: 'monetario', nombre: 'Consumo' },
|
||||
{ origen: 'Familias', destino: 'Estado', tipo: 'monetario', nombre: 'Impuestos' },
|
||||
{ origen: 'Estado', destino: 'Familias', tipo: 'monetario', nombre: 'Transferencias' }
|
||||
],
|
||||
dificultad: 'intermedio'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export default agentes;
|
||||
404
frontend/src/content/modulo1/ejercicios.ts
Normal file
404
frontend/src/content/modulo1/ejercicios.ts
Normal file
@@ -0,0 +1,404 @@
|
||||
import type { Ejercicio } from './introduccion';
|
||||
|
||||
export interface EjercicioDetallado extends Ejercicio {
|
||||
instrucciones: string;
|
||||
pistas?: string[];
|
||||
solucion?: string;
|
||||
dificultad: 'facil' | 'medio' | 'dificil';
|
||||
duracionEstimada: number; // en minutos
|
||||
objetivosAprendizaje: string[];
|
||||
}
|
||||
|
||||
export interface ModuloEjercicios {
|
||||
titulo: string;
|
||||
descripcion: string;
|
||||
ejercicios: EjercicioDetallado[];
|
||||
}
|
||||
|
||||
export const ejercicios: ModuloEjercicios = {
|
||||
titulo: 'Ejercicios Prácticos - Módulo 1',
|
||||
descripcion: 'Pon a prueba tus conocimientos con estos ejercicios interactivos sobre fundamentos de economía',
|
||||
ejercicios: [
|
||||
{
|
||||
id: 'simulador-disyuntivas',
|
||||
tipo: 'slider',
|
||||
titulo: 'Simulador de Disyuntivas Económicas',
|
||||
descripcion: 'Explora cómo una economía debe elegir entre producir diferentes bienes con recursos limitados',
|
||||
instrucciones: `En este ejercicio, juegas el rol de un planificador económico que debe decidir cómo asignar los recursos de una economía entre dos bienes: Alimentos y Tecnología.
|
||||
|
||||
1. Usa los sliders para ajustar la producción de cada bien
|
||||
2. Observa cómo la frontera de posibilidades de producción (FPP) muestra tus opciones
|
||||
3. Identifica los costos de oportunidad de cada decisión
|
||||
4. Experimenta con diferentes combinaciones y encuentra la asignación más eficiente
|
||||
|
||||
Recuerda: No puedes estar fuera de la frontera sin más recursos, y estar dentro significa ineficiencia.`,
|
||||
dificultad: 'medio',
|
||||
duracionEstimada: 15,
|
||||
objetivosAprendizaje: [
|
||||
'Comprender el concepto de escasez y elección',
|
||||
'Visualizar la frontera de posibilidades de producción',
|
||||
'Calcular costos de oportunidad',
|
||||
'Identificar puntos eficientes, ineficientes e inalcanzables'
|
||||
],
|
||||
config: {
|
||||
escenario: {
|
||||
titulo: 'Economía Agrícola-Tecnológica',
|
||||
descripcion: 'Una economía con recursos limitados debe decidir entre producir alimentos (bien de primera necesidad) o bienes tecnológicos (computadoras, smartphones)',
|
||||
bienA: {
|
||||
nombre: 'Alimentos',
|
||||
unidad: 'millones de toneladas',
|
||||
maxProduccion: 100,
|
||||
color: '#4CAF50'
|
||||
},
|
||||
bienB: {
|
||||
nombre: 'Tecnología',
|
||||
unidad: 'millones de unidades',
|
||||
maxProduccion: 80,
|
||||
color: '#2196F3'
|
||||
}
|
||||
},
|
||||
parametros: {
|
||||
mostrarFPP: true,
|
||||
mostrarCostoOportunidad: true,
|
||||
mostrarPuntoActual: true,
|
||||
tipoCurva: 'concava', // refleja costos crecientes
|
||||
puntosDesplazamiento: [
|
||||
{ causa: 'Mejora tecnológica en agricultura', efecto: 'fpp-externo-alimentos' },
|
||||
{ causa: 'Innovación tecnológica general', efecto: 'fpp-externo-ambos' }
|
||||
]
|
||||
},
|
||||
preguntasReflexion: [
|
||||
'¿Qué representa la pendiente de la FPP?',
|
||||
'¿Por qué la curva es cóncava y no una línea recta?',
|
||||
'¿Qué pasaría si la economía está en un punto dentro de la FPP?',
|
||||
'¿Cómo afectaría un terremoto a la FPP?'
|
||||
]
|
||||
},
|
||||
pistas: [
|
||||
'El costo de oportunidad es lo que sacrificas de un bien para obtener más del otro',
|
||||
'La FPP es cóncava porque los recursos no son perfectamente sustituibles entre sectores',
|
||||
'Un punto sobre la FPP es eficiente; dentro es ineficiente; fuera es inalcanzable'
|
||||
],
|
||||
solucion: `La FPP muestra que:
|
||||
1. Existe un trade-off: más alimentos significan menos tecnología y viceversa
|
||||
2. Los costos de oportunidad crecen conforme nos especializamos en un bien
|
||||
3. La eficiencia requiere estar sobre la frontera
|
||||
4. El crecimiento económico desplaza la FPP hacia afuera`
|
||||
},
|
||||
{
|
||||
id: 'quiz-clasificacion-bienes',
|
||||
tipo: 'quiz',
|
||||
titulo: 'Quiz: Clasificación de Bienes y Servicios',
|
||||
descripcion: 'Aprende a clasificar bienes según el comportamiento de la demanda ante cambios en el ingreso',
|
||||
instrucciones: `Clasifica cada bien en la categoría correcta según cómo responde su demanda ante cambios en el ingreso de los consumidores:
|
||||
|
||||
- **Bien Normal**: La demanda aumenta cuando aumenta el ingreso (ej: ropa de calidad, restaurantes)
|
||||
- **Bien Inferior**: La demanda disminuye cuando aumenta el ingreso (ej: fideos instantáneos, transporte público)
|
||||
- **Bien de Lujo**: La demanda aumenta más que proporcionalmente al ingreso (ej: joyas, autos deportivos)
|
||||
|
||||
Lee cuidadosamente cada escenario y selecciona la respuesta correcta.`,
|
||||
dificultad: 'facil',
|
||||
duracionEstimada: 10,
|
||||
objetivosAprendizaje: [
|
||||
'Distinguir entre bienes normales, inferiores y de lujo',
|
||||
'Comprender la elasticidad ingreso de la demanda',
|
||||
'Analizar patrones de consumo según nivel de ingresos'
|
||||
],
|
||||
config: {
|
||||
modo: 'clasificacion-multiple',
|
||||
preguntas: [
|
||||
{
|
||||
id: 'p1',
|
||||
bien: 'Carne de primera calidad',
|
||||
descripcion: 'Carne de res premium vendida en supermercados de alta gama',
|
||||
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
|
||||
respuestaCorrecta: 'Bien de lujo',
|
||||
explicacionDetallada: 'La carne premium es considerada un bien de lujo porque cuando el ingreso aumenta significativamente, las familias aumentan su consumo de este tipo de carne sustituyendo carnes de menor calidad.',
|
||||
categoriaElasticidad: 'Elasticidad ingreso > 1'
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
bien: 'Pan',
|
||||
descripcion: 'Pan básico de consumo diario',
|
||||
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
|
||||
respuestaCorrecta: 'Bien normal',
|
||||
explicacionDetallada: 'El pan es un bien normal porque su consumo aumenta moderadamente con el ingreso, aunque llega un punto donde se estabiliza (saturación).',
|
||||
categoriaElasticidad: '0 < Elasticidad ingreso < 1'
|
||||
},
|
||||
{
|
||||
id: 'p3',
|
||||
bien: 'Transporte público (autobús)',
|
||||
descripcion: 'Servicio de autobuses urbanos',
|
||||
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
|
||||
respuestaCorrecta: 'Bien inferior',
|
||||
explicacionDetallada: 'El transporte público es un bien inferior porque cuando los ingresos aumentan, las personas tienden a comprar automóviles o usar taxis/Uber, reduciendo el uso del autobús.',
|
||||
categoriaElasticidad: 'Elasticidad ingreso < 0'
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
bien: 'Fideos instantáneos',
|
||||
descripcion: 'Comida rápida económica',
|
||||
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
|
||||
respuestaCorrecta: 'Bien inferior',
|
||||
explicacionDetallada: 'Los fideos instantáneos son claramente un bien inferior. A medida que aumentan los ingresos, las personas prefieren alimentos más nutritivos y de mejor calidad.',
|
||||
categoriaElasticidad: 'Elasticidad ingreso < 0'
|
||||
},
|
||||
{
|
||||
id: 'p5',
|
||||
bien: 'Vacaciones en el extranjero',
|
||||
descripcion: 'Viajes turísticos internacionales',
|
||||
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
|
||||
respuestaCorrecta: 'Bien de lujo',
|
||||
explicacionDetallada: 'Las vacaciones internacionales son un bien de lujo porque su consumo aumenta significativamente cuando el ingreso crece, incluso más que proporcionalmente.',
|
||||
categoriaElasticidad: 'Elasticidad ingreso > 1'
|
||||
},
|
||||
{
|
||||
id: 'p6',
|
||||
bien: 'Ropa de marca',
|
||||
descripcion: 'Vestimenta de diseñador',
|
||||
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
|
||||
respuestaCorrecta: 'Bien de lujo',
|
||||
explicacionDetallada: 'La ropa de marca es un bien de lujo porque su demanda crece más rápido que el ingreso, especialmente en rangos de ingreso altos.',
|
||||
categoriaElasticidad: 'Elasticidad ingreso > 1'
|
||||
},
|
||||
{
|
||||
id: 'p7',
|
||||
bien: 'Cine',
|
||||
descripcion: 'Entradas a salas de cine',
|
||||
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
|
||||
respuestaCorrecta: 'Bien normal',
|
||||
explicacionDetallada: 'El cine es un bien normal. Aunque con el auge del streaming podría debatirse, generalmente el consumo de entretenimiento aumenta con el ingreso de forma moderada.',
|
||||
categoriaElasticidad: '0 < Elasticidad ingreso < 1'
|
||||
},
|
||||
{
|
||||
id: 'p8',
|
||||
bien: 'Productos de marca blanca',
|
||||
descripcion: 'Productos genéricos de supermercado',
|
||||
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
|
||||
respuestaCorrecta: 'Bien inferior',
|
||||
explicacionDetallada: 'Los productos de marca blanca son bienes inferiores porque son sustituidos por marcas reconocidas cuando el consumidor tiene mayores ingresos.',
|
||||
categoriaElasticidad: 'Elasticidad ingreso < 0'
|
||||
}
|
||||
],
|
||||
configuracionVisual: {
|
||||
mostrarBarraProgreso: true,
|
||||
mostrarPuntaje: true,
|
||||
retroalimentacionInmediata: true,
|
||||
tiempoLimite: 600, // segundos
|
||||
permitirReintentar: true
|
||||
},
|
||||
nivelesDificultad: {
|
||||
facil: ['p2', 'p4', 'p7'],
|
||||
medio: ['p3', 'p5', 'p8'],
|
||||
dificil: ['p1', 'p6']
|
||||
}
|
||||
},
|
||||
pistas: [
|
||||
'Pregúntate: ¿Qué compraría una persona si ganara el doble de dinero?',
|
||||
'Los bienes de lujo son aquellos que dejarías de comprar primero si perdieras tu empleo',
|
||||
'Un bien inferior no significa que sea de mala calidad, sino que tiene sustitutos mejores cuando aumenta el ingreso'
|
||||
],
|
||||
solucion: `Clasificación según elasticidad-ingreso:
|
||||
- Bien Inferior: Elasticidad < 0 (ej: transporte público, fideos)
|
||||
- Bien Normal: 0 < Elasticidad < 1 (ej: pan, cine)
|
||||
- Bien de Lujo: Elasticidad > 1 (ej: carne premium, vacaciones)`
|
||||
},
|
||||
{
|
||||
id: 'juego-flujo-circular',
|
||||
tipo: 'juego',
|
||||
titulo: 'Juego: El Flujo Circular de la Renta',
|
||||
descripcion: 'Coloca cada elemento en su lugar correcto dentro del modelo del flujo circular',
|
||||
instrucciones: `Completa el diagrama del flujo circular de la renta arrastrando cada elemento a su posición correcta.
|
||||
|
||||
El flujo circular muestra cómo interactúan los agentes económicos:
|
||||
|
||||
**Agentes principales:**
|
||||
1. **Familias/Hogares**: Ofrecen factores productivos (trabajo, capital) y consumen bienes
|
||||
2. **Empresas**: Producen bienes/servicios y demandan factores productivos
|
||||
3. **Estado**: Recauda impuestos y realiza gastos públicos
|
||||
4. **Sector Externo**: Intercambia bienes y servicios con el exterior
|
||||
|
||||
**Tipos de flujos:**
|
||||
- **Flujos reales** (flechas azules): Bienes, servicios, factores productivos
|
||||
- **Flujos monetarios** (flechas verdes): Dinero, pagos, transferencias
|
||||
|
||||
Instrucciones:
|
||||
1. Observa los elementos en la parte inferior
|
||||
2. Arrastra cada uno al círculo correspondiente o a las flechas correctas
|
||||
3. Asegúrate de distinguir entre flujos reales y monetarios
|
||||
4. Completa todos los elementos para ganar`,
|
||||
dificultad: 'dificil',
|
||||
duracionEstimada: 20,
|
||||
objetivosAprendizaje: [
|
||||
'Comprender la interdependencia entre agentes económicos',
|
||||
'Diferenciar entre flujos reales y monetarios',
|
||||
'Identificar los pagos correspondientes a cada factor productivo',
|
||||
'Entender el papel del Estado y el sector externo'
|
||||
],
|
||||
config: {
|
||||
tipoJuego: 'drag-and-drop',
|
||||
diagrama: {
|
||||
agentes: [
|
||||
{
|
||||
id: 'familias',
|
||||
nombre: 'FAMILIAS',
|
||||
posicion: 'izquierda',
|
||||
icono: '👨👩👧👦',
|
||||
color: '#4CAF50'
|
||||
},
|
||||
{
|
||||
id: 'empresas',
|
||||
nombre: 'EMPRESAS',
|
||||
posicion: 'derecha',
|
||||
icono: '🏭',
|
||||
color: '#2196F3'
|
||||
},
|
||||
{
|
||||
id: 'estado',
|
||||
nombre: 'ESTADO',
|
||||
posicion: 'arriba',
|
||||
icono: '🏛️',
|
||||
color: '#FF9800'
|
||||
},
|
||||
{
|
||||
id: 'sector-externo',
|
||||
nombre: 'SECTOR EXTERNO',
|
||||
posicion: 'abajo',
|
||||
icono: '🌍',
|
||||
color: '#9C27B0'
|
||||
}
|
||||
],
|
||||
flujos: [
|
||||
// Flujo real superior (Familias → Empresas)
|
||||
{
|
||||
id: 'flujo1',
|
||||
origen: 'familias',
|
||||
destino: 'empresas',
|
||||
tipo: 'real',
|
||||
elementosCorrectos: ['trabajo', 'tierra', 'capital']
|
||||
},
|
||||
// Flujo monetario superior (Empresas → Familias)
|
||||
{
|
||||
id: 'flujo2',
|
||||
origen: 'empresas',
|
||||
destino: 'familias',
|
||||
tipo: 'monetario',
|
||||
elementosCorrectos: ['salarios', 'renta', 'intereses']
|
||||
},
|
||||
// Flujo real inferior (Empresas → Familias)
|
||||
{
|
||||
id: 'flujo3',
|
||||
origen: 'empresas',
|
||||
destino: 'familias',
|
||||
tipo: 'real',
|
||||
elementosCorrectos: ['bienes', 'servicios']
|
||||
},
|
||||
// Flujo monetario inferior (Familias → Empresas)
|
||||
{
|
||||
id: 'flujo4',
|
||||
origen: 'familias',
|
||||
destino: 'empresas',
|
||||
tipo: 'monetario',
|
||||
elementosCorrectos: ['gasto', 'consumo']
|
||||
},
|
||||
// Flujos del Estado
|
||||
{
|
||||
id: 'flujo5',
|
||||
origen: 'familias',
|
||||
destino: 'estado',
|
||||
tipo: 'monetario',
|
||||
elementosCorrectos: ['impuestos', 'impuestos-directos']
|
||||
},
|
||||
{
|
||||
id: 'flujo6',
|
||||
origen: 'estado',
|
||||
destino: 'familias',
|
||||
tipo: 'monetario',
|
||||
elementosCorrectos: ['transferencias', 'subsidios']
|
||||
},
|
||||
{
|
||||
id: 'flujo7',
|
||||
origen: 'estado',
|
||||
destino: 'empresas',
|
||||
tipo: 'monetario',
|
||||
elementosCorrectos: ['gasto-publico', 'compras-estado']
|
||||
}
|
||||
],
|
||||
elementosArrastrables: [
|
||||
{ id: 'trabajo', texto: '💪 Trabajo', tipo: 'real', categoria: 'factor' },
|
||||
{ id: 'tierra', texto: '🌾 Tierra', tipo: 'real', categoria: 'factor' },
|
||||
{ id: 'capital', texto: '💰 Capital', tipo: 'real', categoria: 'factor' },
|
||||
{ id: 'salarios', texto: '💵 Salarios', tipo: 'monetario', categoria: 'pago' },
|
||||
{ id: 'renta', texto: '🏠 Renta', tipo: 'monetario', categoria: 'pago' },
|
||||
{ id: 'intereses', texto: '📈 Intereses', tipo: 'monetario', categoria: 'pago' },
|
||||
{ id: 'bienes', texto: '📦 Bienes', tipo: 'real', categoria: 'producto' },
|
||||
{ id: 'servicios', texto: '🔧 Servicios', tipo: 'real', categoria: 'producto' },
|
||||
{ id: 'gasto', texto: '💳 Gasto', tipo: 'monetario', categoria: 'pago' },
|
||||
{ id: 'consumo', texto: '🛒 Consumo', tipo: 'monetario', categoria: 'pago' },
|
||||
{ id: 'impuestos', texto: '📝 Impuestos', tipo: 'monetario', categoria: 'pago-estado' },
|
||||
{ id: 'impuestos-directos', texto: '📋 Imp. Directos', tipo: 'monetario', categoria: 'pago-estado' },
|
||||
{ id: 'transferencias', texto: '🎁 Transferencias', tipo: 'monetario', categoria: 'transferencia' },
|
||||
{ id: 'subsidios', texto: '💸 Subsidios', tipo: 'monetario', categoria: 'transferencia' },
|
||||
{ id: 'gasto-publico', texto: '🏗️ Gasto Público', tipo: 'monetario', categoria: 'gobierno' },
|
||||
{ id: 'compras-estado', texto: '🛍️ Compras Estado', tipo: 'monetario', categoria: 'gobierno' }
|
||||
]
|
||||
},
|
||||
niveles: [
|
||||
{
|
||||
nombre: 'Básico',
|
||||
descripcion: 'Solo Familias y Empresas',
|
||||
agentesActivos: ['familias', 'empresas'],
|
||||
elementosDisponibles: ['trabajo', 'salarios', 'bienes', 'gasto', 'servicios', 'consumo'],
|
||||
ayudaMaxima: true
|
||||
},
|
||||
{
|
||||
nombre: 'Intermedio',
|
||||
descripcion: 'Incluye al Estado',
|
||||
agentesActivos: ['familias', 'empresas', 'estado'],
|
||||
elementosDisponibles: ['trabajo', 'tierra', 'capital', 'salarios', 'renta', 'intereses', 'bienes', 'servicios', 'gasto', 'consumo', 'impuestos', 'transferencias', 'gasto-publico'],
|
||||
ayudaMaxima: false
|
||||
},
|
||||
{
|
||||
nombre: 'Avanzado',
|
||||
descripcion: 'Todos los agentes incluyendo Sector Externo',
|
||||
agentesActivos: ['familias', 'empresas', 'estado', 'sector-externo'],
|
||||
elementosDisponibles: 'todos',
|
||||
incluirExportacionesImportaciones: true,
|
||||
ayudaMaxima: false
|
||||
}
|
||||
],
|
||||
sistemaPuntuacion: {
|
||||
acierto: 10,
|
||||
error: -2,
|
||||
bonusCompletitud: 50,
|
||||
tiempoBonus: true
|
||||
}
|
||||
},
|
||||
pistas: [
|
||||
'Las familias venden sus factores productivos (trabajo, tierra, capital) a las empresas',
|
||||
'Las empresas pagan salarios por trabajo, renta por tierra e intereses por capital',
|
||||
'Las familias gastan dinero para comprar bienes y servicios de las empresas',
|
||||
'El Estado recauda impuestos y redistribuye mediante transferencias y gasto público',
|
||||
'Distingue flujos reales (cosas físicas) de flujos monetarios (dinero)'
|
||||
],
|
||||
solucion: `El flujo circular completo:
|
||||
|
||||
**Flujos reales:**
|
||||
- Familias → Empresas: Trabajo, tierra, capital (factores productivos)
|
||||
- Empresas → Familias: Bienes y servicios
|
||||
|
||||
**Flujos monetarios:**
|
||||
- Empresas → Familias: Salarios, renta, intereses (pagos por factores)
|
||||
- Familias → Empresas: Gasto de consumo
|
||||
- Familias → Estado: Impuestos
|
||||
- Estado → Familias/Empresas: Transferencias y gasto público`
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Exportar también los ejercicios individuales para facilitar importaciones selectivas
|
||||
export const ejercicioDisyuntivas = ejercicios.ejercicios[0];
|
||||
export const ejercicioClasificacion = ejercicios.ejercicios[1];
|
||||
export const ejercicioFlujoCircular = ejercicios.ejercicios[2];
|
||||
|
||||
export default ejercicios;
|
||||
192
frontend/src/content/modulo1/factores.ts
Normal file
192
frontend/src/content/modulo1/factores.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import type { ModuloContenido } from './introduccion';
|
||||
|
||||
export const factores: ModuloContenido = {
|
||||
titulo: 'Factores de Producción',
|
||||
contenido: [
|
||||
{
|
||||
titulo: 'La Tierra (Recursos Naturales)',
|
||||
contenido: `La tierra como factor de producción incluye todos los recursos naturales proporcionados por la naturaleza:
|
||||
|
||||
**Características:**
|
||||
- Tierra en sentido estricto (superficie territorial)
|
||||
- Recursos minerales (petróleo, gas, minerales metálicos)
|
||||
- Recursos hídricos (ríos, lagos, aguas subterráneas)
|
||||
- Recursos forestales (madera, productos forestales)
|
||||
- Recursos marinos (pesca)
|
||||
- Recursos energéticos naturales (radiación solar, eólica)
|
||||
|
||||
**Remuneración:**
|
||||
El factor tierra recibe la **renta** o **renta de la tierra** como pago por su uso.
|
||||
|
||||
**Importancia económica:**
|
||||
- Los recursos naturales son la base de muchas industrias
|
||||
- Países ricos en recursos naturales tienen ventajas comparativas
|
||||
- La explotación sostenible garantiza recursos para futuras generaciones
|
||||
- El agotamiento de recursos no renovables crea presión sobre la economía`
|
||||
},
|
||||
{
|
||||
titulo: 'El Trabajo (Factor Humano)',
|
||||
contenido: `El trabajo es el esfuerzo humano (físico y mental) aplicado a la producción de bienes y servicios:
|
||||
|
||||
**Clasificación del trabajo:**
|
||||
|
||||
**Por cualificación:**
|
||||
- Trabajo no calificado: no requiere formación especial
|
||||
- Trabajo semi-calificado: requiere entrenamiento básico
|
||||
- Trabajo calificado: requiere educación especializada
|
||||
- Trabajo altamente calificado: profesionales, técnicos especializados
|
||||
|
||||
**Por sector:**
|
||||
- Trabajo primario: agricultura, pesca, minería
|
||||
- Trabajo secundario: industria, manufactura, construcción
|
||||
- Trabajo terciario: servicios, comercio, administración
|
||||
|
||||
**Remuneración:**
|
||||
El trabajo recibe el **salario** como pago (puede ser por hora, pieza o mensualidad).
|
||||
|
||||
**Características del mercado laboral:**
|
||||
- Oferta de trabajo: personas dispuestas a trabajar
|
||||
- Demanda de trabajo: empresas que necesitan contratar
|
||||
- Desempleo: diferencia entre oferta y demanda efectiva`
|
||||
},
|
||||
{
|
||||
titulo: 'El Capital',
|
||||
contenido: `El capital son los bienes de producción creados por el ser humano para producir otros bienes y servicios:
|
||||
|
||||
**Tipos de capital:**
|
||||
|
||||
**1. Capital físico o tangible:**
|
||||
- Maquinaria y equipos
|
||||
- Edificios e instalaciones
|
||||
- Herramientas y vehículos
|
||||
- Infraestructura (carreteras, puertos, redes)
|
||||
|
||||
**2. Capital financiero:**
|
||||
- Dinero disponible para inversión
|
||||
- Créditos y préstamos
|
||||
- Acciones y bonos
|
||||
|
||||
**3. Capital humano:**
|
||||
- Educación y formación de los trabajadores
|
||||
- Experiencia y habilidades
|
||||
- Salud de la población
|
||||
|
||||
**Formación de capital:**
|
||||
El capital se forma mediante el **ahorro** e **inversión**. El ahorro diferido del consumo actual permite invertir en bienes de capital que aumentarán la producción futura.
|
||||
|
||||
**Remuneración:**
|
||||
El capital recibe el **interés** como pago por su uso.
|
||||
|
||||
**Importancia:**
|
||||
El capital aumenta la productividad del trabajo, permitiendo producir más con menos esfuerzo.`
|
||||
},
|
||||
{
|
||||
titulo: 'Tecnología y Emprendimiento',
|
||||
contenido: `Además de los tres factores clásicos, la economía moderna reconoce dos factores adicionales fundamentales:
|
||||
|
||||
**Tecnología:**
|
||||
Es el conocimiento aplicado a la producción. No es solo máquinas, sino el "saber hacer":
|
||||
|
||||
- Procesos productivos más eficientes
|
||||
- Innovaciones en productos y servicios
|
||||
- Software y sistemas de información
|
||||
- Metodologías de organización
|
||||
|
||||
**Impacto de la tecnología:**
|
||||
- Aumenta la productividad total de los factores
|
||||
- Reduce costos de producción
|
||||
- Crea nuevos productos y mercados
|
||||
- Transforma industrias enteras (disrupción digital)
|
||||
|
||||
**Emprendimiento:**
|
||||
Es la capacidad de organizar y coordinar los otros factores de producción para crear valor:
|
||||
|
||||
**Funciones del empresario:**
|
||||
- Identificar oportunidades de negocio
|
||||
- Asumir riesgos económicos
|
||||
- Innovar (nuevos productos, métodos, mercados)
|
||||
- Tomar decisiones estratégicas
|
||||
- Organizar los factores productivos
|
||||
|
||||
**Remuneración:**
|
||||
El empresario recibe los **beneficios** (o pérdidas) como resultado de su actividad.
|
||||
|
||||
**Diferencia entre empresario y capitalista:**
|
||||
- El capitalista aporta capital y recibe intereses
|
||||
- El empresario organiza la producción y recibe beneficios (que incluyen compensación por su trabajo, riesgo asumido y habilidad empresarial)`
|
||||
},
|
||||
{
|
||||
titulo: 'Productividad y Eficiencia',
|
||||
contenido: `La combinación de factores de producción debe hacerse buscando la máxima eficiencia:
|
||||
|
||||
**Productividad:**
|
||||
Relación entre la producción obtenida y los recursos utilizados:
|
||||
|
||||
Productividad = Producción / Factores utilizados
|
||||
|
||||
**Tipos de productividad:**
|
||||
- Productividad del trabajo: producción por hora trabajada
|
||||
- Productividad del capital: producción por unidad de capital
|
||||
- Productividad total de los factores: eficiencia global
|
||||
|
||||
**Eficiencia técnica vs económica:**
|
||||
|
||||
**Eficiencia técnica:**
|
||||
Producir la máxima cantidad posible con los recursos disponibles (no desperdiciar inputs).
|
||||
|
||||
**Eficiencia económica:**
|
||||
Producir al menor costo posible, considerando los precios de los factores.
|
||||
|
||||
**Retornos a escala:**
|
||||
- Crecientes: duplicar factores más que duplica la producción
|
||||
- Constantes: duplicar factores duplica exactamente la producción
|
||||
- Decrecientes: duplicar factores aumenta menos que el doble la producción`
|
||||
}
|
||||
],
|
||||
ejercicios: [
|
||||
{
|
||||
id: 'quiz-bienes',
|
||||
tipo: 'quiz',
|
||||
titulo: 'Quiz de Clasificación de Bienes',
|
||||
descripcion: 'Clasifica los siguientes bies según su tipo: normal, inferior o de lujo',
|
||||
config: {
|
||||
preguntas: [
|
||||
{
|
||||
bien: 'Un automóvil de lujo',
|
||||
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
|
||||
respuestaCorrecta: 'Bien de lujo',
|
||||
explicacion: 'Los automóviles de lujo aumentan su demanda cuando aumenta el ingreso más que proporcionalmente'
|
||||
},
|
||||
{
|
||||
bien: 'Transporte público',
|
||||
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
|
||||
respuestaCorrecta: 'Bien inferior',
|
||||
explicacion: 'Cuando los ingresos aumentan, las personas tienden a sustituir el transporte público por automóviles privados'
|
||||
},
|
||||
{
|
||||
bien: 'Pan',
|
||||
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
|
||||
respuestaCorrecta: 'Bien normal',
|
||||
explicacion: 'El pan es un bien básico cuya demanda aumenta moderadamente con el ingreso'
|
||||
},
|
||||
{
|
||||
bien: 'Un yate',
|
||||
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
|
||||
respuestaCorrecta: 'Bien de lujo',
|
||||
explicacion: 'Los yates son bienes exclusivos que solo son accesibles con altos ingresos'
|
||||
},
|
||||
{
|
||||
bien: 'Productos de marca genérica',
|
||||
opciones: ['Bien normal', 'Bien inferior', 'Bien de lujo'],
|
||||
respuestaCorrecta: 'Bien inferior',
|
||||
explicacion: 'Las marcas genéricas son sustituidas por marcas premium cuando aumentan los ingresos'
|
||||
}
|
||||
],
|
||||
tiempoLimite: 300,
|
||||
mostrarRetroalimentacion: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export default factores;
|
||||
65
frontend/src/content/modulo1/index.ts
Normal file
65
frontend/src/content/modulo1/index.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Módulo 1: Fundamentos de Economía
|
||||
*
|
||||
* Este módulo cubre los conceptos básicos de economía, incluyendo:
|
||||
* - Definición y ramas de la economía
|
||||
* - Agentes económicos y su interacción
|
||||
* - Factores de producción
|
||||
* - Frontera de posibilidades de producción
|
||||
* - Flujo circular de la renta
|
||||
*/
|
||||
|
||||
// Importar valores para uso local
|
||||
import { introduccion } from './introduccion';
|
||||
import modulo1Introduccion from './introduccion';
|
||||
import { agentes } from './agentes';
|
||||
import modulo1Agentes from './agentes';
|
||||
import { factores } from './factores';
|
||||
import modulo1Factores from './factores';
|
||||
import {
|
||||
ejercicios,
|
||||
ejercicioDisyuntivas,
|
||||
ejercicioClasificacion,
|
||||
ejercicioFlujoCircular
|
||||
} from './ejercicios';
|
||||
import modulo1Ejercicios from './ejercicios';
|
||||
|
||||
// Importar tipos para reexportar
|
||||
import type {
|
||||
EjercicioDetallado,
|
||||
ModuloEjercicios
|
||||
} from './ejercicios';
|
||||
|
||||
// Reexportar tipos
|
||||
export type {
|
||||
EjercicioDetallado,
|
||||
ModuloEjercicios
|
||||
};
|
||||
|
||||
// Exportar todo el módulo como un objeto consolidado
|
||||
export const modulo1 = {
|
||||
id: 'modulo-1',
|
||||
titulo: 'Fundamentos de Economía',
|
||||
descripcion: 'Introducción a los conceptos básicos de economía, agentes económicos, factores de producción y análisis de disyuntivas.',
|
||||
duracionEstimada: '4-5 horas',
|
||||
temas: [
|
||||
introduccion,
|
||||
agentes,
|
||||
factores
|
||||
],
|
||||
ejercicios: ejercicios.ejercicios
|
||||
};
|
||||
|
||||
// Reexportar contenidos para compatibilidad
|
||||
export { introduccion, modulo1Introduccion };
|
||||
export { agentes, modulo1Agentes };
|
||||
export { factores, modulo1Factores };
|
||||
export {
|
||||
ejercicios,
|
||||
ejercicioDisyuntivas,
|
||||
ejercicioClasificacion,
|
||||
ejercicioFlujoCircular,
|
||||
modulo1Ejercicios
|
||||
};
|
||||
|
||||
export default modulo1;
|
||||
81
frontend/src/content/modulo1/introduccion.ts
Normal file
81
frontend/src/content/modulo1/introduccion.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
export interface Seccion {
|
||||
titulo: string;
|
||||
contenido: string;
|
||||
}
|
||||
|
||||
export interface Ejercicio {
|
||||
id: string;
|
||||
tipo: 'slider' | 'quiz' | 'juego';
|
||||
titulo: string;
|
||||
descripcion: string;
|
||||
config: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ModuloContenido {
|
||||
titulo: string;
|
||||
contenido: Seccion[];
|
||||
ejercicios: Ejercicio[];
|
||||
}
|
||||
|
||||
export const introduccion: ModuloContenido = {
|
||||
titulo: 'Introducción a la Economía',
|
||||
contenido: [
|
||||
{
|
||||
titulo: '¿Qué es la Economía?',
|
||||
contenido: `La economía es la ciencia social que estudia cómo los individuos, empresas y gobiernos toman decisiones sobre la asignación de recursos escasos para satisfacer necesidades ilimitadas. El término proviene del griego "oikonomia" (gestión del hogar).
|
||||
|
||||
Los recursos disponibles (tierra, trabajo, capital) son limitados, mientras que las necesidades humanas son infinitas. Esta tensión entre lo que queremos y lo que podemos obtener constituye el problema fundamental de la economía.`
|
||||
},
|
||||
{
|
||||
titulo: 'Microeconomía vs Macroeconomía',
|
||||
contenido: `La economía se divide en dos grandes ramas:
|
||||
|
||||
**Microeconomía**: Estudia el comportamiento de agentes económicos individuales (hogares, empresas, mercados específicos). Analiza decisiones como: ¿Cuánto producirá una empresa? ¿Qué cantidad comprará un consumidor? ¿Cómo se determina el precio de un bien?
|
||||
|
||||
**Macroeconomía**: Examina el funcionamiento de la economía en su conjunto. Estudia variables agregadas como el Producto Interno Bruto (PIB), la inflación, el desempleo y el crecimiento económico. Busca entender los ciclos económicos y las políticas para estabilizar la economía.`
|
||||
},
|
||||
{
|
||||
titulo: 'Las Tres Preguntas Fundamentales',
|
||||
contenido: `Toda sociedad debe responder tres preguntas económicas básicas:
|
||||
|
||||
1. **¿Qué producir?**: Determinar qué bienes y servicios se fabricarán dados los recursos limitados. ¿Más comida o más ropa? ¿Más hospitales o más escuelas?
|
||||
|
||||
2. **¿Cómo producir?**: Elegir la combinación de factores de producción más eficiente. ¿Usar más trabajo manual o más maquinaria? ¿Tecnología intensiva o labor intensiva?
|
||||
|
||||
3. **¿Para quién producir?**: Distribuir los bienes y servicios entre la población. ¿Quién recibe qué? ¿Basado en la capacidad de pago o en la necesidad?`
|
||||
},
|
||||
{
|
||||
titulo: 'La Frontera de Posibilidades de Producción (FPP)',
|
||||
contenido: `La Frontera de Posibilidades de Producción (o Curva de Transformación) representa gráficamente las combinaciones máximas de dos bienes que una economía puede producir utilizando todos sus recursos y tecnología disponibles de manera eficiente.
|
||||
|
||||
**Características importantes:**
|
||||
- **Pendiente negativa**: Para producir más de un bien, debemos sacrificar algo del otro (costo de oportunidad)
|
||||
- **Forma cóncava**: Los costos de oportunidad crecientes reflejan que los recursos no son perfectamente adaptables
|
||||
- **Puntos sobre la curva**: Producción eficiente (todos los recursos utilizados)
|
||||
- **Puntos dentro de la curva**: Ineficiencia o desempleo de recursos
|
||||
- **Puntos fuera de la curva**: Inalcanzables con los recursos actuales
|
||||
|
||||
**Desplazamientos de la FPP:**
|
||||
- Hacia afuera: Crecimiento económico (más recursos o mejor tecnología)
|
||||
- Hacia adentro: Destrucción de recursos o regresión tecnológica`
|
||||
}
|
||||
],
|
||||
ejercicios: [
|
||||
{
|
||||
id: 'fpp-simulador',
|
||||
tipo: 'slider',
|
||||
titulo: 'Simulador de Disyuntivas',
|
||||
descripcion: 'Ajusta los sliders para ver cómo la producción de dos bienes compite por recursos limitados',
|
||||
config: {
|
||||
bienes: [
|
||||
{ nombre: 'Bien de Consumo', max: 100 },
|
||||
{ nombre: 'Bien de Capital', max: 100 }
|
||||
],
|
||||
mostrarCostoOportunidad: true,
|
||||
mostrarFPP: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export default introduccion;
|
||||
369
frontend/src/content/modulo2/demanda.ts
Normal file
369
frontend/src/content/modulo2/demanda.ts
Normal file
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* Módulo 2: Ley de la Demanda
|
||||
*
|
||||
* Este módulo cubre los fundamentos de la demanda en economía,
|
||||
* incluyendo la ley de la demanda, factores determinantes y
|
||||
* tipos de curvas de demanda.
|
||||
*/
|
||||
|
||||
// ============================================
|
||||
// TIPOS Y ENUMERACIONES
|
||||
// ============================================
|
||||
|
||||
export type TipoBien = 'normal' | 'inferior' | 'lujo' | 'necesidad';
|
||||
|
||||
export type TipoRelacionPrecio = 'sustituto' | 'complementario' | 'independiente';
|
||||
|
||||
export enum DireccionDesplazamiento {
|
||||
IZQUIERDA = 'izquierda', // Disminución de demanda
|
||||
DERECHA = 'derecha', // Aumento de demanda
|
||||
NINGUNO = 'ninguno' // Sin cambio
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// INTERFACES
|
||||
// ============================================
|
||||
|
||||
export interface PuntoDemanda {
|
||||
precio: number;
|
||||
cantidad: number;
|
||||
}
|
||||
|
||||
export interface CurvaDemanda {
|
||||
id: string;
|
||||
nombre: string;
|
||||
puntos: PuntoDemanda[];
|
||||
descripcion: string;
|
||||
}
|
||||
|
||||
export interface FactorDesplazamiento {
|
||||
nombre: string;
|
||||
descripcion: string;
|
||||
direccion: DireccionDesplazamiento;
|
||||
ejemplo: string;
|
||||
icono: string;
|
||||
}
|
||||
|
||||
export interface EjemploDemanda {
|
||||
titulo: string;
|
||||
bien: string;
|
||||
escenario: string;
|
||||
explicacion: string;
|
||||
graficoData: PuntoDemanda[];
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// CONTENIDO TEÓRICO
|
||||
// ============================================
|
||||
|
||||
export const definicionDemanda = {
|
||||
titulo: 'Definición de Demanda',
|
||||
definicion: 'La demanda es la cantidad de un bien o servicio que los consumidores están dispuestos y pueden comprar a diferentes precios durante un período específico, manteniendo constantes otros factores (ceteris paribus).',
|
||||
|
||||
elementosClave: [
|
||||
{
|
||||
elemento: 'Disposición a comprar',
|
||||
descripcion: 'El consumidor debe querer adquirir el bien (preferencia)'
|
||||
},
|
||||
{
|
||||
elemento: 'Capacidad de compra',
|
||||
descripcion: 'El consumidor debe tener los recursos necesarios (ingreso)'
|
||||
},
|
||||
{
|
||||
elemento: 'Precios variables',
|
||||
descripcion: 'Se analiza la relación a diferentes niveles de precio'
|
||||
},
|
||||
{
|
||||
elemento: 'Período de tiempo',
|
||||
descripcion: 'La demanda siempre se refiere a un período específico'
|
||||
}
|
||||
],
|
||||
|
||||
diferenciaDeseo: {
|
||||
deseo: 'Quiero un auto de lujo (sin capacidad de compra)',
|
||||
demanda: 'Puedo y quiero comprar 2 litros de leche semanales a $2 cada uno'
|
||||
}
|
||||
};
|
||||
|
||||
export const leyDemanda = {
|
||||
titulo: 'Ley de la Demanda',
|
||||
|
||||
enunciado: 'Existe una relación inversa entre el precio de un bien y la cantidad demandada: cuando el precio aumenta, la cantidad demandada disminuye, y viceversa.',
|
||||
|
||||
explicacion: 'Esta relación inversa ocurre por dos efectos principales:',
|
||||
|
||||
efectos: [
|
||||
{
|
||||
nombre: 'Efecto Sustitución',
|
||||
descripcion: 'Cuando el precio de un bien aumenta, los consumidores sustituyen hacia bienes alternativos más baratos que satisfacen necesidades similares.',
|
||||
ejemplo: 'Si el precio de la carne de res sube, los consumidores compran más pollo o pescado.'
|
||||
},
|
||||
{
|
||||
nombre: 'Efecto Ingreso',
|
||||
descripcion: 'Cuando el precio aumenta, el poder adquisitivo real del consumidor disminuye, permitiéndole comprar menos cantidad del bien.',
|
||||
ejemplo: 'Si el precio de la gasolina sube, con el mismo presupuesto puedo comprar menos litros.'
|
||||
}
|
||||
],
|
||||
|
||||
representacionMatematica: {
|
||||
funcion: 'Qd = f(P)',
|
||||
donde: {
|
||||
Qd: 'Cantidad demandada',
|
||||
P: 'Precio del bien',
|
||||
f: 'Función decreciente (pendiente negativa)'
|
||||
},
|
||||
ejemploLineal: 'Qd = 100 - 2P',
|
||||
interpretacion: 'Por cada aumento de $1 en el precio, la cantidad demandada disminuye en 2 unidades.'
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// FACTORES QUE DESPLAZAN LA CURVA DE DEMANDA
|
||||
// ============================================
|
||||
|
||||
export const factoresDesplazamiento: FactorDesplazamiento[] = [
|
||||
{
|
||||
nombre: 'Ingreso del consumidor',
|
||||
descripcion: 'Cambios en el ingreso disponible de los consumidores',
|
||||
direccion: DireccionDesplazamiento.DERECHA,
|
||||
ejemplo: 'Un aumento de sueldo permite comprar más restaurantes (bien normal) o menos fideos instantáneos (bien inferior)',
|
||||
icono: '💰'
|
||||
},
|
||||
{
|
||||
nombre: 'Gustos y preferencias',
|
||||
descripcion: 'Cambios en los gustos de los consumidores por modas, publicidad o información',
|
||||
direccion: DireccionDesplazamiento.DERECHA,
|
||||
ejemplo: 'Una campaña de salud que promueve el consumo de agua aumenta la demanda de botellas',
|
||||
icono: '❤️'
|
||||
},
|
||||
{
|
||||
nombre: 'Precio de bienes relacionados',
|
||||
descripcion: 'Cambios en el precio de sustitutos o complementarios',
|
||||
direccion: DireccionDesplazamiento.DERECHA,
|
||||
ejemplo: 'Si sube el precio del café, aumenta la demanda de té (sustituto)',
|
||||
icono: '🔗'
|
||||
},
|
||||
{
|
||||
nombre: 'Expectativas futuras',
|
||||
descripcion: 'Expectativas sobre precios, ingresos o disponibilidad futura',
|
||||
direccion: DireccionDesplazamiento.DERECHA,
|
||||
ejemplo: 'Si se espera que suba el precio de la vivienda, la demanda actual aumenta',
|
||||
icono: '🔮'
|
||||
},
|
||||
{
|
||||
nombre: 'Número de compradores',
|
||||
descripcion: 'Cambios en la población o demografía del mercado',
|
||||
direccion: DireccionDesplazamiento.DERECHA,
|
||||
ejemplo: 'Llegada de turistas aumenta la demanda de hospedaje en temporada alta',
|
||||
icono: '👥'
|
||||
}
|
||||
];
|
||||
|
||||
export const tiposBienesDemanda = {
|
||||
bienNormal: {
|
||||
nombre: 'Bien Normal',
|
||||
definicion: 'Demanda aumenta cuando aumenta el ingreso',
|
||||
relacionIngreso: 'Directa',
|
||||
ejemplos: ['Ropa de marca', 'Restaurantes', 'Viajes', 'Electrónicos'],
|
||||
elasticidadIngreso: 'E_Y > 0'
|
||||
},
|
||||
|
||||
bienInferior: {
|
||||
nombre: 'Bien Inferior',
|
||||
definicion: 'Demanda disminuye cuando aumenta el ingreso',
|
||||
relacionIngreso: 'Inversa',
|
||||
ejemplos: ['Fideos instantáneos', 'Transporte público', 'Marcas genéricas', 'Comida rápida económica'],
|
||||
elasticidadIngreso: 'E_Y < 0'
|
||||
},
|
||||
|
||||
bienLujo: {
|
||||
nombre: 'Bien de Lujo',
|
||||
definicion: 'Demanda aumenta proporcionalmente más que el ingreso',
|
||||
relacionIngreso: 'Directa (elástica)',
|
||||
ejemplos: ['Yates', 'Joyería', 'Autos deportivos', 'Viajes en primera clase'],
|
||||
elasticidadIngreso: 'E_Y > 1'
|
||||
},
|
||||
|
||||
bienNecesidad: {
|
||||
nombre: 'Bien de Necesidad',
|
||||
definicion: 'Demanda aumenta menos proporcionalmente que el ingreso',
|
||||
relacionIngreso: 'Directa (inelástica)',
|
||||
ejemplos: ['Sal', 'Agua', 'Pan básico', 'Medicinas esenciales'],
|
||||
elasticidadIngreso: '0 < E_Y < 1'
|
||||
}
|
||||
};
|
||||
|
||||
export const bienesRelacionados = {
|
||||
sustitutos: {
|
||||
nombre: 'Bienes Sustitutos',
|
||||
definicion: 'Bienes que pueden reemplazarse mutuamente para satisfacer la misma necesidad',
|
||||
relacionPrecio: 'Directa: si sube el precio de A, aumenta la demanda de B',
|
||||
coeficiente: 'E_AB > 0',
|
||||
ejemplos: [
|
||||
{ bienA: 'Coca-Cola', bienB: 'Pepsi' },
|
||||
{ bienA: 'Mantequilla', bienB: 'Margarina' },
|
||||
{ bienA: 'Carne de res', bienB: 'Pollo' }
|
||||
]
|
||||
},
|
||||
|
||||
complementarios: {
|
||||
nombre: 'Bienes Complementarios',
|
||||
definicion: 'Bienes que se consumen juntos para mayor satisfacción',
|
||||
relacionPrecio: 'Inversa: si sube el precio de A, disminuye la demanda de B',
|
||||
coeficiente: 'E_AB < 0',
|
||||
ejemplos: [
|
||||
{ bienA: 'Autos', bienB: 'Gasolina' },
|
||||
{ bienA: 'Café', bienB: 'Azúcar' },
|
||||
{ bienA: 'Impresora', bienB: 'Tinta' }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// CURVAS DE DEMANDA
|
||||
// ============================================
|
||||
|
||||
export const curvaDemandaIndividual: CurvaDemanda = {
|
||||
id: 'demanda-individual',
|
||||
nombre: 'Curva de Demanda Individual',
|
||||
descripcion: 'Muestra la relación entre precio y cantidad demandada por un solo consumidor',
|
||||
puntos: [
|
||||
{ precio: 10, cantidad: 1 },
|
||||
{ precio: 8, cantidad: 3 },
|
||||
{ precio: 6, cantidad: 5 },
|
||||
{ precio: 4, cantidad: 7 },
|
||||
{ precio: 2, cantidad: 9 }
|
||||
]
|
||||
};
|
||||
|
||||
export const curvaDemandaMercado: CurvaDemanda = {
|
||||
id: 'demanda-mercado',
|
||||
nombre: 'Curva de Demanda de Mercado',
|
||||
descripcion: 'Suma horizontal de todas las demandas individuales en el mercado',
|
||||
puntos: [
|
||||
{ precio: 10, cantidad: 100 },
|
||||
{ precio: 8, cantidad: 300 },
|
||||
{ precio: 6, cantidad: 500 },
|
||||
{ precio: 4, cantidad: 700 },
|
||||
{ precio: 2, cantidad: 900 }
|
||||
]
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// EJEMPLOS PRÁCTICOS
|
||||
// ============================================
|
||||
|
||||
export const ejemplosDemanda: EjemploDemanda[] = [
|
||||
{
|
||||
titulo: 'Demanda de Entradas de Cine',
|
||||
bien: 'Entradas de cine',
|
||||
escenario: 'El cine reduce sus precios de $12 a $8 durante los martes',
|
||||
explicacion: 'Según la ley de la demanda, al disminuir el precio, más personas asistirán al cine. La cantidad demandada aumenta moviéndonos a lo largo de la curva.',
|
||||
graficoData: [
|
||||
{ precio: 12, cantidad: 100 },
|
||||
{ precio: 10, cantidad: 150 },
|
||||
{ precio: 8, cantidad: 220 },
|
||||
{ precio: 6, cantidad: 300 },
|
||||
{ precio: 4, cantidad: 400 }
|
||||
]
|
||||
},
|
||||
{
|
||||
titulo: 'Efecto de Ingreso en Restaurant',
|
||||
bien: 'Comida en restaurantes',
|
||||
escenario: 'Los habitantes de una ciudad reciben un aumento salarial del 20%',
|
||||
explicacion: 'Los restaurantes son un bien normal. Al aumentar el ingreso, la demanda se desplaza a la derecha: a cada precio, se demanda más cantidad.',
|
||||
graficoData: [
|
||||
{ precio: 50, cantidad: 200 },
|
||||
{ precio: 40, cantidad: 300 },
|
||||
{ precio: 30, cantidad: 450 },
|
||||
{ precio: 20, cantidad: 600 },
|
||||
{ precio: 10, cantidad: 800 }
|
||||
]
|
||||
},
|
||||
{
|
||||
titulo: 'Sustitutos: Café vs Té',
|
||||
bien: 'Té',
|
||||
escenario: 'Una sequía en Brasil aumenta el precio del café en un 50%',
|
||||
explicacion: 'Como café y té son sustitutos, al subir el precio del café, la demanda de té se desplaza a la derecha. Más consumidores optarán por té.',
|
||||
graficoData: [
|
||||
{ precio: 5, cantidad: 100 },
|
||||
{ precio: 4, cantidad: 180 },
|
||||
{ precio: 3, cantidad: 280 },
|
||||
{ precio: 2, cantidad: 400 },
|
||||
{ precio: 1, cantidad: 550 }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// MOVIMIENTO VS DESPLAZAMIENTO
|
||||
// ============================================
|
||||
|
||||
export const diferenciaMovimientoDesplazamiento = {
|
||||
titulo: 'Movimiento a lo largo vs Desplazamiento de la curva',
|
||||
|
||||
movimiento: {
|
||||
nombre: 'Movimiento a lo largo de la curva',
|
||||
causa: 'Cambio en el precio del propio bien',
|
||||
efecto: 'Cambio en la cantidad demandada (no en la demanda)',
|
||||
direccion: 'Subida o bajada por la misma curva',
|
||||
ejemplo: 'El precio del pan sube de $2 a $3 → compramos menos pan'
|
||||
},
|
||||
|
||||
desplazamiento: {
|
||||
nombre: 'Desplazamiento de la curva',
|
||||
causa: 'Cambio en factores distintos al precio (ingreso, gustos, precios relacionados)',
|
||||
efecto: 'Cambio en la demanda (toda la curva se mueve)',
|
||||
direccionDerecha: 'Aumento de demanda (más cantidad a cada precio)',
|
||||
direccionIzquierda: 'Disminución de demanda (menos cantidad a cada precio)',
|
||||
ejemplo: 'Aumento de ingreso → compramos más restaurantes a todos los precios'
|
||||
},
|
||||
|
||||
tablaComparativa: [
|
||||
{ concepto: 'Causa', movimiento: 'Precio del bien cambia', desplazamiento: 'Otros factores cambian' },
|
||||
{ concepto: 'Gráfico', movimiento: 'Nos movemos sobre la curva', desplazamiento: 'Curva se desplaza' },
|
||||
{ concepto: 'Terminología', movimiento: 'Cambio en cantidad demandada', desplazamiento: 'Cambio en demanda' },
|
||||
{ concepto: 'Ejemplo', movimiento: 'Precio de manzanas ↓', desplazamiento: 'Ingreso ↑ (bien normal)' }
|
||||
]
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// RESUMEN Y PUNTOS CLAVE
|
||||
// ============================================
|
||||
|
||||
export const resumenDemanda = {
|
||||
titulo: 'Resumen: Demanda',
|
||||
|
||||
puntosClave: [
|
||||
'La demanda requiere disposición Y capacidad de comprar',
|
||||
'La ley de la demanda establece relación inversa precio-cantidad',
|
||||
'La curva de demanda tiene pendiente negativa',
|
||||
'El desplazamiento de la curva es causado por factores no-precio',
|
||||
'Los bienes normales tienen demanda directa con el ingreso',
|
||||
'Los bienes inferiores tienen demanda inversa con el ingreso',
|
||||
'Sustitutos: precio de A ↑ → demanda de B ↑',
|
||||
'Complementarios: precio de A ↑ → demanda de B ↓'
|
||||
],
|
||||
|
||||
formulaRecordatorio: {
|
||||
leyDemanda: 'P ↑ → Qd ↓ (ceteris paribus)',
|
||||
demandaMercado: 'Qd_mercado = Σ Qd_individuales',
|
||||
elasticidadPrecio: 'Ed = (%ΔQd) / (%ΔP) < 0'
|
||||
}
|
||||
};
|
||||
|
||||
// Exportación por defecto para facilitar importaciones
|
||||
export default {
|
||||
definicion: definicionDemanda,
|
||||
ley: leyDemanda,
|
||||
factores: factoresDesplazamiento,
|
||||
tiposBienes: tiposBienesDemanda,
|
||||
bienesRelacionados,
|
||||
curvas: {
|
||||
individual: curvaDemandaIndividual,
|
||||
mercado: curvaDemandaMercado
|
||||
},
|
||||
ejemplos: ejemplosDemanda,
|
||||
diferencia: diferenciaMovimientoDesplazamiento,
|
||||
resumen: resumenDemanda
|
||||
};
|
||||
855
frontend/src/content/modulo2/ejercicios.ts
Normal file
855
frontend/src/content/modulo2/ejercicios.ts
Normal file
@@ -0,0 +1,855 @@
|
||||
/**
|
||||
* Módulo 2: Ejercicios Interactivos
|
||||
*
|
||||
* Este módulo contiene la estructura de ejercicios para practicar
|
||||
* los conceptos de oferta, demanda y equilibrio.
|
||||
*/
|
||||
|
||||
import type { PuntoMercado } from './equilibrio';
|
||||
|
||||
// ============================================
|
||||
// TIPOS Y ENUMERACIONES
|
||||
// ============================================
|
||||
|
||||
export enum TipoEjercicio {
|
||||
CONSTRUCTOR_CURVAS = 'constructor_curvas',
|
||||
SIMULADOR_PRECIOS = 'simulador_precios',
|
||||
IDENTIFICAR_SHOCKS = 'identificar_shocks'
|
||||
}
|
||||
|
||||
export enum Dificultad {
|
||||
FACIL = 'facil',
|
||||
MEDIO = 'medio',
|
||||
DIFICIL = 'dificil'
|
||||
}
|
||||
|
||||
export enum TipoRespuesta {
|
||||
MULTIPLE_CHOICE = 'multiple_choice',
|
||||
ARRASTRAR_SOLTAR = 'arrastrar_soltar',
|
||||
GRAFICO_INTERACTIVO = 'grafico_interactivo',
|
||||
NUMERICO = 'numerico',
|
||||
SELECCIONAR = 'seleccionar'
|
||||
}
|
||||
|
||||
export enum TipoShock {
|
||||
DEMANDA_AUMENTA = 'demanda_aumenta',
|
||||
DEMANDA_DISMINUYE = 'demanda_disminuye',
|
||||
OFERTA_AUMENTA = 'oferta_aumenta',
|
||||
OFERTA_DISMINUYE = 'oferta_disminuye',
|
||||
AMBAS_OFERTA_DEMANDA = 'ambas_oferta_demanda'
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// INTERFACES
|
||||
// ============================================
|
||||
|
||||
export interface PuntoGrafico {
|
||||
x: number;
|
||||
y: number;
|
||||
etiqueta?: string;
|
||||
tipo: 'demanda' | 'oferta' | 'equilibrio' | 'interseccion';
|
||||
}
|
||||
|
||||
export interface CurvaEjercicio {
|
||||
id: string;
|
||||
tipo: 'demanda' | 'oferta';
|
||||
puntos: PuntoGrafico[];
|
||||
color: string;
|
||||
etiqueta: string;
|
||||
editable: boolean;
|
||||
}
|
||||
|
||||
export interface OpcionRespuesta {
|
||||
id: string;
|
||||
texto: string;
|
||||
correcta: boolean;
|
||||
retroalimentacion?: string;
|
||||
}
|
||||
|
||||
export interface Pregunta {
|
||||
id: string;
|
||||
enunciado: string;
|
||||
tipo: TipoRespuesta;
|
||||
opciones?: OpcionRespuesta[];
|
||||
respuestaCorrecta?: number | string | string[];
|
||||
ayuda?: string;
|
||||
puntos: number;
|
||||
}
|
||||
|
||||
export interface NivelEjercicio {
|
||||
id: string;
|
||||
nombre: string;
|
||||
dificultad: Dificultad;
|
||||
descripcion: string;
|
||||
completado: boolean;
|
||||
desbloqueado: boolean;
|
||||
}
|
||||
|
||||
export interface Ejercicio {
|
||||
id: string;
|
||||
tipo: TipoEjercicio;
|
||||
titulo: string;
|
||||
descripcion: string;
|
||||
instrucciones: string[];
|
||||
dificultad: Dificultad;
|
||||
niveles: NivelEjercicio[];
|
||||
preguntas?: Pregunta[];
|
||||
configuracionGrafico?: ConfiguracionGrafico;
|
||||
datosSimulacion?: DatosSimulacion;
|
||||
escenariosShock?: EscenarioShock[];
|
||||
}
|
||||
|
||||
export interface ConfiguracionGrafico {
|
||||
ancho: number;
|
||||
alto: number;
|
||||
escalaX: { min: number; max: number; etiqueta: string };
|
||||
escalaY: { min: number; max: number; etiqueta: string };
|
||||
curvasIniciales: CurvaEjercicio[];
|
||||
puntosObjetivo: PuntoGrafico[];
|
||||
toleranciaError: number;
|
||||
}
|
||||
|
||||
export interface DatosSimulacion {
|
||||
funcionDemanda: string;
|
||||
funcionOferta: string;
|
||||
precioEquilibrio: number;
|
||||
cantidadEquilibrio: number;
|
||||
rangoPrecios: { min: number; max: number };
|
||||
controlesPrecio: {
|
||||
precioMaximo: number | null;
|
||||
precioMinimo: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface EscenarioShock {
|
||||
id: string;
|
||||
titulo: string;
|
||||
descripcion: string;
|
||||
mercado: string;
|
||||
evento: string;
|
||||
tipoShock: TipoShock;
|
||||
magnitud: 'pequeña' | 'media' | 'grande';
|
||||
graficoInicial: {
|
||||
demanda: PuntoMercado[];
|
||||
oferta: PuntoMercado[];
|
||||
};
|
||||
graficoFinal: {
|
||||
demanda: PuntoMercado[];
|
||||
oferta: PuntoMercado[];
|
||||
};
|
||||
resultadoEsperado: {
|
||||
precioCambio: 'sube' | 'baja' | 'igual';
|
||||
cantidadCambio: 'sube' | 'baja' | 'igual' | 'indeterminado';
|
||||
explicacion: string;
|
||||
};
|
||||
opciones: OpcionRespuesta[];
|
||||
}
|
||||
|
||||
export interface ProgresoEjercicio {
|
||||
ejercicioId: string;
|
||||
completado: boolean;
|
||||
puntuacion: number;
|
||||
tiempoSegundos: number;
|
||||
intentos: number;
|
||||
nivelesCompletados: string[];
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// EJERCICIO 1: CONSTRUCTOR DE CURVAS
|
||||
// ============================================
|
||||
|
||||
export const constructorCurvas: Ejercicio = {
|
||||
id: 'ejercicio-1-constructor-curvas',
|
||||
tipo: TipoEjercicio.CONSTRUCTOR_CURVAS,
|
||||
titulo: 'Constructor de Curvas de Oferta y Demanda',
|
||||
descripcion: 'Construye curvas de oferta y demanda arrastrando puntos para entender sus pendientes y movimientos.',
|
||||
instrucciones: [
|
||||
'Observa los puntos dados en el gráfico',
|
||||
'Arrastra cada punto para formar una curva de demanda con pendiente negativa',
|
||||
'Arrastra los puntos de oferta para formar una curva con pendiente positiva',
|
||||
'Encuentra el punto de equilibrio donde se intersecan ambas curvas',
|
||||
'Verifica tu respuesta con el botón "Comprobar"'
|
||||
],
|
||||
dificultad: Dificultad.FACIL,
|
||||
|
||||
niveles: [
|
||||
{
|
||||
id: 'nivel-1-basico',
|
||||
nombre: 'Nivel 1: Trazado Básico',
|
||||
dificultad: Dificultad.FACIL,
|
||||
descripcion: 'Traza una curva de demanda simple con 3 puntos',
|
||||
completado: false,
|
||||
desbloqueado: true
|
||||
},
|
||||
{
|
||||
id: 'nivel-2-oferta-demanda',
|
||||
nombre: 'Nivel 2: Ambas Curvas',
|
||||
dificultad: Dificultad.FACIL,
|
||||
descripcion: 'Traza curvas de oferta y demanda y encuentra el equilibrio',
|
||||
completado: false,
|
||||
desbloqueado: false
|
||||
},
|
||||
{
|
||||
id: 'nivel-3-desplazamientos',
|
||||
nombre: 'Nivel 3: Desplazamientos',
|
||||
dificultad: Dificultad.MEDIO,
|
||||
descripcion: 'Muestra cómo cambian las curvas ante diferentes shocks',
|
||||
completado: false,
|
||||
desbloqueado: false
|
||||
},
|
||||
{
|
||||
id: 'nivel-4-precision',
|
||||
nombre: 'Nivel 4: Precisión',
|
||||
dificultad: Dificultad.DIFICIL,
|
||||
descripcion: 'Traza curvas con tolerancia mínima de error',
|
||||
completado: false,
|
||||
desbloqueado: false
|
||||
}
|
||||
],
|
||||
|
||||
configuracionGrafico: {
|
||||
ancho: 600,
|
||||
alto: 400,
|
||||
escalaX: { min: 0, max: 100, etiqueta: 'Cantidad (Q)' },
|
||||
escalaY: { min: 0, max: 50, etiqueta: 'Precio (P)' },
|
||||
toleranciaError: 5,
|
||||
|
||||
curvasIniciales: [
|
||||
{
|
||||
id: 'demanda-nivel-1',
|
||||
tipo: 'demanda',
|
||||
etiqueta: 'Demanda',
|
||||
color: '#e74c3c',
|
||||
editable: true,
|
||||
puntos: [
|
||||
{ x: 20, y: 40, tipo: 'demanda', etiqueta: 'A' },
|
||||
{ x: 50, y: 25, tipo: 'demanda', etiqueta: 'B' },
|
||||
{ x: 80, y: 10, tipo: 'demanda', etiqueta: 'C' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'oferta-nivel-2',
|
||||
tipo: 'oferta',
|
||||
etiqueta: 'Oferta',
|
||||
color: '#27ae60',
|
||||
editable: true,
|
||||
puntos: [
|
||||
{ x: 20, y: 10, tipo: 'oferta', etiqueta: 'D' },
|
||||
{ x: 50, y: 25, tipo: 'oferta', etiqueta: 'E' },
|
||||
{ x: 80, y: 40, tipo: 'oferta', etiqueta: 'F' }
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
puntosObjetivo: [
|
||||
{ x: 50, y: 25, tipo: 'equilibrio', etiqueta: 'E*' }
|
||||
]
|
||||
},
|
||||
|
||||
preguntas: [
|
||||
{
|
||||
id: 'pregunta-1-pendiente',
|
||||
enunciado: '¿Qué tipo de pendiente tiene la curva de demanda?',
|
||||
tipo: TipoRespuesta.MULTIPLE_CHOICE,
|
||||
puntos: 10,
|
||||
ayuda: 'Recuerda la ley de la demanda: cuando el precio sube, la cantidad demandada baja.',
|
||||
opciones: [
|
||||
{ id: 'a', texto: 'Pendiente positiva (sube de izquierda a derecha)', correcta: false, retroalimentacion: 'Incorrecto. La demanda tiene pendiente negativa.' },
|
||||
{ id: 'b', texto: 'Pendiente negativa (baja de izquierda a derecha)', correcta: true, retroalimentacion: '¡Correcto! La curva de demanda tiene pendiente negativa por la ley de la demanda.' },
|
||||
{ id: 'c', texto: 'Pendiente cero (línea horizontal)', correcta: false, retroalimentacion: 'Incorrecto. Eso sería oferta perfectamente elástica.' },
|
||||
{ id: 'd', texto: 'Pendiente infinita (línea vertical)', correcta: false, retroalimentacion: 'Incorrecto. Eso sería oferta perfectamente inelástica.' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'pregunta-2-equilibrio',
|
||||
enunciado: '¿Dónde ocurre el equilibrio de mercado?',
|
||||
tipo: TipoRespuesta.MULTIPLE_CHOICE,
|
||||
puntos: 15,
|
||||
ayuda: 'El equilibrio es donde las decisiones de compradores y vendedores coinciden.',
|
||||
opciones: [
|
||||
{ id: 'a', texto: 'Donde la demanda es máxima', correcta: false },
|
||||
{ id: 'b', texto: 'Donde la oferta es máxima', correcta: false },
|
||||
{ id: 'c', texto: 'En la intersección de oferta y demanda', correcta: true, retroalimentacion: '¡Correcto! El equilibrio ocurre donde Qd = Qs.' },
|
||||
{ id: 'd', texto: 'En el origen (0,0)', correcta: false }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// EJERCICIO 2: SIMULADOR DE PRECIOS INTERVENIDOS
|
||||
// ============================================
|
||||
|
||||
export const simuladorPrecios: Ejercicio = {
|
||||
id: 'ejercicio-2-simulador-precios',
|
||||
tipo: TipoEjercicio.SIMULADOR_PRECIOS,
|
||||
titulo: 'Simulador de Precios Intervenidos',
|
||||
descripcion: 'Ajusta precios máximos y mínimos para observar sus efectos en el mercado: escasez, superávit, y pérdida de bienestar.',
|
||||
instrucciones: [
|
||||
'Observa el equilibrio inicial del mercado',
|
||||
'Usa los controles deslizantes para establecer un precio máximo o mínimo',
|
||||
'Observa cómo cambian las cantidades demandadas y ofrecidas',
|
||||
'Identifica si se genera escasez o superávit',
|
||||
'Analiza el área de pérdida de bienestar (triángulo)',
|
||||
'Responde las preguntas sobre cada escenario'
|
||||
],
|
||||
dificultad: Dificultad.MEDIO,
|
||||
|
||||
niveles: [
|
||||
{
|
||||
id: 'nivel-1-techo',
|
||||
nombre: 'Nivel 1: Precio Máximo',
|
||||
dificultad: Dificultad.FACIL,
|
||||
descripcion: 'Establece un precio máximo y observa la escasez generada',
|
||||
completado: false,
|
||||
desbloqueado: true
|
||||
},
|
||||
{
|
||||
id: 'nivel-2-piso',
|
||||
nombre: 'Nivel 2: Precio Mínimo',
|
||||
dificultad: Dificultad.FACIL,
|
||||
descripcion: 'Establece un precio mínimo y observa el superávit',
|
||||
completado: false,
|
||||
desbloqueado: false
|
||||
},
|
||||
{
|
||||
id: 'nivel-3-ambos',
|
||||
nombre: 'Nivel 3: Combinado',
|
||||
dificultad: Dificultad.MEDIO,
|
||||
descripcion: 'Analiza escenarios con precios mínimos y máximos simultáneos',
|
||||
completado: false,
|
||||
desbloqueado: false
|
||||
},
|
||||
{
|
||||
id: 'nivel-4-calculo',
|
||||
nombre: 'Nivel 4: Cálculo de Pérdida',
|
||||
dificultad: Dificultad.DIFICIL,
|
||||
descripcion: 'Calcula numéricamente la pérdida de bienestar',
|
||||
completado: false,
|
||||
desbloqueado: false
|
||||
}
|
||||
],
|
||||
|
||||
datosSimulacion: {
|
||||
funcionDemanda: 'Qd = 100 - 2P',
|
||||
funcionOferta: 'Qs = 20 + 2P',
|
||||
precioEquilibrio: 20,
|
||||
cantidadEquilibrio: 60,
|
||||
rangoPrecios: { min: 0, max: 50 },
|
||||
controlesPrecio: {
|
||||
precioMaximo: 15,
|
||||
precioMinimo: 25
|
||||
}
|
||||
},
|
||||
|
||||
preguntas: [
|
||||
{
|
||||
id: 'sim-pregunta-1',
|
||||
enunciado: 'Si estableces un precio máximo de $15 (debajo del equilibrio de $20), ¿qué ocurre?',
|
||||
tipo: TipoRespuesta.MULTIPLE_CHOICE,
|
||||
puntos: 20,
|
||||
opciones: [
|
||||
{
|
||||
id: 'a',
|
||||
texto: 'Se genera un exceso de oferta (superávit)',
|
||||
correcta: false,
|
||||
retroalimentacion: 'Incorrecto. Un precio máximo por debajo del equilibrio genera escasez, no superávit.'
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
texto: 'Se genera un exceso de demanda (escasez)',
|
||||
correcta: true,
|
||||
retroalimentacion: '¡Correcto! A $15, Qd = 70 y Qs = 50, hay escasez de 20 unidades.'
|
||||
},
|
||||
{
|
||||
id: 'c',
|
||||
texto: 'El mercado permanece en equilibrio',
|
||||
correcta: false,
|
||||
retroalimentacion: 'Incorrecto. El precio controlado impide alcanzar el equilibrio.'
|
||||
},
|
||||
{
|
||||
id: 'd',
|
||||
texto: 'La cantidad transada aumenta',
|
||||
correcta: false,
|
||||
retroalimentacion: 'Incorrecto. La cantidad transada disminuye al nivel de la oferta (50).'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'sim-pregunta-2',
|
||||
enunciado: '¿Cuál es la cantidad transada con un precio máximo de $15?',
|
||||
tipo: TipoRespuesta.NUMERICO,
|
||||
puntos: 25,
|
||||
respuestaCorrecta: 50,
|
||||
ayuda: 'Con precio máximo, la cantidad transada es el menor entre cantidad demandada y cantidad ofrecida.'
|
||||
},
|
||||
{
|
||||
id: 'sim-pregunta-3',
|
||||
enunciado: 'Si el gobierno establece un precio mínimo de $25, ¿qué cantidad demandarán los consumidores?',
|
||||
tipo: TipoRespuesta.NUMERICO,
|
||||
puntos: 25,
|
||||
respuestaCorrecta: 50,
|
||||
ayuda: 'Usa la función de demanda: Qd = 100 - 2P. Sustituye P = 25.'
|
||||
},
|
||||
{
|
||||
id: 'sim-pregunta-4',
|
||||
enunciado: 'Selecciona todos los efectos de un precio máximo efectivo:',
|
||||
tipo: TipoRespuesta.SELECCIONAR,
|
||||
puntos: 30,
|
||||
opciones: [
|
||||
{ id: 'a', texto: 'Escasez del bien', correcta: true },
|
||||
{ id: 'b', texto: 'Colas y listas de espera', correcta: true },
|
||||
{ id: 'c', texto: 'Aumento de calidad', correcta: false },
|
||||
{ id: 'd', texto: 'Mercados negros', correcta: true },
|
||||
{ id: 'e', texto: 'Reducción de oferta', correcta: true },
|
||||
{ id: 'f', texto: 'Mayor bienestar total', correcta: false }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// EJERCICIO 3: IDENTIFICAR SHOCKS
|
||||
// ============================================
|
||||
|
||||
export const identificarShocks: Ejercicio = {
|
||||
id: 'ejercicio-3-identificar-shocks',
|
||||
tipo: TipoEjercicio.IDENTIFICAR_SHOCKS,
|
||||
titulo: 'Identificador de Shocks del Mercado',
|
||||
descripcion: 'Analiza escenarios económicos reales e identifica si afectan la oferta, la demanda, ambas, y cómo cambian precio y cantidad de equilibrio.',
|
||||
instrucciones: [
|
||||
'Lee el escenario presentado cuidadosamente',
|
||||
'Identifica qué factor económico ha cambiado',
|
||||
'Determina si afecta a la oferta, demanda, o ambas',
|
||||
'Predice la dirección del cambio (aumenta/disminuye)',
|
||||
'Indica cómo cambiarán el precio y la cantidad de equilibrio',
|
||||
'Verifica tu respuesta y lee la explicación'
|
||||
],
|
||||
dificultad: Dificultad.MEDIO,
|
||||
|
||||
niveles: [
|
||||
{
|
||||
id: 'nivel-1-shocks-simples',
|
||||
nombre: 'Nivel 1: Shocks Simples',
|
||||
dificultad: Dificultad.FACIL,
|
||||
descripcion: 'Identifica cambios solo en oferta o solo en demanda',
|
||||
completado: false,
|
||||
desbloqueado: true
|
||||
},
|
||||
{
|
||||
id: 'nivel-2-magnitud',
|
||||
nombre: 'Nivel 2: Magnitud de Cambios',
|
||||
dificultad: Dificultad.MEDIO,
|
||||
descripcion: 'Determina qué curva se desplaza más y efecto neto',
|
||||
completado: false,
|
||||
desbloqueado: false
|
||||
},
|
||||
{
|
||||
id: 'nivel-3-escenarios-reales',
|
||||
nombre: 'Nivel 3: Escenarios Reales',
|
||||
dificultad: Dificultad.MEDIO,
|
||||
descripcion: 'Analiza noticias económicas reales',
|
||||
completado: false,
|
||||
desbloqueado: false
|
||||
},
|
||||
{
|
||||
id: 'nivel-4-complejos',
|
||||
nombre: 'Nivel 4: Casos Complejos',
|
||||
dificultad: Dificultad.DIFICIL,
|
||||
descripcion: 'Escenarios donde oferta y demanda cambian simultáneamente',
|
||||
completado: false,
|
||||
desbloqueado: false
|
||||
}
|
||||
],
|
||||
|
||||
escenariosShock: [
|
||||
// Nivel 1 - Simples
|
||||
{
|
||||
id: 'shock-1-cafe',
|
||||
titulo: 'Café y Clima',
|
||||
descripcion: 'Una sequía severa afecta las principales zonas cafetaleras de Brasil.',
|
||||
mercado: 'Café',
|
||||
evento: 'Sequía en Brasil',
|
||||
tipoShock: TipoShock.OFERTA_DISMINUYE,
|
||||
magnitud: 'grande',
|
||||
graficoInicial: {
|
||||
demanda: [
|
||||
{ precio: 10, cantidad: 100 },
|
||||
{ precio: 8, cantidad: 120 },
|
||||
{ precio: 6, cantidad: 140 }
|
||||
],
|
||||
oferta: [
|
||||
{ precio: 6, cantidad: 100 },
|
||||
{ precio: 8, cantidad: 120 },
|
||||
{ precio: 10, cantidad: 140 }
|
||||
]
|
||||
},
|
||||
graficoFinal: {
|
||||
demanda: [
|
||||
{ precio: 10, cantidad: 100 },
|
||||
{ precio: 8, cantidad: 120 },
|
||||
{ precio: 6, cantidad: 140 }
|
||||
],
|
||||
oferta: [
|
||||
{ precio: 8, cantidad: 80 },
|
||||
{ precio: 10, cantidad: 100 },
|
||||
{ precio: 12, cantidad: 120 }
|
||||
]
|
||||
},
|
||||
resultadoEsperado: {
|
||||
precioCambio: 'sube',
|
||||
cantidadCambio: 'baja',
|
||||
explicacion: 'La sequía reduce la cosecha, desplazando la oferta a la izquierda. Menor cantidad disponible a cada precio.'
|
||||
},
|
||||
opciones: [
|
||||
{ id: 'a', texto: 'Demanda aumenta → P sube, Q sube', correcta: false },
|
||||
{ id: 'b', texto: 'Oferta disminuye → P sube, Q baja', correcta: true },
|
||||
{ id: 'c', texto: 'Oferta aumenta → P baja, Q sube', correcta: false },
|
||||
{ id: 'd', texto: 'Demanda disminuye → P baja, Q baja', correcta: false }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'shock-2-ingreso',
|
||||
titulo: 'Restaurant y Bonos',
|
||||
descripcion: 'El gobierno entrega bonos de $500 a todos los ciudadanos.',
|
||||
mercado: 'Comida en restaurantes',
|
||||
evento: 'Aumento de ingreso',
|
||||
tipoShock: TipoShock.DEMANDA_AUMENTA,
|
||||
magnitud: 'media',
|
||||
graficoInicial: {
|
||||
demanda: [
|
||||
{ precio: 20, cantidad: 100 },
|
||||
{ precio: 16, cantidad: 140 },
|
||||
{ precio: 12, cantidad: 180 }
|
||||
],
|
||||
oferta: [
|
||||
{ precio: 12, cantidad: 100 },
|
||||
{ precio: 16, cantidad: 140 },
|
||||
{ precio: 20, cantidad: 180 }
|
||||
]
|
||||
},
|
||||
graficoFinal: {
|
||||
demanda: [
|
||||
{ precio: 24, cantidad: 100 },
|
||||
{ precio: 20, cantidad: 140 },
|
||||
{ precio: 16, cantidad: 180 }
|
||||
],
|
||||
oferta: [
|
||||
{ precio: 12, cantidad: 100 },
|
||||
{ precio: 16, cantidad: 140 },
|
||||
{ precio: 20, cantidad: 180 }
|
||||
]
|
||||
},
|
||||
resultadoEsperado: {
|
||||
precioCambio: 'sube',
|
||||
cantidadCambio: 'sube',
|
||||
explicacion: 'Los restaurantes son un bien normal. Al aumentar el ingreso, la demanda se desplaza a la derecha.'
|
||||
},
|
||||
opciones: [
|
||||
{ id: 'a', texto: 'Demanda aumenta → P sube, Q sube', correcta: true },
|
||||
{ id: 'b', texto: 'Demanda disminuye → P baja, Q baja', correcta: false },
|
||||
{ id: 'c', texto: 'Oferta aumenta → P baja, Q sube', correcta: false },
|
||||
{ id: 'd', texto: 'Oferta disminuye → P sube, Q baja', correcta: false }
|
||||
]
|
||||
},
|
||||
// Nivel 2 - Magnitud
|
||||
{
|
||||
id: 'shock-3-tecnologia',
|
||||
titulo: 'Autos Eléctricos',
|
||||
descripcion: 'Nueva tecnología de baterías reduce costos de producción en 40%.',
|
||||
mercado: 'Autos eléctricos',
|
||||
evento: 'Avance tecnológico',
|
||||
tipoShock: TipoShock.OFERTA_AUMENTA,
|
||||
magnitud: 'grande',
|
||||
graficoInicial: {
|
||||
demanda: [
|
||||
{ precio: 50000, cantidad: 10000 },
|
||||
{ precio: 40000, cantidad: 20000 }
|
||||
],
|
||||
oferta: [
|
||||
{ precio: 30000, cantidad: 10000 },
|
||||
{ precio: 40000, cantidad: 20000 }
|
||||
]
|
||||
},
|
||||
graficoFinal: {
|
||||
demanda: [
|
||||
{ precio: 50000, cantidad: 10000 },
|
||||
{ precio: 40000, cantidad: 20000 }
|
||||
],
|
||||
oferta: [
|
||||
{ precio: 20000, cantidad: 10000 },
|
||||
{ precio: 28000, cantidad: 20000 }
|
||||
]
|
||||
},
|
||||
resultadoEsperado: {
|
||||
precioCambio: 'baja',
|
||||
cantidadCambio: 'sube',
|
||||
explicacion: 'La tecnología mejora la productividad, aumentando oferta. Precios más bajos y mayor cantidad.'
|
||||
},
|
||||
opciones: [
|
||||
{ id: 'a', texto: 'Oferta aumenta significativamente → P baja mucho, Q sube mucho', correcta: true },
|
||||
{ id: 'b', texto: 'Oferta disminuye → P sube, Q baja', correcta: false },
|
||||
{ id: 'c', texto: 'Demanda aumenta → P sube, Q sube', correcta: false },
|
||||
{ id: 'd', texto: 'Demanda disminuye → P baja, Q baja', correcta: false }
|
||||
]
|
||||
},
|
||||
// Nivel 3 - Escenarios reales
|
||||
{
|
||||
id: 'shock-4-petroleo',
|
||||
titulo: 'Crisis del Petróleo',
|
||||
descripcion: 'Conflicto en Medio Oriente reduce exportaciones de petróleo. Simultáneamente, países invierten en energías renovables.',
|
||||
mercado: 'Petróleo',
|
||||
evento: 'Reducción oferta + cambio gustos',
|
||||
tipoShock: TipoShock.AMBAS_OFERTA_DEMANDA,
|
||||
magnitud: 'media',
|
||||
graficoInicial: {
|
||||
demanda: [
|
||||
{ precio: 100, cantidad: 80 },
|
||||
{ precio: 80, cantidad: 100 }
|
||||
],
|
||||
oferta: [
|
||||
{ precio: 60, cantidad: 80 },
|
||||
{ precio: 80, cantidad: 100 }
|
||||
]
|
||||
},
|
||||
graficoFinal: {
|
||||
demanda: [
|
||||
{ precio: 100, cantidad: 70 },
|
||||
{ precio: 80, cantidad: 90 }
|
||||
],
|
||||
oferta: [
|
||||
{ precio: 70, cantidad: 70 },
|
||||
{ precio: 90, cantidad: 90 }
|
||||
]
|
||||
},
|
||||
resultadoEsperado: {
|
||||
precioCambio: 'sube',
|
||||
cantidadCambio: 'baja',
|
||||
explicacion: 'Oferta disminuye (conflicto) y demanda disminuye (alternativas). El precio sube (oferta cae más), cantidad cae (ambas).'
|
||||
},
|
||||
opciones: [
|
||||
{ id: 'a', texto: 'Oferta ↓ y Demanda ↓ → P indeterminado, Q baja', correcta: false },
|
||||
{ id: 'b', texto: 'Oferta ↓ más que Demanda ↓ → P sube, Q baja', correcta: true },
|
||||
{ id: 'c', texto: 'Oferta ↑ y Demanda ↑ → P indeterminado, Q sube', correcta: false },
|
||||
{ id: 'd', texto: 'Solo oferta ↓ → P sube, Q baja', correcta: false }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'shock-5-casa',
|
||||
titulo: 'Mercado Inmobiliario',
|
||||
descripcion: 'Tasas de interés bajan a mínimos históricos. Al mismo tiempo, regulaciones ambientales dificultan nueva construcción.',
|
||||
mercado: 'Vivienda',
|
||||
evento: 'Crédito barato + regulaciones',
|
||||
tipoShock: TipoShock.AMBAS_OFERTA_DEMANDA,
|
||||
magnitud: 'media',
|
||||
graficoInicial: {
|
||||
demanda: [
|
||||
{ precio: 300000, cantidad: 1000 },
|
||||
{ precio: 250000, cantidad: 1500 }
|
||||
],
|
||||
oferta: [
|
||||
{ precio: 200000, cantidad: 1000 },
|
||||
{ precio: 250000, cantidad: 1500 }
|
||||
]
|
||||
},
|
||||
graficoFinal: {
|
||||
demanda: [
|
||||
{ precio: 350000, cantidad: 1000 },
|
||||
{ precio: 300000, cantidad: 1500 }
|
||||
],
|
||||
oferta: [
|
||||
{ precio: 250000, cantidad: 800 },
|
||||
{ precio: 300000, cantidad: 1200 }
|
||||
]
|
||||
},
|
||||
resultadoEsperado: {
|
||||
precioCambio: 'sube',
|
||||
cantidadCambio: 'indeterminado',
|
||||
explicacion: 'Demanda aumenta (crédito barato) y oferta disminuye (regulaciones). El precio definitivamente sube, pero el efecto en cantidad depende de qué cambio sea mayor.'
|
||||
},
|
||||
opciones: [
|
||||
{ id: 'a', texto: 'Demanda ↑ y Oferta ↓ → P sube, Q indeterminado', correcta: true },
|
||||
{ id: 'b', texto: 'Demanda ↓ y Oferta ↑ → P baja, Q indeterminado', correcta: false },
|
||||
{ id: 'c', texto: 'Demanda ↑ y Oferta ↑ → P indeterminado, Q sube', correcta: false },
|
||||
{ id: 'd', texto: 'Demanda ↓ y Oferta ↓ → P indeterminado, Q baja', correcta: false }
|
||||
]
|
||||
},
|
||||
// Nivel 4 - Complejos
|
||||
{
|
||||
id: 'shock-6-pandemia',
|
||||
titulo: 'Efecto Pandemia',
|
||||
descripcion: 'Durante COVID-19: cierres de fábricas reducen producción de laptops, pero trabajo remoto aumenta demanda dramáticamente.',
|
||||
mercado: 'Laptops',
|
||||
evento: 'Pandemia',
|
||||
tipoShock: TipoShock.AMBAS_OFERTA_DEMANDA,
|
||||
magnitud: 'grande',
|
||||
graficoInicial: {
|
||||
demanda: [
|
||||
{ precio: 800, cantidad: 50000 },
|
||||
{ precio: 600, cantidad: 80000 }
|
||||
],
|
||||
oferta: [
|
||||
{ precio: 400, cantidad: 50000 },
|
||||
{ precio: 600, cantidad: 80000 }
|
||||
]
|
||||
},
|
||||
graficoFinal: {
|
||||
demanda: [
|
||||
{ precio: 1200, cantidad: 80000 },
|
||||
{ precio: 1000, cantidad: 110000 }
|
||||
],
|
||||
oferta: [
|
||||
{ precio: 600, cantidad: 40000 },
|
||||
{ precio: 800, cantidad: 60000 }
|
||||
]
|
||||
},
|
||||
resultadoEsperado: {
|
||||
precioCambio: 'sube',
|
||||
cantidadCambio: 'indeterminado',
|
||||
explicacion: 'Oferta disminuyó (cierres) pero demanda aumentó mucho más (trabajo remoto). Precios subieron significativamente. La cantidad pudo subir o bajar según magnitudes relativas.'
|
||||
},
|
||||
opciones: [
|
||||
{ id: 'a', texto: 'Demanda ↑↑↑ más que Oferta ↓ → P sube mucho, Q probablemente sube', correcta: true },
|
||||
{ id: 'b', texto: 'Demanda y Oferta disminuyen → P indeterminado, Q baja', correcta: false },
|
||||
{ id: 'c', texto: 'Solo demanda aumenta → P sube, Q sube', correcta: false },
|
||||
{ id: 'd', texto: 'Solo oferta disminuye → P sube, Q baja', correcta: false }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// FUNCIÓN AUXILIAR: CALCULAR RESULTADO
|
||||
// ============================================
|
||||
|
||||
export function calcularResultadoShock(
|
||||
shockOferta: 'aumenta' | 'disminuye' | 'sin_cambio',
|
||||
shockDemanda: 'aumenta' | 'disminuye' | 'sin_cambio',
|
||||
_magnitudOferta: 'mayor' | 'menor' | 'igual',
|
||||
_magnitudDemanda: 'mayor' | 'menor' | 'igual'
|
||||
): { precio: 'sube' | 'baja' | 'igual' | 'indeterminado'; cantidad: 'sube' | 'baja' | 'igual' | 'indeterminado' } {
|
||||
// Lógica simplificada para determinar resultado
|
||||
let precio: 'sube' | 'baja' | 'igual' | 'indeterminado' = 'igual';
|
||||
let cantidad: 'sube' | 'baja' | 'igual' | 'indeterminado' = 'igual';
|
||||
|
||||
// Análisis de precio
|
||||
if (shockOferta === 'aumenta' && shockDemanda === 'aumenta') {
|
||||
precio = 'indeterminado';
|
||||
} else if (shockOferta === 'disminuye' && shockDemanda === 'disminuye') {
|
||||
precio = 'indeterminado';
|
||||
} else if (shockOferta === 'aumenta' && shockDemanda === 'disminuye') {
|
||||
precio = 'baja';
|
||||
} else if (shockOferta === 'disminuye' && shockDemanda === 'aumenta') {
|
||||
precio = 'sube';
|
||||
} else if (shockDemanda === 'aumenta') {
|
||||
precio = 'sube';
|
||||
} else if (shockDemanda === 'disminuye') {
|
||||
precio = 'baja';
|
||||
} else if (shockOferta === 'aumenta') {
|
||||
precio = 'baja';
|
||||
} else if (shockOferta === 'disminuye') {
|
||||
precio = 'sube';
|
||||
}
|
||||
|
||||
// Análisis de cantidad
|
||||
if (shockOferta === 'aumenta' && shockDemanda === 'disminuye') {
|
||||
cantidad = 'indeterminado';
|
||||
} else if (shockOferta === 'disminuye' && shockDemanda === 'aumenta') {
|
||||
cantidad = 'indeterminado';
|
||||
} else if (shockOferta === 'aumenta' && shockDemanda === 'aumenta') {
|
||||
cantidad = 'sube';
|
||||
} else if (shockOferta === 'disminuye' && shockDemanda === 'disminuye') {
|
||||
cantidad = 'baja';
|
||||
} else if (shockOferta === 'aumenta') {
|
||||
cantidad = 'sube';
|
||||
} else if (shockOferta === 'disminuye') {
|
||||
cantidad = 'baja';
|
||||
} else if (shockDemanda === 'aumenta') {
|
||||
cantidad = 'sube';
|
||||
} else if (shockDemanda === 'disminuye') {
|
||||
cantidad = 'baja';
|
||||
}
|
||||
|
||||
return { precio, cantidad };
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DATOS DE PROGRESO EJEMPLO
|
||||
// ============================================
|
||||
|
||||
export const progresoInicial: ProgresoEjercicio[] = [
|
||||
{
|
||||
ejercicioId: 'ejercicio-1-constructor-curvas',
|
||||
completado: false,
|
||||
puntuacion: 0,
|
||||
tiempoSegundos: 0,
|
||||
intentos: 0,
|
||||
nivelesCompletados: []
|
||||
},
|
||||
{
|
||||
ejercicioId: 'ejercicio-2-simulador-precios',
|
||||
completado: false,
|
||||
puntuacion: 0,
|
||||
tiempoSegundos: 0,
|
||||
intentos: 0,
|
||||
nivelesCompletados: []
|
||||
},
|
||||
{
|
||||
ejercicioId: 'ejercicio-3-identificar-shocks',
|
||||
completado: false,
|
||||
puntuacion: 0,
|
||||
tiempoSegundos: 0,
|
||||
intentos: 0,
|
||||
nivelesCompletados: []
|
||||
}
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// RESUMEN DE EJERCICIOS
|
||||
// ============================================
|
||||
|
||||
export const resumenEjercicios = {
|
||||
titulo: 'Ejercicios del Módulo 2',
|
||||
descripcion: 'Practica los conceptos de oferta, demanda y equilibrio con estos ejercicios interactivos.',
|
||||
|
||||
ejercicios: [
|
||||
{
|
||||
id: 'ejercicio-1',
|
||||
nombre: 'Constructor de Curvas',
|
||||
habilidades: ['Trazar curvas', 'Identificar pendientes', 'Encontrar equilibrio'],
|
||||
tiempoEstimado: '10-15 minutos'
|
||||
},
|
||||
{
|
||||
id: 'ejercicio-2',
|
||||
nombre: 'Simulador de Precios',
|
||||
habilidades: ['Controles de precio', 'Calcular desequilibrios', 'Pérdida de bienestar'],
|
||||
tiempoEstimado: '15-20 minutos'
|
||||
},
|
||||
{
|
||||
id: 'ejercicio-3',
|
||||
nombre: 'Identificar Shocks',
|
||||
habilidades: ['Análisis de escenarios', 'Desplazamientos de curvas', 'Predicción de cambios'],
|
||||
tiempoEstimado: '20-25 minutos'
|
||||
}
|
||||
],
|
||||
|
||||
consejos: [
|
||||
'Comienza con el Ejercicio 1 si eres principiante',
|
||||
'Revisa la teoría antes de intentar los ejercicios',
|
||||
'Usa papel y lápiz para hacer cálculos',
|
||||
'No te preocupes por errores, son parte del aprendizaje',
|
||||
'Repite los ejercicios hasta dominarlos'
|
||||
]
|
||||
};
|
||||
|
||||
// Exportación por defecto
|
||||
export default {
|
||||
constructorCurvas,
|
||||
simuladorPrecios,
|
||||
identificarShocks,
|
||||
progresoInicial,
|
||||
resumen: resumenEjercicios,
|
||||
utilidades: {
|
||||
calcularResultadoShock
|
||||
}
|
||||
};
|
||||
608
frontend/src/content/modulo2/equilibrio.ts
Normal file
608
frontend/src/content/modulo2/equilibrio.ts
Normal file
@@ -0,0 +1,608 @@
|
||||
/**
|
||||
* Módulo 2: Equilibrio de Mercado
|
||||
*
|
||||
* Este módulo cubre el concepto de equilibrio, desequilibrios,
|
||||
* y controles de precios en los mercados.
|
||||
*/
|
||||
|
||||
// ============================================
|
||||
// TIPOS Y ENUMERACIONES
|
||||
// ============================================
|
||||
|
||||
export enum TipoDesequilibrio {
|
||||
EXCESO_OFERTA = 'exceso_oferta', // Superávit
|
||||
EXCESO_DEMANDA = 'exceso_demanda', // Escasez
|
||||
EQUILIBRIO = 'equilibrio'
|
||||
}
|
||||
|
||||
export enum TipoControlPrecio {
|
||||
PRECIO_MAXIMO = 'precio_maximo', // Techo
|
||||
PRECIO_MINIMO = 'precio_minimo', // Piso
|
||||
NINGUNO = 'ninguno'
|
||||
}
|
||||
|
||||
export enum EfectoControlPrecio {
|
||||
ESCASEZ = 'escasez',
|
||||
SUPERAVIT = 'superavit',
|
||||
MERCADO_NEGRO = 'mercado_negro',
|
||||
DESEMPLEO = 'desempleo',
|
||||
RACIONAMIENTO = 'racionamiento',
|
||||
NINGUNO = 'ninguno'
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// INTERFACES
|
||||
// ============================================
|
||||
|
||||
export interface PuntoMercado {
|
||||
precio: number;
|
||||
cantidad: number;
|
||||
}
|
||||
|
||||
export interface EquilibrioMercado {
|
||||
precioEquilibrio: number;
|
||||
cantidadEquilibrio: number;
|
||||
punto: PuntoMercado;
|
||||
excedenteConsumidor: number;
|
||||
excedenteProductor: number;
|
||||
bienestarTotal: number;
|
||||
}
|
||||
|
||||
export interface CurvaMercado {
|
||||
demanda: PuntoMercado[];
|
||||
oferta: PuntoMercado[];
|
||||
}
|
||||
|
||||
export interface Desequilibrio {
|
||||
tipo: TipoDesequilibrio;
|
||||
precioActual: number;
|
||||
cantidadDemandada: number;
|
||||
cantidadOfrecida: number;
|
||||
diferencia: number;
|
||||
magnitud: number;
|
||||
presionPrecio: 'subir' | 'bajar' | 'ninguna';
|
||||
}
|
||||
|
||||
export interface ControlPrecio {
|
||||
tipo: TipoControlPrecio;
|
||||
nivel: number;
|
||||
precioEquilibrio: number;
|
||||
efectivo: boolean;
|
||||
efectos: EfectoControlPrecio[];
|
||||
cantidadTransada: number;
|
||||
perdidaBienestar: number;
|
||||
}
|
||||
|
||||
export interface EjemploEquilibrio {
|
||||
titulo: string;
|
||||
mercado: string;
|
||||
descripcion: string;
|
||||
datos: CurvaMercado;
|
||||
equilibrio: EquilibrioMercado;
|
||||
escenarios: EscenarioDesequilibrio[];
|
||||
}
|
||||
|
||||
export interface EscenarioDesequilibrio {
|
||||
nombre: string;
|
||||
precio: number;
|
||||
tipo: TipoDesequilibrio;
|
||||
explicacion: string;
|
||||
resultado: string;
|
||||
}
|
||||
|
||||
export interface HistoriaPrecio {
|
||||
periodo: string;
|
||||
descripcion: string;
|
||||
precioControl: number;
|
||||
precioEquilibrio: number;
|
||||
consecuencias: string[];
|
||||
lecciones: string;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// CONTENIDO TEÓRICO
|
||||
// ============================================
|
||||
|
||||
export const definicionEquilibrio = {
|
||||
titulo: 'Equilibrio de Mercado',
|
||||
|
||||
definicion: 'El equilibrio de mercado es una situación en la que la cantidad demandada de un bien es igual a la cantidad ofrecida. En este punto, no hay tendencia al cambio: ni compradores ni vendedores tienen incentivo para alterar sus decisiones.',
|
||||
|
||||
caracteristicas: [
|
||||
{
|
||||
caracteristica: 'Cantidad demandada = Cantidad ofrecida',
|
||||
explicacion: 'No hay exceso de oferta ni de demanda'
|
||||
},
|
||||
{
|
||||
caracteristica: 'Precio estable',
|
||||
explicacion: 'No hay presiones para que el precio suba o baje'
|
||||
},
|
||||
{
|
||||
caracteristica: 'Eficiencia',
|
||||
explicacion: 'Se maximiza el bienestar social (excedente total)'
|
||||
},
|
||||
{
|
||||
caracteristica: 'Voluntad de intercambio',
|
||||
explicacion: 'Todos los intercambios mutuamente beneficiosos ocurren'
|
||||
}
|
||||
],
|
||||
|
||||
mecanismoAjuste: {
|
||||
titulo: 'Mecanismo de Ajuste al Equilibrio',
|
||||
proceso: [
|
||||
{
|
||||
paso: 1,
|
||||
situacion: 'Precio por encima del equilibrio',
|
||||
resultado: 'Exceso de oferta (superávit)',
|
||||
ajuste: 'Vendedores compiten reduciendo precios para vender excedentes'
|
||||
},
|
||||
{
|
||||
paso: 2,
|
||||
situacion: 'Precio por debajo del equilibrio',
|
||||
resultado: 'Exceso de demanda (escasez)',
|
||||
ajuste: 'Compradores compiten ofreciendo precios más altos'
|
||||
},
|
||||
{
|
||||
paso: 3,
|
||||
situacion: 'Precio de equilibrio',
|
||||
resultado: 'Cantidad demandada = Cantidad ofrecida',
|
||||
ajuste: 'No hay presiones adicionales; mercado está en equilibrio'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
representacionMatematica: {
|
||||
condicion: 'Qd = Qs',
|
||||
ejemplo: {
|
||||
demanda: 'Qd = 100 - 2P',
|
||||
oferta: 'Qs = 20 + 3P',
|
||||
resolucion: [
|
||||
'100 - 2P = 20 + 3P',
|
||||
'80 = 5P',
|
||||
'P* = 16 (precio de equilibrio)',
|
||||
'Q* = 100 - 2(16) = 68 (cantidad de equilibrio)'
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// EXCEDENTES DEL CONSUMIDOR Y PRODUCTOR
|
||||
// ============================================
|
||||
|
||||
export const excedentesMercado = {
|
||||
titulo: 'Bienestar en el Equilibrio',
|
||||
|
||||
excedenteConsumidor: {
|
||||
nombre: 'Excedente del Consumidor (EC)',
|
||||
definicion: 'Diferencia entre lo que los consumidores están dispuestos a pagar y lo que realmente pagan',
|
||||
formula: 'EC = Valoración - Precio pagado',
|
||||
calculo: 'Área bajo la curva de demanda y arriba del precio de equilibrio',
|
||||
interpretacion: 'Beneficio neto que obtienen los consumidores del intercambio',
|
||||
ejemplo: 'Dispuesto a pagar $50, pago $30 → Excedente = $20'
|
||||
},
|
||||
|
||||
excedenteProductor: {
|
||||
nombre: 'Excedente del Productor (EP)',
|
||||
definicion: 'Diferencia entre el precio que reciben los productores y el costo mínimo al que estarían dispuestos a vender',
|
||||
formula: 'EP = Precio recibido - Costo de producción',
|
||||
calculo: 'Área arriba de la curva de oferta y debajo del precio de equilibrio',
|
||||
interpretacion: 'Beneficio neto que obtienen los productores del intercambio',
|
||||
ejemplo: 'Dispuesto a vender a $20, recibo $30 → Excedente = $10'
|
||||
},
|
||||
|
||||
bienestarTotal: {
|
||||
nombre: 'Bienestar Total (Excedente Total)',
|
||||
definicion: 'Suma de excedentes del consumidor y del productor',
|
||||
formula: 'BT = EC + EP',
|
||||
propiedad: 'En equilibrio competitivo, el bienestar total se maximiza',
|
||||
perdida: 'Cualquier desviación del equilibrio genera pérdida de bienestar'
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// DESEQUILIBRIOS DE MERCADO
|
||||
// ============================================
|
||||
|
||||
export const excesoOferta: Desequilibrio = {
|
||||
tipo: TipoDesequilibrio.EXCESO_OFERTA,
|
||||
precioActual: 25,
|
||||
cantidadDemandada: 40,
|
||||
cantidadOfrecida: 80,
|
||||
diferencia: 40,
|
||||
magnitud: 40,
|
||||
presionPrecio: 'bajar'
|
||||
};
|
||||
|
||||
export const excesoDemanda: Desequilibrio = {
|
||||
tipo: TipoDesequilibrio.EXCESO_DEMANDA,
|
||||
precioActual: 10,
|
||||
cantidadDemandada: 90,
|
||||
cantidadOfrecida: 30,
|
||||
diferencia: 60,
|
||||
magnitud: 60,
|
||||
presionPrecio: 'subir'
|
||||
};
|
||||
|
||||
export const tiposDesequilibrio = {
|
||||
excesoOferta: {
|
||||
nombre: 'Exceso de Oferta (Superávit)',
|
||||
definicion: 'Cantidad ofrecida > Cantidad demandada',
|
||||
causas: [
|
||||
'Precio por encima del equilibrio',
|
||||
'Aumento repentino de producción',
|
||||
'Caída inesperada de la demanda'
|
||||
],
|
||||
consecuencias: [
|
||||
'Acumulación de inventarios',
|
||||
'Presión a la baja en precios',
|
||||
'Posibles quiebras si persiste',
|
||||
'Competencia agresiva entre vendedores'
|
||||
],
|
||||
ejemplos: [
|
||||
'Viviendas sin vender después de una burbuja',
|
||||
'Excedentes agrícolas después de buenas cosechas',
|
||||
'Autos en concesionarias durante recesión'
|
||||
],
|
||||
grafico: {
|
||||
descripcion: 'A P > P*, Qs > Qd',
|
||||
areaSuperavit: 'Distancia horizontal entre curvas al precio dado'
|
||||
}
|
||||
},
|
||||
|
||||
excesoDemanda: {
|
||||
nombre: 'Exceso de Demanda (Escasez)',
|
||||
definicion: 'Cantidad demandada > Cantidad ofrecida',
|
||||
causas: [
|
||||
'Precio por debajo del equilibrio',
|
||||
'Aumento repentino de la demanda',
|
||||
'Caída inesperada de la oferta'
|
||||
],
|
||||
consecuencias: [
|
||||
'Colas y listas de espera',
|
||||
'Presión al alza en precios',
|
||||
'Racionamiento de productos',
|
||||
'Mercados negros',
|
||||
'Malestar social'
|
||||
],
|
||||
ejemplos: [
|
||||
'Gasolina en crisis (colas en bomba)',
|
||||
'Entradas para conciertos populares',
|
||||
'Vivienda asequible en ciudades caras',
|
||||
'Productos básicos con precios controlados'
|
||||
],
|
||||
grafico: {
|
||||
descripcion: 'A P < P*, Qd > Qs',
|
||||
areaEscasez: 'Distancia horizontal entre curvas al precio dado'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// CONTROLES DE PRECIO
|
||||
// ============================================
|
||||
|
||||
export const controlesPrecio = {
|
||||
titulo: 'Controles de Precio',
|
||||
|
||||
introduccion: 'Los gobiernos a veces intervienen estableciendo precios máximos o mínimos que difieren del precio de equilibrio de mercado.',
|
||||
|
||||
precioMaximo: {
|
||||
nombre: 'Precio Máximo (Techo)',
|
||||
definicion: 'Precio legal más alto al que se puede vender un bien',
|
||||
condicionEfectivo: 'Debe estar DEBAJO del precio de equilibrio',
|
||||
efectos: {
|
||||
cuandoEsEfectivo: [
|
||||
'Escasez persistente (Qd > Qs)',
|
||||
'Racionamiento del bien',
|
||||
'Colas y esperas',
|
||||
'Mercados negros',
|
||||
'Reducción de calidad',
|
||||
'Pérdida de bienestar'
|
||||
],
|
||||
cuandoNoEsEfectivo: [
|
||||
'Precio máximo > precio de equilibrio',
|
||||
'Mercado opera normalmente',
|
||||
'Sin efectos sobre cantidad transada'
|
||||
]
|
||||
},
|
||||
ejemplos: [
|
||||
{ caso: 'Alquileres', ubicacion: 'Nueva York, San Francisco', resultado: 'Escasez de vivienda, subarriendos' },
|
||||
{ caso: 'Gasolina', ubicacion: 'Estados Unidos 1970s', resultado: 'Largas colas, mercado negro' },
|
||||
{ caso: 'Productos básicos', ubicacion: 'Venezuela', resultado: 'Desabastecimiento, contrabando' }
|
||||
]
|
||||
},
|
||||
|
||||
precioMinimo: {
|
||||
nombre: 'Precio Mínimo (Piso)',
|
||||
definicion: 'Precio legal más bajo al que se puede vender un bien',
|
||||
condicionEfectivo: 'Debe estar ARRIBA del precio de equilibrio',
|
||||
efectos: {
|
||||
cuandoEsEfectivo: [
|
||||
'Superávit persistente (Qs > Qd)',
|
||||
'Acumulación de inventarios',
|
||||
'Desperdicio de recursos',
|
||||
'Mercados negros (venta a precio menor)',
|
||||
'Pérdida de bienestar'
|
||||
],
|
||||
cuandoNoEsEfectivo: [
|
||||
'Precio mínimo < precio de equilibrio',
|
||||
'Mercado opera normalmente',
|
||||
'Sin efectos sobre cantidad transada'
|
||||
]
|
||||
},
|
||||
ejemplos: [
|
||||
{ caso: 'Salario mínimo', ubicacion: 'Mayoría de países', resultado: 'Desempleo potencial en trabajadores no calificados' },
|
||||
{ caso: 'Precios agrícolas', ubicacion: 'Unión Europea', resultado: 'Superávits, gasto gubernamental' },
|
||||
{ caso: 'Alcohol/tabaco', ubicacion: 'Políticas de salud', resultado: 'Menor consumo, contrabando' }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// EJEMPLOS DE CONTROLES HISTÓRICOS
|
||||
// ============================================
|
||||
|
||||
export const historiasControlesPrecio: HistoriaPrecio[] = [
|
||||
{
|
||||
periodo: '1971-1974',
|
||||
descripcion: 'Control de precios de gasolina en EE.UU.',
|
||||
precioControl: 0.36,
|
||||
precioEquilibrio: 0.55,
|
||||
consecuencias: [
|
||||
'Largas colas en gasolineras (hasta 4 horas)',
|
||||
'Racionamiento por día par/impar',
|
||||
'Mercado negro de gasolina',
|
||||
'Violencia en gasolineras',
|
||||
'Desabastecimiento regional'
|
||||
],
|
||||
lecciones: 'Los controles de precios generan escasez cuando están por debajo del equilibrio'
|
||||
},
|
||||
{
|
||||
periodo: '1946-1947',
|
||||
descripcion: 'Control de alquileres en Nueva York',
|
||||
precioControl: 75,
|
||||
precioEquilibrio: 100,
|
||||
consecuencias: [
|
||||
'Reducción de mantenimiento de edificios',
|
||||
'Conversión de apartamentos a condominios',
|
||||
'Mercado negro (pagos bajo mesa)',
|
||||
'Escasez crónica de vivienda',
|
||||
'Subarriendos a precios más altos'
|
||||
],
|
||||
lecciones: 'Precios máximos reducen la calidad y cantidad de oferta a largo plazo'
|
||||
},
|
||||
{
|
||||
periodo: 'Actual',
|
||||
descripcion: 'Salario mínimo en diferentes países',
|
||||
precioControl: 15,
|
||||
precioEquilibrio: 12,
|
||||
consecuencias: [
|
||||
'Reducción de contratación de jóvenes',
|
||||
'Automatización de trabajos (kioscos)',
|
||||
'Reducción de horas trabajadas',
|
||||
'Beneficio para trabajadores que mantienen empleo',
|
||||
'Posible aumento de precios'
|
||||
],
|
||||
lecciones: 'Los precios mínimos crean desempleo cuando están por encima del equilibrio, pero benefician a quienes mantienen el empleo'
|
||||
}
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// EJEMPLOS PRÁCTICOS DE EQUILIBRIO
|
||||
// ============================================
|
||||
|
||||
export const ejemplosEquilibrio: EjemploEquilibrio[] = [
|
||||
{
|
||||
titulo: 'Mercado de Manzanas',
|
||||
mercado: 'Manzanas',
|
||||
descripcion: 'Análisis del equilibrio en un mercado agrícola simple',
|
||||
datos: {
|
||||
demanda: [
|
||||
{ precio: 1, cantidad: 90 },
|
||||
{ precio: 2, cantidad: 80 },
|
||||
{ precio: 3, cantidad: 70 },
|
||||
{ precio: 4, cantidad: 60 },
|
||||
{ precio: 5, cantidad: 50 }
|
||||
],
|
||||
oferta: [
|
||||
{ precio: 1, cantidad: 10 },
|
||||
{ precio: 2, cantidad: 30 },
|
||||
{ precio: 3, cantidad: 50 },
|
||||
{ precio: 4, cantidad: 70 },
|
||||
{ precio: 5, cantidad: 90 }
|
||||
]
|
||||
},
|
||||
equilibrio: {
|
||||
precioEquilibrio: 3.5,
|
||||
cantidadEquilibrio: 60,
|
||||
punto: { precio: 3.5, cantidad: 60 },
|
||||
excedenteConsumidor: 45,
|
||||
excedenteProductor: 45,
|
||||
bienestarTotal: 90
|
||||
},
|
||||
escenarios: [
|
||||
{
|
||||
nombre: 'Precio por encima del equilibrio',
|
||||
precio: 5,
|
||||
tipo: TipoDesequilibrio.EXCESO_OFERTA,
|
||||
explicacion: 'A $5, los productores ofrecen 90 unidades pero solo se demandan 50',
|
||||
resultado: 'Superávit de 40 unidades. Presión a la baja en precios.'
|
||||
},
|
||||
{
|
||||
nombre: 'Precio por debajo del equilibrio',
|
||||
precio: 2,
|
||||
tipo: TipoDesequilibrio.EXCESO_DEMANDA,
|
||||
explicacion: 'A $2, se demandan 80 unidades pero solo se ofrecen 30',
|
||||
resultado: 'Escasez de 50 unidades. Colas y presión al alza.'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
titulo: 'Mercado Laboral: Desempleo',
|
||||
mercado: 'Trabajo no calificado',
|
||||
descripcion: 'Efecto del salario mínimo en el empleo',
|
||||
datos: {
|
||||
demanda: [
|
||||
{ precio: 4, cantidad: 100 },
|
||||
{ precio: 6, cantidad: 80 },
|
||||
{ precio: 8, cantidad: 60 },
|
||||
{ precio: 10, cantidad: 40 },
|
||||
{ precio: 12, cantidad: 20 }
|
||||
],
|
||||
oferta: [
|
||||
{ precio: 4, cantidad: 20 },
|
||||
{ precio: 6, cantidad: 40 },
|
||||
{ precio: 8, cantidad: 60 },
|
||||
{ precio: 10, cantidad: 80 },
|
||||
{ precio: 12, cantidad: 100 }
|
||||
]
|
||||
},
|
||||
equilibrio: {
|
||||
precioEquilibrio: 8,
|
||||
cantidadEquilibrio: 60,
|
||||
punto: { precio: 8, cantidad: 60 },
|
||||
excedenteConsumidor: 180,
|
||||
excedenteProductor: 180,
|
||||
bienestarTotal: 360
|
||||
},
|
||||
escenarios: [
|
||||
{
|
||||
nombre: 'Salario mínimo efectivo',
|
||||
precio: 12,
|
||||
tipo: TipoDesequilibrio.EXCESO_OFERTA,
|
||||
explicacion: 'Salario mínimo de $12 está por encima del equilibrio de $8',
|
||||
resultado: '100 personas buscan trabajo, pero solo 20 empleos. Desempleo de 80 personas.'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// TABLA COMPARATIVA: CONTROLES DE PRECIO
|
||||
// ============================================
|
||||
|
||||
export const tablaComparativaControles = {
|
||||
titulo: 'Comparación de Controles de Precio',
|
||||
|
||||
filas: [
|
||||
{
|
||||
caracteristica: 'Nombre',
|
||||
precioMaximo: 'Techo, Precio Máximo',
|
||||
precioMinimo: 'Piso, Precio Mínimo'
|
||||
},
|
||||
{
|
||||
caracteristica: 'Ubicación',
|
||||
precioMaximo: 'Debajo del equilibrio',
|
||||
precioMinimo: 'Arriba del equilibrio'
|
||||
},
|
||||
{
|
||||
caracteristica: 'Desequilibrio',
|
||||
precioMaximo: 'Exceso de demanda (escasez)',
|
||||
precioMinimo: 'Exceso de oferta (superávit)'
|
||||
},
|
||||
{
|
||||
caracteristica: 'Cantidad transada',
|
||||
precioMaximo: 'Determinada por oferta (Qs)',
|
||||
precioMinimo: 'Determinada por demanda (Qd)'
|
||||
},
|
||||
{
|
||||
caracteristica: 'Efectos secundarios',
|
||||
precioMaximo: 'Colas, mercado negro, baja calidad',
|
||||
precioMinimo: 'Inventarios, desperdicio, mercado negro'
|
||||
},
|
||||
{
|
||||
caracteristica: 'Ejemplo común',
|
||||
precioMaximo: 'Alquileres, gasolina',
|
||||
precioMinimo: 'Salarios mínimos, precios agrícolas'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// PÉRDIDA DE BIENESTAR (DEADWEIGHT LOSS)
|
||||
// ============================================
|
||||
|
||||
export const perdidaBienestar = {
|
||||
titulo: 'Pérdida de Eficiencia por Controles',
|
||||
|
||||
definicion: 'La pérdida de peso muerto (deadweight loss) es la reducción en el bienestar total que ocurre cuando el mercado no alcanza el equilibrio. Representa transacciones mutuamente beneficiosas que no ocurren.',
|
||||
|
||||
calculoPrecioMaximo: {
|
||||
pasos: [
|
||||
'En equilibrio: BT = EC + EP',
|
||||
'Con precio máximo: BT_control = EC_nuevo + EP_nuevo',
|
||||
'Pérdida = BT_equilibrio - BT_control',
|
||||
'Representada por el triángulo entre cantidad transada y cantidad de equilibrio'
|
||||
],
|
||||
areas: [
|
||||
'Pérdida de excedente del consumidor (transacciones perdidas)',
|
||||
'Pérdida de excedente del productor (transacciones perdidas)',
|
||||
'Área total: triángulo entre curvas de oferta y demanda'
|
||||
]
|
||||
},
|
||||
|
||||
calculoPrecioMinimo: {
|
||||
pasos: [
|
||||
'En equilibrio: BT = EC + EP',
|
||||
'Con precio mínimo: BT_control = EC_nuevo + EP_nuevo',
|
||||
'Pérdida = BT_equilibrio - BT_control',
|
||||
'Representada por el triángulo entre cantidad demandada y cantidad de equilibrio'
|
||||
],
|
||||
areas: [
|
||||
'Pérdida por producción ineficiente (costos > beneficios)',
|
||||
'Pérdida por consumo perdido (beneficios > costos)',
|
||||
'Área total: triángulo entre curvas de oferta y demanda'
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// RESUMEN
|
||||
// ============================================
|
||||
|
||||
export const resumenEquilibrio = {
|
||||
titulo: 'Resumen: Equilibrio de Mercado',
|
||||
|
||||
puntosClave: [
|
||||
'Equilibrio: Qd = Qs, sin tendencia al cambio',
|
||||
'Precio de equilibrio estable sin presiones',
|
||||
'Exceso de oferta: P > P* → presión a la baja',
|
||||
'Exceso de demanda: P < P* → presión al alza',
|
||||
'Precio máximo efectivo: P_max < P* → escasez',
|
||||
'Precio mínimo efectivo: P_min > P* → superávit',
|
||||
'Controles de precio generan pérdida de bienestar',
|
||||
'Mercados tienden al equilibrio mediante ajustes de precio'
|
||||
],
|
||||
|
||||
formulasRecordatorio: {
|
||||
condicionEquilibrio: 'Qd = Qs',
|
||||
excesoOferta: 'Qs - Qd > 0 cuando P > P*',
|
||||
excesoDemanda: 'Qd - Qs > 0 cuando P < P*',
|
||||
bienestarTotal: 'BT = EC + EP',
|
||||
perdidaBienestar: 'DWL = BT_equilibrio - BT_control'
|
||||
},
|
||||
|
||||
mapaConceptual: {
|
||||
centro: 'Equilibrio',
|
||||
ramas: [
|
||||
{ nombre: 'Condición', elementos: ['Qd = Qs', 'P estable', 'Eficiencia máxima'] },
|
||||
{ nombre: 'Desequilibrios', elementos: ['Exceso oferta (superávit)', 'Exceso demanda (escasez)'] },
|
||||
{ nombre: 'Controles', elementos: ['Precio máximo → escasez', 'Precio mínimo → superávit'] },
|
||||
{ nombre: 'Bienestar', elementos: ['Excedente consumidor', 'Excedente productor', 'Pérdida peso muerto'] }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// Exportación por defecto
|
||||
export default {
|
||||
definicion: definicionEquilibrio,
|
||||
excedentes: excedentesMercado,
|
||||
desequilibrios: tiposDesequilibrio,
|
||||
controles: controlesPrecio,
|
||||
historias: historiasControlesPrecio,
|
||||
ejemplos: ejemplosEquilibrio,
|
||||
comparativa: tablaComparativaControles,
|
||||
perdidaBienestar,
|
||||
resumen: resumenEquilibrio
|
||||
};
|
||||
487
frontend/src/content/modulo2/oferta.ts
Normal file
487
frontend/src/content/modulo2/oferta.ts
Normal file
@@ -0,0 +1,487 @@
|
||||
/**
|
||||
* Módulo 2: Ley de la Oferta
|
||||
*
|
||||
* Este módulo cubre los fundamentos de la oferta en economía,
|
||||
* incluyendo la ley de la oferta, factores determinantes y
|
||||
* comportamiento de los productores.
|
||||
*/
|
||||
|
||||
// ============================================
|
||||
// TIPOS Y ENUMERACIONES
|
||||
// ============================================
|
||||
|
||||
export enum DireccionDesplazamientoOferta {
|
||||
IZQUIERDA = 'izquierda', // Disminución de oferta
|
||||
DERECHA = 'derecha', // Aumento de oferta
|
||||
NINGUNO = 'ninguno' // Sin cambio
|
||||
}
|
||||
|
||||
export enum HorizonteTemporal {
|
||||
CORTO_PLAZO = 'corto_plazo',
|
||||
LARGO_PLAZO = 'largo_plazo'
|
||||
}
|
||||
|
||||
export enum TipoMercado {
|
||||
COMPETENCIA_PERFECTA = 'competencia_perfecta',
|
||||
MONOPOLIO = 'monopolio',
|
||||
OLIGOPOLIO = 'oligopolio',
|
||||
COMPETENCIA_MONOPOLISTICA = 'competencia_monopolistica'
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// INTERFACES
|
||||
// ============================================
|
||||
|
||||
export interface PuntoOferta {
|
||||
precio: number;
|
||||
cantidad: number;
|
||||
}
|
||||
|
||||
export interface CurvaOferta {
|
||||
id: string;
|
||||
nombre: string;
|
||||
puntos: PuntoOferta[];
|
||||
descripcion: string;
|
||||
horizonteTemporal: HorizonteTemporal;
|
||||
}
|
||||
|
||||
export interface FactorDesplazamientoOferta {
|
||||
nombre: string;
|
||||
descripcion: string;
|
||||
direccion: DireccionDesplazamientoOferta;
|
||||
ejemplo: string;
|
||||
icono: string;
|
||||
mecanismo: string;
|
||||
}
|
||||
|
||||
export interface CostoProduccion {
|
||||
categoria: string;
|
||||
componentes: string[];
|
||||
impactoOferta: string;
|
||||
}
|
||||
|
||||
export interface EjemploOferta {
|
||||
titulo: string;
|
||||
bien: string;
|
||||
escenario: string;
|
||||
explicacion: string;
|
||||
graficoData: PuntoOferta[];
|
||||
impactoEconomico: string;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// CONTENIDO TEÓRICO
|
||||
// ============================================
|
||||
|
||||
export const definicionOferta = {
|
||||
titulo: 'Definición de Oferta',
|
||||
|
||||
definicion: 'La oferta es la cantidad de un bien o servicio que los productores están dispuestos y pueden ofrecer al mercado a diferentes precios durante un período específico, manteniendo constantes otros factores.',
|
||||
|
||||
elementosClave: [
|
||||
{
|
||||
elemento: 'Disposición a vender',
|
||||
descripcion: 'El productor debe querer ofrecer el bien (rentabilidad)'
|
||||
},
|
||||
{
|
||||
elemento: 'Capacidad de producción',
|
||||
descripcion: 'El productor debe tener los recursos para producir'
|
||||
},
|
||||
{
|
||||
elemento: 'Precios variables',
|
||||
descripcion: 'Se analiza la relación a diferentes niveles de precio'
|
||||
},
|
||||
{
|
||||
elemento: 'Período de tiempo',
|
||||
descripcion: 'La oferta siempre se refiere a un período específico'
|
||||
}
|
||||
],
|
||||
|
||||
diferenciaCapacidad: {
|
||||
capacidad: 'Puedo producir 1000 unidades (capacidad técnica)',
|
||||
oferta: 'Estoy dispuesto a ofrecer 800 unidades a $10 porque es rentable'
|
||||
}
|
||||
};
|
||||
|
||||
export const leyOferta = {
|
||||
titulo: 'Ley de la Oferta',
|
||||
|
||||
enunciado: 'Existe una relación directa entre el precio de un bien y la cantidad ofrecida: cuando el precio aumenta, la cantidad ofrecida aumenta, y viceversa.',
|
||||
|
||||
explicacion: 'Esta relación directa se explica por:',
|
||||
|
||||
razones: [
|
||||
{
|
||||
nombre: 'Motivación de lucro',
|
||||
descripcion: 'A precios más altos, la producción es más rentable, incentivando a los productores a aumentar la oferta.',
|
||||
ejemplo: 'Si el precio de las manzanas sube a $5/kg, más agricultores querrán producir manzanas.'
|
||||
},
|
||||
{
|
||||
nombre: 'Costos crecientes',
|
||||
descripcion: 'Para producir más, las empresas deben usar recursos menos eficientes o pagar costos más altos por factores adicionales.',
|
||||
ejemplo: 'Para cultivar más trigo, se deben usar tierras menos fértiles que requieren más insumos.'
|
||||
},
|
||||
{
|
||||
nombre: 'Entrada de nuevos productores',
|
||||
descripcion: 'Precios más altos atraen a nuevos productores al mercado, aumentando la oferta total.',
|
||||
ejemplo: 'Si el café está caro, agricultores de otras zonas comienzan a cultivar café.'
|
||||
}
|
||||
],
|
||||
|
||||
representacionMatematica: {
|
||||
funcion: 'Qs = f(P)',
|
||||
donde: {
|
||||
Qs: 'Cantidad ofrecida',
|
||||
P: 'Precio del bien',
|
||||
f: 'Función creciente (pendiente positiva)'
|
||||
},
|
||||
ejemploLineal: 'Qs = 20 + 3P',
|
||||
interpretacion: 'Por cada aumento de $1 en el precio, la cantidad ofrecida aumenta en 3 unidades.'
|
||||
},
|
||||
|
||||
excepciones: [
|
||||
{
|
||||
caso: 'Bienes de especulación',
|
||||
descripcion: 'Si los productores esperan precios aún más altos en el futuro, pueden reducir la oferta actual.',
|
||||
ejemplo: 'Productores de petróleo reducen oferta actual esperando precios más altos.'
|
||||
},
|
||||
{
|
||||
caso: 'Bienes de lujo exclusivo',
|
||||
descripcion: 'Para mantener exclusividad, productores pueden limitar cantidad aunque el precio sea alto.',
|
||||
ejemplo: 'Relojes suizos de lujo mantienen producción limitada a pesar de alta demanda.'
|
||||
},
|
||||
{
|
||||
caso: 'Trabajo (curva de oferta retrógrada)',
|
||||
descripcion: 'Muy altos salarios pueden reducir horas trabajadas (preferencia por ocio).',
|
||||
ejemplo: 'Médicos especialistas trabajan menos horas cuando ganan suficiente.'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// FACTORES QUE DESPLAZAN LA CURVA DE OFERTA
|
||||
// ============================================
|
||||
|
||||
export const factoresDesplazamientoOferta: FactorDesplazamientoOferta[] = [
|
||||
{
|
||||
nombre: 'Tecnología',
|
||||
descripcion: 'Avances tecnológicos que mejoran la productividad',
|
||||
direccion: DireccionDesplazamientoOferta.DERECHA,
|
||||
mecanismo: 'Reduce costos unitarios, permite producir más con mismos recursos',
|
||||
ejemplo: 'Nuevas máquinas de coser automáticas duplican la producción de ropa',
|
||||
icono: '⚙️'
|
||||
},
|
||||
{
|
||||
nombre: 'Precio de insumos',
|
||||
descripcion: 'Cambios en el costo de materias primas, mano de obra o capital',
|
||||
direccion: DireccionDesplazamientoOferta.IZQUIERDA,
|
||||
mecanismo: 'Aumenta costos de producción, reduce rentabilidad',
|
||||
ejemplo: 'Subida del precio del petróleo aumenta costos de transporte y plásticos',
|
||||
icono: '⛽'
|
||||
},
|
||||
{
|
||||
nombre: 'Número de vendedores',
|
||||
descripcion: 'Entrada o salida de empresas del mercado',
|
||||
direccion: DireccionDesplazamientoOferta.DERECHA,
|
||||
mecanismo: 'Más productores = más oferta total en el mercado',
|
||||
ejemplo: 'Eliminación de aranceles permite entrada de productores extranjeros',
|
||||
icono: '🏭'
|
||||
},
|
||||
{
|
||||
nombre: 'Expectativas de precios',
|
||||
descripcion: 'Expectativas sobre precios futuros del bien',
|
||||
direccion: DireccionDesplazamientoOferta.IZQUIERDA,
|
||||
mecanismo: 'Si esperan precios más altos futuros, reducen oferta actual',
|
||||
ejemplo: 'Agricultores almacenan granos esperando precios más altos en invierno',
|
||||
icono: '📈'
|
||||
},
|
||||
{
|
||||
nombre: 'Impuestos y subsidios',
|
||||
descripcion: 'Políticas gubernamentales que afectan costos',
|
||||
direccion: DireccionDesplazamientoOferta.IZQUIERDA, // Para impuestos
|
||||
mecanismo: 'Impuestos aumentan costos; subsidios reducen costos',
|
||||
ejemplo: 'Nuevo impuesto al tabaco reduce oferta; subsidio a energías renovables aumenta oferta',
|
||||
icono: '🏛️'
|
||||
},
|
||||
{
|
||||
nombre: 'Condiciones naturales',
|
||||
descripcion: 'Eventos climáticos, desastres naturales o condiciones ambientales',
|
||||
direccion: DireccionDesplazamientoOferta.IZQUIERDA,
|
||||
mecanismo: 'Afecta capacidad productiva de sectores agrícolas o naturales',
|
||||
ejemplo: 'Sequía reduce cosecha de trigo; huracán afecta producción petrolera',
|
||||
icono: '🌪️'
|
||||
},
|
||||
{
|
||||
nombre: 'Regulaciones gubernamentales',
|
||||
descripcion: 'Normativas ambientales, laborales o de producción',
|
||||
direccion: DireccionDesplazamientoOferta.IZQUIERDA,
|
||||
mecanismo: 'Mayores requisitos aumentan costos de cumplimiento',
|
||||
ejemplo: 'Nuevas normas ambientales requieren filtros costosos en fábricas',
|
||||
icono: '📋'
|
||||
}
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// COSTOS DE PRODUCCIÓN
|
||||
// ============================================
|
||||
|
||||
export const costosProduccion = {
|
||||
titulo: 'Costos de Producción y Oferta',
|
||||
|
||||
introduccion: 'Los costos son fundamentales para entender las decisiones de oferta. Los productores maximizan ganancias donde Ingreso Marginal = Costo Marginal.',
|
||||
|
||||
categorias: [
|
||||
{
|
||||
categoria: 'Costos Fijos (CF)',
|
||||
definicion: 'Costos que no varían con la cantidad producida',
|
||||
ejemplos: ['Alquiler de local', 'Seguros', 'Salarios administrativos', 'Licencias'],
|
||||
ejemplosCantidad: 'Ej: $10,000 mensuales sin importar producción'
|
||||
},
|
||||
{
|
||||
categoria: 'Costos Variables (CV)',
|
||||
definicion: 'Costos que varían directamente con la producción',
|
||||
ejemplos: ['Materias primas', 'Mano de obra directa', 'Energía productiva', 'Envases'],
|
||||
ejemplosCantidad: 'Ej: $5 por unidad producida'
|
||||
},
|
||||
{
|
||||
categoria: 'Costos Totales (CT)',
|
||||
definicion: 'Suma de costos fijos y variables',
|
||||
formula: 'CT = CF + CV',
|
||||
ejemplosCantidad: 'Ej: CT = $10,000 + $5 × Q'
|
||||
}
|
||||
],
|
||||
|
||||
costosMarginales: {
|
||||
nombre: 'Costo Marginal (CM)',
|
||||
definicion: 'Costo adicional de producir una unidad más',
|
||||
importancia: 'Determina la curva de oferta del productor',
|
||||
relacionOferta: 'El productor ofrecerá cantidades donde P ≥ CM',
|
||||
formula: 'CM = ΔCT / ΔQ'
|
||||
},
|
||||
|
||||
tablaEjemplo: [
|
||||
{ q: 0, cf: 100, cv: 0, ct: 100, cme: '-', cmg: '-' },
|
||||
{ q: 1, cf: 100, cv: 50, ct: 150, cme: 150, cmg: 50 },
|
||||
{ q: 2, cf: 100, cv: 90, ct: 190, cme: 95, cmg: 40 },
|
||||
{ q: 3, cf: 100, cv: 140, ct: 240, cme: 80, cmg: 50 },
|
||||
{ q: 4, cf: 100, cv: 220, ct: 320, cme: 80, cmg: 80 },
|
||||
{ q: 5, cf: 100, cv: 340, ct: 440, cme: 88, cmg: 120 }
|
||||
]
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// CURVAS DE OFERTA
|
||||
// ============================================
|
||||
|
||||
export const curvaOfertaIndividual: CurvaOferta = {
|
||||
id: 'oferta-individual',
|
||||
nombre: 'Curva de Oferta Individual',
|
||||
descripcion: 'Muestra la relación entre precio y cantidad ofrecida por un solo productor',
|
||||
horizonteTemporal: HorizonteTemporal.CORTO_PLAZO,
|
||||
puntos: [
|
||||
{ precio: 2, cantidad: 10 },
|
||||
{ precio: 4, cantidad: 30 },
|
||||
{ precio: 6, cantidad: 55 },
|
||||
{ precio: 8, cantidad: 85 },
|
||||
{ precio: 10, cantidad: 120 }
|
||||
]
|
||||
};
|
||||
|
||||
export const curvaOfertaMercado: CurvaOferta = {
|
||||
id: 'oferta-mercado',
|
||||
nombre: 'Curva de Oferta de Mercado',
|
||||
descripcion: 'Suma horizontal de todas las ofertas individuales en el mercado',
|
||||
horizonteTemporal: HorizonteTemporal.CORTO_PLAZO,
|
||||
puntos: [
|
||||
{ precio: 2, cantidad: 1000 },
|
||||
{ precio: 4, cantidad: 3000 },
|
||||
{ precio: 6, cantidad: 5500 },
|
||||
{ precio: 8, cantidad: 8500 },
|
||||
{ precio: 10, cantidad: 12000 }
|
||||
]
|
||||
};
|
||||
|
||||
export const curvaOfertaLargoPlazo: CurvaOferta = {
|
||||
id: 'oferta-largo-plazo',
|
||||
nombre: 'Curva de Oferta a Largo Plazo',
|
||||
descripcion: 'A largo plazo, más elástica debido a la entrada/salida de empresas',
|
||||
horizonteTemporal: HorizonteTemporal.LARGO_PLAZO,
|
||||
puntos: [
|
||||
{ precio: 2, cantidad: 500 },
|
||||
{ precio: 3, cantidad: 2000 },
|
||||
{ precio: 4, cantidad: 5000 },
|
||||
{ precio: 5, cantidad: 10000 },
|
||||
{ precio: 6, cantidad: 18000 }
|
||||
]
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// EJEMPLOS PRÁCTICOS
|
||||
// ============================================
|
||||
|
||||
export const ejemplosOferta: EjemploOferta[] = [
|
||||
{
|
||||
titulo: 'Tecnología en Manufactura',
|
||||
bien: 'Smartphones',
|
||||
escenario: 'Implementación de robots en línea de ensamblaje reduce tiempo de producción en 40%',
|
||||
explicacion: 'El avance tecnológico desplaza la curva de oferta a la derecha. A cada precio, los productores pueden ofrecer más unidades porque sus costos unitarios han disminuido.',
|
||||
graficoData: [
|
||||
{ precio: 400, cantidad: 5000 },
|
||||
{ precio: 350, cantidad: 7000 },
|
||||
{ precio: 300, cantidad: 9500 },
|
||||
{ precio: 250, cantidad: 12000 },
|
||||
{ precio: 200, cantidad: 15000 }
|
||||
],
|
||||
impactoEconomico: 'Precios más bajos para consumidores y mayor acceso tecnológico'
|
||||
},
|
||||
{
|
||||
titulo: 'Shock de Insumos: Petróleo',
|
||||
bien: 'Gasolina',
|
||||
escenario: 'Conflicto geopolítico reduce exportaciones de petróleo crudo en 30%',
|
||||
explicacion: 'El aumento del precio de la materia prima (petróleo) desplaza la curva de oferta de gasolina a la izquierda. Es más costoso producir gasolina.',
|
||||
graficoData: [
|
||||
{ precio: 5, cantidad: 8000 },
|
||||
{ precio: 6, cantidad: 6500 },
|
||||
{ precio: 7, cantidad: 5000 },
|
||||
{ precio: 8, cantidad: 3500 },
|
||||
{ precio: 9, cantidad: 2000 }
|
||||
],
|
||||
impactoEconomico: 'Aumento de precios en transporte y productos derivados'
|
||||
},
|
||||
{
|
||||
titulo: 'Entrada de Nuevos Productores',
|
||||
bien: 'Café de especialidad',
|
||||
escenario: 'Eliminación de barreras comerciales permite importación de café de nuevos países',
|
||||
explicacion: 'La entrada de más productores al mercado aumenta la oferta total. La curva se desplaza a la derecha, beneficiando a los consumidores con más opciones.',
|
||||
graficoData: [
|
||||
{ precio: 20, cantidad: 1000 },
|
||||
{ precio: 18, cantidad: 1800 },
|
||||
{ precio: 16, cantidad: 2800 },
|
||||
{ precio: 14, cantidad: 4000 },
|
||||
{ precio: 12, cantidad: 5500 }
|
||||
],
|
||||
impactoEconomico: 'Mayor diversidad de productos y presión a la baja en precios'
|
||||
}
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// OFERTA EN DIFERENTES HORIZONTES TEMPORALES
|
||||
// ============================================
|
||||
|
||||
export const ofertaTemporal = {
|
||||
titulo: 'Oferta: Corto vs Largo Plazo',
|
||||
|
||||
cortoPlazo: {
|
||||
definicion: 'Período en el que al menos un factor de producción es fijo',
|
||||
caracteristicas: [
|
||||
'Capacidad productiva limitada',
|
||||
'No puede entrar/salir de empresas',
|
||||
'Curva de oferta más inclinada (inelástica)',
|
||||
'Ajustes principalmente en intensidad de uso'
|
||||
],
|
||||
ejemplo: 'Una fábrica puede aumentar producción con turnos extra, pero no construir nuevas plantas'
|
||||
},
|
||||
|
||||
largoPlazo: {
|
||||
definicion: 'Período en el que todos los factores de producción son variables',
|
||||
caracteristicas: [
|
||||
'Capacidad productiva ajustable',
|
||||
'Entrada y salida de empresas',
|
||||
'Curva de oferta más plana (elástica)',
|
||||
'Ajustes en escala y número de productores'
|
||||
],
|
||||
ejemplo: 'Nuevas fábricas se construyen, tecnologías cambian, empresas entran o salen del mercado'
|
||||
},
|
||||
|
||||
comparacionElasticidad: {
|
||||
cortoPlazo: 'Inelástica: dificultad para ajustar producción rápidamente',
|
||||
largoPlazo: 'Elástica: tiempo suficiente para todos los ajustes',
|
||||
ejemploAgricultura: 'Corto plazo: usar fertilizantes. Largo plazo: cultivar más tierra.'
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// MOVIMIENTO VS DESPLAZAMIENTO
|
||||
// ============================================
|
||||
|
||||
export const diferenciaMovimientoDesplazamientoOferta = {
|
||||
titulo: 'Movimiento a lo largo vs Desplazamiento de la curva de oferta',
|
||||
|
||||
movimiento: {
|
||||
nombre: 'Movimiento a lo largo de la curva',
|
||||
causa: 'Cambio en el precio del propio bien',
|
||||
efecto: 'Cambio en la cantidad ofrecida',
|
||||
direccion: 'Subida o bajada por la misma curva',
|
||||
ejemplo: 'El precio del trigo sube de $5 a $7 → agricultores ofrecen más trigo',
|
||||
representacion: 'Movimiento de un punto a otro en la misma curva'
|
||||
},
|
||||
|
||||
desplazamiento: {
|
||||
nombre: 'Desplazamiento de la curva',
|
||||
causa: 'Cambio en factores distintos al precio (tecnología, costos, regulaciones)',
|
||||
efecto: 'Cambio en la oferta (toda la curva se mueve)',
|
||||
direccionDerecha: 'Aumento de oferta (más cantidad a cada precio)',
|
||||
direccionIzquierda: 'Disminución de oferta (menos cantidad a cada precio)',
|
||||
ejemplo: 'Nueva tecnología reduce costos → más oferta a todos los precios',
|
||||
representacion: 'Curva completa se desplaza'
|
||||
},
|
||||
|
||||
tablaComparativa: [
|
||||
{ concepto: 'Causa', movimiento: 'Precio del bien cambia', desplazamiento: 'Otros factores cambian' },
|
||||
{ concepto: 'Gráfico', movimiento: 'Nos movemos sobre la curva', desplazamiento: 'Curva se desplaza' },
|
||||
{ concepto: 'Terminología', movimiento: 'Cambio en cantidad ofrecida', desplazamiento: 'Cambio en oferta' },
|
||||
{ concepto: 'Ejemplo', movimiento: 'Precio de manzanas ↑', desplazamiento: 'Tecnología mejora ↑' }
|
||||
]
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// RESUMEN Y PUNTOS CLAVE
|
||||
// ============================================
|
||||
|
||||
export const resumenOferta = {
|
||||
titulo: 'Resumen: Oferta',
|
||||
|
||||
puntosClave: [
|
||||
'La oferta requiere disposición Y capacidad de producir',
|
||||
'La ley de la oferta establece relación directa precio-cantidad',
|
||||
'La curva de oferta tiene pendiente positiva',
|
||||
'La oferta se desplaza por cambios en costos, tecnología, número de vendedores',
|
||||
'Tecnología mejora → oferta aumenta (desplazamiento derecha)',
|
||||
'Costos de insumos suben → oferta disminuye (desplazamiento izquierda)',
|
||||
'A largo plazo, la oferta es más elástica que a corto plazo',
|
||||
'Costo marginal determina la curva de oferta del productor'
|
||||
],
|
||||
|
||||
formulaRecordatorio: {
|
||||
leyOferta: 'P ↑ → Qs ↑ (ceteris paribus)',
|
||||
ofertaMercado: 'Qs_mercado = Σ Qs_individuales',
|
||||
decisionProductor: 'Producir donde P ≥ CMg'
|
||||
},
|
||||
|
||||
mapaConceptual: {
|
||||
centro: 'Oferta',
|
||||
ramas: [
|
||||
{ nombre: 'Ley', elementos: ['Relación directa P-Q', 'Pendiente positiva'] },
|
||||
{ nombre: 'Determinantes', elementos: ['Tecnología', 'Costos', 'Vendedores', 'Expectativas'] },
|
||||
{ nombre: 'Tipos', elementos: ['Individual', 'Mercado', 'Corto plazo', 'Largo plazo'] },
|
||||
{ nombre: 'Costos', elementos: ['Fijos', 'Variables', 'Marginal', 'Total'] }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// Exportación por defecto
|
||||
export default {
|
||||
definicion: definicionOferta,
|
||||
ley: leyOferta,
|
||||
factores: factoresDesplazamientoOferta,
|
||||
costos: costosProduccion,
|
||||
curvas: {
|
||||
individual: curvaOfertaIndividual,
|
||||
mercado: curvaOfertaMercado,
|
||||
largoPlazo: curvaOfertaLargoPlazo
|
||||
},
|
||||
ejemplos: ejemplosOferta,
|
||||
temporal: ofertaTemporal,
|
||||
diferencia: diferenciaMovimientoDesplazamientoOferta,
|
||||
resumen: resumenOferta
|
||||
};
|
||||
450
frontend/src/content/modulo3/clasificacion.ts
Normal file
450
frontend/src/content/modulo3/clasificacion.ts
Normal file
@@ -0,0 +1,450 @@
|
||||
export const clasificacionBienes = {
|
||||
id: "clasificacion-bienes-elasticidad",
|
||||
titulo: "Clasificación de Bienes según Elasticidad",
|
||||
|
||||
introduccion: {
|
||||
descripcion: `La elasticidad nos permite clasificar los bienes en diferentes categorías según
|
||||
su comportamiento ante cambios en el ingreso (elasticidad ingreso) y ante cambios en el precio
|
||||
de otros bienes (elasticidad cruzada). Esta clasificación es fundamental para entender las
|
||||
relaciones de consumo y para la toma de decisiones empresariales y de política económica.`
|
||||
},
|
||||
|
||||
clasificacionPorIngreso: {
|
||||
titulo: "Clasificación según Elasticidad Ingreso (Ei)",
|
||||
descripcion: "Los bienes se clasifican según cómo responde su demanda ante cambios en el ingreso de los consumidores",
|
||||
formulaReferencia: "Ei = (% cambio en cantidad demandada) / (% cambio en ingreso)",
|
||||
|
||||
categorias: [
|
||||
{
|
||||
tipo: "Bienes Normales",
|
||||
condicion: "Ei > 0",
|
||||
descripcion: "La cantidad demandada aumenta cuando aumenta el ingreso. Son bienes que los consumidores desean más a medida que se vuelven más ricos.",
|
||||
signo: "Positivo",
|
||||
relacionIngreso: "Directa",
|
||||
grafica: "Curva con pendiente positiva en plano Ingreso-Cantidad",
|
||||
ejemplos: [
|
||||
"Ropa de calidad",
|
||||
"Electrodomésticos",
|
||||
"Entretenimiento",
|
||||
"Educación",
|
||||
"Viajes"
|
||||
],
|
||||
comportamientoCicloEconomico: "Demanda aumenta en expansiones económicas",
|
||||
|
||||
subclasificacion: [
|
||||
{
|
||||
subtipo: "Bienes Necesarios",
|
||||
condicion: "0 < Ei < 1",
|
||||
descripcion: "La demanda aumenta con el ingreso, pero en menor proporción. Son bienes esenciales que todos consumen, pero los ricos no consumen proporcionalmente más.",
|
||||
caracteristicas: [
|
||||
"Demanda crece menos que proporcionalmente al ingreso",
|
||||
"Son bienes básicos indispensables",
|
||||
"La proporción del ingreso gastada disminuye al subir ingresos"
|
||||
],
|
||||
ejemplos: [
|
||||
{ bien: "Alimentos básicos", eiAproximado: "0.2 - 0.5" },
|
||||
{ bien: "Servicios médicos básicos", eiAproximado: "0.3 - 0.6" },
|
||||
{ bien: "Vivienda básica", eiAproximado: "0.4 - 0.8" },
|
||||
{ bien: "Transporte público", eiAproximado: "0.1 - 0.4" }
|
||||
],
|
||||
curvaEngel: "Pendiente positiva pero convexa (aplana al subir ingreso)"
|
||||
},
|
||||
{
|
||||
subtipo: "Bienes de Lujo",
|
||||
condicion: "Ei > 1",
|
||||
descripcion: "La demanda aumenta más que proporcionalmente al ingreso. Cuando los ingresos crecen, el gasto en estos bienes crece más rápido.",
|
||||
caracteristicas: [
|
||||
"Demanda crece más que proporcionalmente al ingreso",
|
||||
"Son deseables pero no esenciales",
|
||||
"La proporción del ingreso gastada aumenta con el ingreso"
|
||||
],
|
||||
ejemplos: [
|
||||
{ bien: "Viajes internacionales", eiAproximado: "2.0 - 3.5" },
|
||||
{ bien: "Restaurantes de lujo", eiAproximado: "1.5 - 2.5" },
|
||||
{ bien: "Joyas finas", eiAproximado: "2.0 - 4.0" },
|
||||
{ bien: "Autos deportivos", eiAproximado: "2.5 - 3.5" },
|
||||
{ bien: "Arte y antigüedades", eiAproximado: "1.8 - 3.0" }
|
||||
],
|
||||
curvaEngel: "Pendiente positiva y cóncava (se empinada al subir ingreso)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
tipo: "Bienes Inferiores",
|
||||
condicion: "Ei < 0",
|
||||
descripcion: "La cantidad demandada disminuye cuando aumenta el ingreso. Los consumidores sustituyen estos bienes por alternativas de mayor calidad a medida que pueden pagar más.",
|
||||
signo: "Negativo",
|
||||
relacionIngreso: "Inversa",
|
||||
grafica: "Curva con pendiente negativa en plano Ingreso-Cantidad",
|
||||
|
||||
caracteristicas: [
|
||||
"Demanda decrece al aumentar el ingreso",
|
||||
"Sustituidos por bienes de mayor calidad",
|
||||
"Mayor consumo en grupos de bajos ingresos",
|
||||
"No son necesariamente de mala calidad, sino que hay mejores alternativas"
|
||||
],
|
||||
|
||||
ejemplos: [
|
||||
{
|
||||
bien: "Transporte público",
|
||||
explicacion: "Personas con más ingreso compran auto",
|
||||
eiAproximado: "-0.3 a -0.6"
|
||||
},
|
||||
{
|
||||
bien: "Fideos instantáneos",
|
||||
explicacion: "Sustituidos por comida fresca",
|
||||
eiAproximado: "-0.5 a -0.8"
|
||||
},
|
||||
{
|
||||
bien: "Marcas genéricas",
|
||||
explicacion: "Sustituidas por marcas reconocidas",
|
||||
eiAproximado: "-0.4 a -0.7"
|
||||
},
|
||||
{
|
||||
bien: "Carne de segunda",
|
||||
explicacion: "Sustituida por cortes de primera",
|
||||
eiAproximado: "-0.6 a -1.0"
|
||||
},
|
||||
{
|
||||
bien: "Ropa de segunda mano",
|
||||
explicacion: "Sustituida por ropa nueva",
|
||||
eiAproximado: "-0.8 a -1.5"
|
||||
},
|
||||
{
|
||||
bien: "Productos enlatados",
|
||||
explicacion: "Sustituidos por productos frescos",
|
||||
eiAproximado: "-0.3 a -0.5"
|
||||
}
|
||||
],
|
||||
|
||||
comportamientoCicloEconomico: "Demanda aumenta en recesiones",
|
||||
empresasEjemplo: ["Dollar stores", "Marcas blancas", "Comida rápida económica"],
|
||||
nota: "Un bien puede ser inferior para algunos grupos de ingreso y normal para otros"
|
||||
}
|
||||
],
|
||||
|
||||
ejemploNumerico: {
|
||||
titulo: "Ejemplo Completo de Clasificación",
|
||||
|
||||
escenario: "Un consumidor tiene los siguientes cambios en su consumo cuando su ingreso mensual sube de $3000 a $3600 (20% de aumento):",
|
||||
|
||||
casos: [
|
||||
{
|
||||
bien: "Pan",
|
||||
cantidadInicial: 20,
|
||||
cantidadFinal: 21,
|
||||
calculoEi: "%ΔQ = 5%, %ΔI = 20%, Ei = 5/20 = 0.25",
|
||||
clasificacion: "Bien NORMAL NECESARIO",
|
||||
justificacion: "0 < 0.25 < 1 → La demanda aumenta poco con el ingreso"
|
||||
},
|
||||
{
|
||||
bien: "Restaurantes de lujo",
|
||||
cantidadInicial: 2,
|
||||
cantidadFinal: 5,
|
||||
calculoEi: "%ΔQ = 150%, %ΔI = 20%, Ei = 150/20 = 7.5",
|
||||
clasificacion: "Bien de LUJO",
|
||||
justificacion: "Ei = 7.5 > 1 → La demanda crece mucho más que el ingreso"
|
||||
},
|
||||
{
|
||||
bien: "Fideos instantáneos",
|
||||
cantidadInicial: 15,
|
||||
cantidadFinal: 10,
|
||||
calculoEi: "%ΔQ = -33.3%, %ΔI = 20%, Ei = -33.3/20 = -1.67",
|
||||
clasificacion: "Bien INFERIOR",
|
||||
justificacion: "Ei = -1.67 < 0 → La demanda disminuye al subir el ingreso"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
clasificacionPorElasticidadCruzada: {
|
||||
titulo: "Clasificación según Elasticidad Cruzada (Exy)",
|
||||
descripcion: "Los bienes se clasifican según cómo afecta el precio de un bien Y a la demanda del bien X",
|
||||
formulaReferencia: "Exy = (% cambio en Qx) / (% cambio en Py)",
|
||||
|
||||
categorias: [
|
||||
{
|
||||
tipo: "Bienes Sustitutos",
|
||||
condicion: "Exy > 0",
|
||||
signo: "Positivo",
|
||||
descripcion: "Cuando sube el precio del bien Y, aumenta la demanda del bien X. Los bienes pueden usarse en lugar uno del otro para satisfacer la misma necesidad.",
|
||||
|
||||
caracteristicas: [
|
||||
"Satisfacen necesidades similares",
|
||||
"Los consumidores pueden intercambiarlos",
|
||||
"Compiten en el mismo mercado",
|
||||
"A mayor diferencia de precio, mayor sustitución"
|
||||
],
|
||||
|
||||
ejemplos: [
|
||||
{
|
||||
par: "Coca-Cola y Pepsi",
|
||||
exyAproximado: "+0.8",
|
||||
comentario: "Sustitutos cercanos"
|
||||
},
|
||||
{
|
||||
par: "Café y té",
|
||||
exyAproximado: "+0.5",
|
||||
comentario: "Sustitutos moderados"
|
||||
},
|
||||
{
|
||||
par: "Mantequilla y margarina",
|
||||
exyAproximado: "+1.2",
|
||||
comentario: "Muy buenos sustitutos"
|
||||
},
|
||||
{
|
||||
par: "Carne de res y pollo",
|
||||
exyAproximado: "+0.6",
|
||||
comentario: "Sustitutos proteicos"
|
||||
},
|
||||
{
|
||||
par: "Uber y taxi",
|
||||
exyAproximado: "+1.5",
|
||||
comentario: "Sustitutos cercanos en transporte"
|
||||
}
|
||||
],
|
||||
|
||||
relacionPrecioDemanda: "P↑ de Y → Q↑ de X",
|
||||
curvaDemanda: "Se desplaza a la derecha cuando sube Py",
|
||||
|
||||
ejemploNumerico: {
|
||||
titulo: "Ejemplo: Coca-Cola (X) y Pepsi (Y)",
|
||||
datos: {
|
||||
precioPepsiInicial: 3,
|
||||
precioPepsiFinal: 3.6,
|
||||
cantidadCocaInicial: 100,
|
||||
cantidadCocaFinal: 125
|
||||
},
|
||||
calculo: [
|
||||
"%ΔQx = (125-100)/100 × 100 = 25%",
|
||||
"%ΔPy = (3.6-3)/3 × 100 = 20%",
|
||||
"Exy = 25% / 20% = +1.25"
|
||||
],
|
||||
interpretacion: "Son sustitutos cercanos porque Exy > 0 y relativamente alto"
|
||||
}
|
||||
},
|
||||
{
|
||||
tipo: "Bienes Complementarios",
|
||||
condicion: "Exy < 0",
|
||||
signo: "Negativo",
|
||||
descripcion: "Cuando sube el precio del bien Y, disminuye la demanda del bien X. Los bienes se consumen juntos o uno es necesario para usar el otro.",
|
||||
|
||||
caracteristicas: [
|
||||
"Se consumen conjuntamente",
|
||||
"Uno complementa al otro",
|
||||
"El aumento de precio de uno reduce la demanda de ambos",
|
||||
"A veces forman un 'sistema' de consumo"
|
||||
],
|
||||
|
||||
tiposComplementariedad: [
|
||||
{
|
||||
tipo: "Complementos perfectos",
|
||||
descripcion: "Se consumen en proporciones fijas",
|
||||
ejemplos: ["Zapatos izquierdo y derecho", "Automóvil y gasolina (aprox)"]
|
||||
},
|
||||
{
|
||||
tipo: "Complementos imperfectos",
|
||||
descripcion: "Se consumen juntos pero no en proporción fija",
|
||||
ejemplos: ["Cerveza y hamburguesas", "Celular y aplicaciones"]
|
||||
}
|
||||
],
|
||||
|
||||
ejemplos: [
|
||||
{
|
||||
par: "Autos y gasolina",
|
||||
exyAproximado: "-0.4",
|
||||
comentario: "Complementos esenciales"
|
||||
},
|
||||
{
|
||||
par: "Computadores y software",
|
||||
exyAproximado: "-0.8",
|
||||
comentario: "Fuerte complementariedad"
|
||||
},
|
||||
{
|
||||
par: "Tortillas y frijoles",
|
||||
exyAproximado: "-0.3",
|
||||
comentario: "Complementos dietarios"
|
||||
},
|
||||
{
|
||||
par: "Impresoras y tinta",
|
||||
exyAproximado: "-1.2",
|
||||
comentario: "Complementos técnicos"
|
||||
},
|
||||
{
|
||||
par: "Cámaras y rollos/memorias",
|
||||
exyAproximado: "-0.9",
|
||||
comentario: "Complementos fotográficos"
|
||||
}
|
||||
],
|
||||
|
||||
relacionPrecioDemanda: "P↑ de Y → Q↓ de X",
|
||||
curvaDemanda: "Se desplaza a la izquierda cuando sube Py",
|
||||
|
||||
estrategiaEmpresas: "Las empresas a veces venden un bien barato (impresora) para ganar en el complemento (tinta)",
|
||||
|
||||
ejemploNumerico: {
|
||||
titulo: "Ejemplo: Autos (X) y Gasolina (Y)",
|
||||
datos: {
|
||||
precioGasolinaInicial: 4,
|
||||
precioGasolinaFinal: 5,
|
||||
cantidadAutosInicial: 1000,
|
||||
cantidadAutosFinal: 850
|
||||
},
|
||||
calculo: [
|
||||
"%ΔQx = (850-1000)/1000 × 100 = -15%",
|
||||
"%ΔPy = (5-4)/4 × 100 = 25%",
|
||||
"Exy = -15% / 25% = -0.6"
|
||||
],
|
||||
interpretacion: "Son complementarios porque Exy < 0"
|
||||
}
|
||||
},
|
||||
{
|
||||
tipo: "Bienes Independientes",
|
||||
condicion: "Exy = 0",
|
||||
signo: "Cero",
|
||||
descripcion: "El precio del bien Y no afecta la demanda del bien X. No existe relación de consumo entre ellos.",
|
||||
|
||||
caracteristicas: [
|
||||
"No se relacionan en el consumo",
|
||||
"Pertenecen a categorías completamente diferentes",
|
||||
"El cambio de precio de uno no afecta al otro"
|
||||
],
|
||||
|
||||
ejemplos: [
|
||||
{ par: "Libros y tomates", explicacion: "Sin relación de consumo" },
|
||||
{ par: "Zapatos y sillas", explicacion: "Bienes de categorías distintas" },
|
||||
{ par: "Computadores y sal", explicacion: "Sin relación de consumo" },
|
||||
{ par: "Viajes y papel higiénico", explicacion: "Necesidades independientes" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
matrizClasificacionCompleta: {
|
||||
titulo: "Matriz de Clasificación Completa",
|
||||
descripcion: "Un bien puede clasificarse usando ambos criterios simultáneamente",
|
||||
|
||||
matriz: [
|
||||
{
|
||||
combinacion: "Bien Normal + Sustituto",
|
||||
ejemplo: "Restaurantes de lujo vs. restaurantes medianos",
|
||||
caracteristicas: "Demanda crece con ingreso, compite con similares"
|
||||
},
|
||||
{
|
||||
combinacion: "Bien Normal + Complemento",
|
||||
ejemplo: "Autos eléctricos (complemento: estaciones de carga)",
|
||||
caracteristicas: "Demanda crece con ingreso, depende de bien relacionado"
|
||||
},
|
||||
{
|
||||
combinacion: "Bien Inferior + Sustituto",
|
||||
ejemplo: "Transporte público vs. taxis",
|
||||
caracteristicas: "Demanda cae con ingreso, compite con alternativas"
|
||||
},
|
||||
{
|
||||
combinacion: "Bien Inferior + Complemento",
|
||||
ejemplo: "Fideos instantáneos + salsa instantánea",
|
||||
caracteristicas: "Ambos tienen demanda decreciente con ingreso"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
aplicacionesPracticas: {
|
||||
titulo: "Aplicaciones Prácticas de la Clasificación",
|
||||
|
||||
aplicaciones: [
|
||||
{
|
||||
area: "Marketing y Estrategia Empresarial",
|
||||
usos: [
|
||||
"Identificar mercados objetivo según nivel de ingreso",
|
||||
"Desarrollar estrategias de precios basadas en elasticidad",
|
||||
"Diseñar campañas para bienes de lujo vs. necesarios"
|
||||
]
|
||||
},
|
||||
{
|
||||
area: "Política Económica",
|
||||
usos: [
|
||||
"Diseñar impuestos sobre bienes inelásticos (generan más recaudación)",
|
||||
"Subvencionar bienes necesarios para grupos de bajos ingresos",
|
||||
"Predecir efectos de políticas redistributivas"
|
||||
]
|
||||
},
|
||||
{
|
||||
area: "Análisis de Mercado",
|
||||
usos: [
|
||||
"Identificar oportunidades de negocio en diferentes segmentos",
|
||||
"Predecir demanda en ciclos económicos",
|
||||
"Analizar competencia entre productos sustitutos"
|
||||
]
|
||||
},
|
||||
{
|
||||
area: "Planificación Financiera",
|
||||
usos: [
|
||||
"Sectores defensivos (bienes necesarios) vs. cíclicos (lujos)",
|
||||
"Diversificación de inversiones",
|
||||
"Evaluación de riesgos en recesiones"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
ejerciciosResueltos: [
|
||||
{
|
||||
id: 1,
|
||||
enunciado: "Clasifica los siguientes bienes según su elasticidad ingreso esperada: a) Arroz, b) Yates, c) Autobuses, d) Medicinas",
|
||||
|
||||
respuestas: [
|
||||
{
|
||||
bien: "Arroz",
|
||||
eiEstimado: "0.2 - 0.4",
|
||||
clasificacion: "Bien NORMAL NECESARIO",
|
||||
justificacion: "Es un alimento básico. La demanda aumenta con el ingreso pero poco."
|
||||
},
|
||||
{
|
||||
bien: "Yates",
|
||||
eiEstimado: "3.0 - 5.0",
|
||||
clasificacion: "Bien de LUJO",
|
||||
justificacion: "Solo los muy ricos los compran. Demanda muy sensible al ingreso."
|
||||
},
|
||||
{
|
||||
bien: "Autobuses",
|
||||
eiEstimado: "-0.5 - -0.3",
|
||||
clasificacion: "Bien INFERIOR",
|
||||
justificacion: "Con más ingreso la gente prefiere auto o taxi."
|
||||
},
|
||||
{
|
||||
bien: "Medicinas esenciales",
|
||||
eiEstimado: "0.0 - 0.1",
|
||||
clasificacion: "Bien NORMAL NECESARIO (casi inelástico)",
|
||||
justificacion: "Todos las necesitan sin importar el ingreso."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
enunciado: "¿Son sustitutos o complementos los siguientes pares? a) Netflix y cines, b) Lápices y papel, c) iPhone y Samsung",
|
||||
|
||||
respuestas: [
|
||||
{
|
||||
par: "Netflix y cines",
|
||||
exyEsperado: "+0.6",
|
||||
clasificacion: "SUSTITUTOS",
|
||||
explicacion: "Compiten por el tiempo de entretenimiento del consumidor. Si suben las entradas de cine, más gente se queda en casa con Netflix."
|
||||
},
|
||||
{
|
||||
par: "Lápices y papel",
|
||||
exyEsperado: "-0.4",
|
||||
clasificacion: "COMPLEMENTOS",
|
||||
explicacion: "Se usan juntos. Si sube el precio del papel, se demandan menos lápices."
|
||||
},
|
||||
{
|
||||
par: "iPhone y Samsung",
|
||||
exyEsperado: "+1.2",
|
||||
clasificacion: "SUSTITUTOS CERCANOS",
|
||||
explicacion: "Son competidores directos en smartphones. Alta sustituibilidad."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export default clasificacionBienes;
|
||||
242
frontend/src/content/modulo3/conceptos.ts
Normal file
242
frontend/src/content/modulo3/conceptos.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
export const conceptosElasticidad = {
|
||||
id: "conceptos-elasticidad",
|
||||
titulo: "Conceptos Fundamentales de Elasticidad",
|
||||
|
||||
introduccion: {
|
||||
descripcion: `La elasticidad mide la sensibilidad o respuesta de la cantidad demandada u ofrecida
|
||||
de un bien ante cambios en variables económicas como el precio, el ingreso o el precio de otros bienes.
|
||||
Es una herramienta fundamental para analizar cómo reaccionan los consumidores y productores ante
|
||||
cambios en el mercado.`,
|
||||
importancia: [
|
||||
"Permite predecir cambios en la cantidad demandada ante variaciones de precio",
|
||||
"Ayuda a las empresas a fijar estrategias de precios óptimas",
|
||||
"Permite clasificar bienes según su comportamiento ante cambios económicos",
|
||||
"Es esencial para la formulación de políticas fiscales y de ingresos"
|
||||
]
|
||||
},
|
||||
|
||||
definicionElasticidad: {
|
||||
titulo: "¿Qué es la Elasticidad?",
|
||||
definicion: "La elasticidad mide el grado de respuesta de la cantidad demandada (u ofrecida) ante cambios porcentuales en variables económicas relevantes.",
|
||||
interpretacionIntuitiva: "Una elasticidad alta significa que la cantidad es muy sensible a cambios en la variable. Una elasticidad baja indica poca sensibilidad.",
|
||||
|
||||
formulaGeneral: {
|
||||
simbolo: "E",
|
||||
ecuacion: "E = (% cambio en la cantidad) / (% cambio en la variable)",
|
||||
latex: "E = \\frac{\\% \\Delta Q}{\\% \\Delta X}",
|
||||
donde: [
|
||||
{ variable: "E", significado: "Coeficiente de elasticidad (número puro)" },
|
||||
{ variable: "% ΔQ", significado: "Porcentaje de cambio en la cantidad" },
|
||||
{ variable: "% ΔX", significado: "Porcentaje de cambio en la variable (precio, ingreso, etc.)" }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
elasticidadPrecioDemanda: {
|
||||
titulo: "Elasticidad Precio de la Demanda (Ed)",
|
||||
definicion: "Mide el porcentaje de cambio en la cantidad demandada como respuesta a un cambio porcentual en el precio del bien.",
|
||||
|
||||
formula: {
|
||||
ecuacion: "Ed = (% cambio en cantidad demandada) / (% cambio en precio)",
|
||||
latex: "E_d = \\frac{\\% \\Delta Q_d}{\\% \\Delta P} = \\frac{\\frac{Q_2 - Q_1}{Q_1} \\times 100}{\\frac{P_2 - P_1}{P_1} \\times 100}",
|
||||
simplificada: "Ed = (ΔQ/Q) / (ΔP/P) = (ΔQ/ΔP) × (P/Q)"
|
||||
},
|
||||
|
||||
metodoPuntoMedio: {
|
||||
nombre: "Método del Punto Medio (Arc Elasticity)",
|
||||
descripcion: "Método más preciso que usa el promedio de los valores inicial y final",
|
||||
formula: {
|
||||
latex: "E_d = \\frac{\\frac{Q_2 - Q_1}{(Q_1 + Q_2)/2}}{\\frac{P_2 - P_1}{(P_1 + P_2)/2}}",
|
||||
descripcion: "Usa los valores promedio como base para calcular los porcentajes"
|
||||
}
|
||||
},
|
||||
|
||||
interpretacion: [
|
||||
{
|
||||
rango: "|Ed| > 1",
|
||||
clasificacion: "Demanda ELÁSTICA",
|
||||
significado: "La cantidad cambia en mayor proporción que el precio",
|
||||
ejemplo: "Si el precio sube 10%, la cantidad demandada baja más de 10%",
|
||||
bienesTipicos: ["Bienes de lujo", "Productos con muchos sustitutos", "Bienes no esenciales"]
|
||||
},
|
||||
{
|
||||
rango: "|Ed| < 1",
|
||||
clasificacion: "Demanda INELÁSTICA",
|
||||
significado: "La cantidad cambia en menor proporción que el precio",
|
||||
ejemplo: "Si el precio sube 10%, la cantidad demandada baja menos de 10%",
|
||||
bienesTipicos: ["Bienes necesarios", "Medicinas", "Combustible", "Sal"]
|
||||
},
|
||||
{
|
||||
rango: "|Ed| = 1",
|
||||
clasificacion: "Demanda UNITARIA",
|
||||
significado: "La cantidad cambia en la misma proporción que el precio",
|
||||
ejemplo: "Si el precio sube 10%, la cantidad demandada baja exactamente 10%",
|
||||
bienesTipicos: ["Raramente ocurre en la realidad", "Curva teórica de demanda rectangular de hiperbola"]
|
||||
},
|
||||
{
|
||||
rango: "|Ed| = 0",
|
||||
clasificacion: "Demanda PERFECTAMENTE INELÁSTICA",
|
||||
significado: "La cantidad no cambia ante cualquier cambio de precio",
|
||||
ejemplo: "Medicinas indispensables para la vida",
|
||||
representacionGrafica: "Línea vertical"
|
||||
},
|
||||
{
|
||||
rango: "|Ed| = ∞",
|
||||
clasificacion: "Demanda PERFECTAMENTE ELÁSTICA",
|
||||
significado: "Cualquier cambio de precio elimina toda la demanda",
|
||||
ejemplo: "Bienes con sustitutos perfectos en mercados competitivos",
|
||||
representacionGrafica: "Línea horizontal"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
determinantesElasticidad: {
|
||||
titulo: "Factores que Determinan la Elasticidad",
|
||||
factores: [
|
||||
{
|
||||
factor: "Disponibilidad de sustitutos",
|
||||
efecto: "Más sustitutos → Mayor elasticidad",
|
||||
explicacion: "Si existen muchos bienes similares, los consumidores pueden cambiar fácilmente cuando sube el precio"
|
||||
},
|
||||
{
|
||||
factor: "Necesidad vs. Lujo",
|
||||
efecto: "Necesidades → Menor elasticidad | Lujos → Mayor elasticidad",
|
||||
explicacion: "Los bienes necesarios se siguen consumiendo aunque suban de precio"
|
||||
},
|
||||
{
|
||||
factor: "Proporción del ingreso",
|
||||
efecto: "Mayor proporción → Mayor elasticidad",
|
||||
explicacion: "Bienes caros (autos, casas) tienen elasticidad mayor que bienes baratos (fósforos)"
|
||||
},
|
||||
{
|
||||
factor: "Horizonte temporal",
|
||||
efecto: "Largo plazo → Mayor elasticidad",
|
||||
explicacion: "A largo plazo los consumidores pueden ajustar hábitos y encontrar alternativas"
|
||||
},
|
||||
{
|
||||
factor: "Definición del mercado",
|
||||
efecto: "Mercado amplio → Menor elasticidad | Mercado específico → Mayor elasticidad",
|
||||
explicacion: "La demanda de 'alimentos' es inelástica, pero la de 'manzanas' es más elástica"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
ejemplosNumericos: [
|
||||
{
|
||||
titulo: "Ejemplo 1: Cálculo básico de elasticidad",
|
||||
datos: {
|
||||
precioInicial: 10,
|
||||
precioFinal: 12,
|
||||
cantidadInicial: 100,
|
||||
cantidadFinal: 80
|
||||
},
|
||||
pasos: [
|
||||
{
|
||||
paso: 1,
|
||||
descripcion: "Calcular % cambio en cantidad",
|
||||
calculo: "(80 - 100) / 100 × 100 = -20%"
|
||||
},
|
||||
{
|
||||
paso: 2,
|
||||
descripcion: "Calcular % cambio en precio",
|
||||
calculo: "(12 - 10) / 10 × 100 = 20%"
|
||||
},
|
||||
{
|
||||
paso: 3,
|
||||
descripcion: "Calcular elasticidad",
|
||||
calculo: "Ed = -20% / 20% = -1.0",
|
||||
nota: "En valor absoluto: |Ed| = 1.0"
|
||||
},
|
||||
{
|
||||
paso: 4,
|
||||
descripcion: "Interpretación",
|
||||
resultado: "Demanda UNITARIA - la cantidad disminuye en la misma proporción que aumenta el precio"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
titulo: "Ejemplo 2: Método del punto medio",
|
||||
datos: {
|
||||
precioInicial: 8,
|
||||
precioFinal: 10,
|
||||
cantidadInicial: 120,
|
||||
cantidadFinal: 90
|
||||
},
|
||||
pasos: [
|
||||
{
|
||||
paso: 1,
|
||||
descripcion: "Calcular cambio en cantidad usando promedio",
|
||||
calculo: "(90 - 120) / ((120 + 90)/2) = -30 / 105 = -0.2857 = -28.57%"
|
||||
},
|
||||
{
|
||||
paso: 2,
|
||||
descripcion: "Calcular cambio en precio usando promedio",
|
||||
calculo: "(10 - 8) / ((8 + 10)/2) = 2 / 9 = 0.2222 = 22.22%"
|
||||
},
|
||||
{
|
||||
paso: 3,
|
||||
descripcion: "Calcular elasticidad",
|
||||
calculo: "Ed = -28.57% / 22.22% = -1.29",
|
||||
nota: "En valor absoluto: |Ed| = 1.29"
|
||||
},
|
||||
{
|
||||
paso: 4,
|
||||
descripcion: "Interpretación",
|
||||
resultado: "Demanda ELÁSTICA - la cantidad es muy sensible al precio"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
relacionIngresoTotal: {
|
||||
titulo: "Relación entre Elasticidad e Ingreso Total",
|
||||
definicion: "El ingreso total (IT) es el precio multiplicado por la cantidad vendida: IT = P × Q",
|
||||
|
||||
reglas: [
|
||||
{
|
||||
elasticidad: "Elástica (|Ed| > 1)",
|
||||
efectoPrecioArriba: "El ingreso total DISMINUYE",
|
||||
efectoPrecioAbajo: "El ingreso total AUMENTA",
|
||||
explicacion: "La cantidad cambia más que proporcionalmente al precio"
|
||||
},
|
||||
{
|
||||
elasticidad: "Inelástica (|Ed| < 1)",
|
||||
efectoPrecioArriba: "El ingreso total AUMENTA",
|
||||
efectoPrecioAbajo: "El ingreso total DISMINUYE",
|
||||
explicacion: "La cantidad cambia menos que proporcionalmente al precio"
|
||||
},
|
||||
{
|
||||
elasticidad: "Unitaria (|Ed| = 1)",
|
||||
efectoPrecioArriba: "El ingreso total se MANTIENE CONSTANTE",
|
||||
efectoPrecioAbajo: "El ingreso total se MANTIENE CONSTANTE",
|
||||
explicacion: "Los cambios en precio y cantidad se compensan exactamente"
|
||||
}
|
||||
],
|
||||
|
||||
ejemploNumerico: {
|
||||
descripcion: "Ejemplo: Producto con demanda elástica (|Ed| = 2)",
|
||||
escenarioBase: { precio: 100, cantidad: 1000, ingresoTotal: 100000 },
|
||||
escenarioPrecioSube: { precio: 110, cantidad: 800, ingresoTotal: 88000, cambio: "-12%" },
|
||||
escenarioPrecioBaja: { precio: 90, cantidad: 1200, ingresoTotal: 108000, cambio: "+8%" },
|
||||
conclusion: "Al ser elástica, subir el precio reduce los ingresos, y bajar el precio aumenta los ingresos"
|
||||
}
|
||||
},
|
||||
|
||||
resumenVisual: {
|
||||
titulo: "Resumen Visual de Elasticidad",
|
||||
tablaInterpretacion: {
|
||||
columnas: ["|Ed|", "Clasificación", "Respuesta de Q", "Ejemplo"],
|
||||
filas: [
|
||||
["0", "Perfectamente inelástica", "Sin cambio", "Insulina"],
|
||||
["0 - 0.5", "Muy inelástica", "Cambia poco", "Sal"],
|
||||
["0.5 - 1", "Inelástica", "Cambia menos proporcional", "Gasolina"],
|
||||
["1", "Unitaria", "Cambia igual proporción", "Teórico"],
|
||||
["1 - 2", "Elástica", "Cambia más proporcional", "Restaurantes"],
|
||||
["2 - 5", "Muy elástica", "Cambia mucho", "Cine"],
|
||||
["∞", "Perfectamente elástica", "Q → 0 con cualquier ΔP", "Trigo en mercado mundial"]
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default conceptosElasticidad;
|
||||
677
frontend/src/content/modulo3/ejercicios.ts
Normal file
677
frontend/src/content/modulo3/ejercicios.ts
Normal file
@@ -0,0 +1,677 @@
|
||||
export interface PasoEjercicio {
|
||||
paso: number;
|
||||
descripcion: string;
|
||||
formula?: string;
|
||||
latex?: string;
|
||||
calculo: string;
|
||||
resultado?: string;
|
||||
explicacion?: string;
|
||||
}
|
||||
|
||||
export interface Ejercicio {
|
||||
id: string;
|
||||
tipo: "calculadora" | "clasificacion" | "examen";
|
||||
titulo: string;
|
||||
dificultad: "basico" | "intermedio" | "avanzado";
|
||||
tiempoEstimado: number;
|
||||
enunciado: string;
|
||||
datos?: Record<string, number>;
|
||||
pasos: PasoEjercicio[];
|
||||
respuestaCorrecta: string | number;
|
||||
interpretacion: string;
|
||||
pistas?: string[];
|
||||
}
|
||||
|
||||
export const ejerciciosElasticidad: Ejercicio[] = [
|
||||
{
|
||||
id: "ejercicio-1-calculadora",
|
||||
tipo: "calculadora",
|
||||
titulo: "Calculadora de Elasticidad Precio - Paso a Paso",
|
||||
dificultad: "basico",
|
||||
tiempoEstimado: 10,
|
||||
enunciado: `Una tienda vende café gourmet. Cuando el precio es de $10 por libra,
|
||||
venden 200 libras al mes. Cuando suben el precio a $12, las ventas bajan a 150 libras.
|
||||
Calcula la elasticidad precio de la demanda usando el método del punto medio y clasifica el resultado.`,
|
||||
|
||||
datos: {
|
||||
precioInicial: 10,
|
||||
precioFinal: 12,
|
||||
cantidadInicial: 200,
|
||||
cantidadFinal: 150
|
||||
},
|
||||
|
||||
pasos: [
|
||||
{
|
||||
paso: 1,
|
||||
descripcion: "Identificar los datos del problema",
|
||||
calculo: "P₁ = $10, P₂ = $12, Q₁ = 200, Q₂ = 150"
|
||||
},
|
||||
{
|
||||
paso: 2,
|
||||
descripcion: "Calcular el cambio en cantidad (ΔQ)",
|
||||
formula: "ΔQ = Q₂ - Q₁",
|
||||
calculo: "ΔQ = 150 - 200 = -50 libras"
|
||||
},
|
||||
{
|
||||
paso: 3,
|
||||
descripcion: "Calcular el cambio en precio (ΔP)",
|
||||
formula: "ΔP = P₂ - P₁",
|
||||
calculo: "ΔP = $12 - $10 = $2"
|
||||
},
|
||||
{
|
||||
paso: 4,
|
||||
descripcion: "Calcular el promedio de cantidades",
|
||||
formula: "Q̄ = (Q₁ + Q₂) / 2",
|
||||
latex: "\\bar{Q} = \\frac{Q_1 + Q_2}{2}",
|
||||
calculo: "Q̄ = (200 + 150) / 2 = 175 libras"
|
||||
},
|
||||
{
|
||||
paso: 5,
|
||||
descripcion: "Calcular el promedio de precios",
|
||||
formula: "P̄ = (P₁ + P₂) / 2",
|
||||
latex: "\\bar{P} = \\frac{P_1 + P_2}{2}",
|
||||
calculo: "P̄ = ($10 + $12) / 2 = $11"
|
||||
},
|
||||
{
|
||||
paso: 6,
|
||||
descripcion: "Calcular el % cambio en cantidad",
|
||||
formula: "%ΔQ = (ΔQ / Q̄) × 100",
|
||||
latex: "\\% \\Delta Q = \\frac{\\Delta Q}{\\bar{Q}} \\times 100",
|
||||
calculo: "%ΔQ = (-50 / 175) × 100 = -28.57%"
|
||||
},
|
||||
{
|
||||
paso: 7,
|
||||
descripcion: "Calcular el % cambio en precio",
|
||||
formula: "%ΔP = (ΔP / P̄) × 100",
|
||||
latex: "\\% \\Delta P = \\frac{\\Delta P}{\\bar{P}} \\times 100",
|
||||
calculo: "%ΔP = (2 / 11) × 100 = 18.18%"
|
||||
},
|
||||
{
|
||||
paso: 8,
|
||||
descripcion: "Calcular la elasticidad precio de la demanda",
|
||||
formula: "Ed = %ΔQ / %ΔP",
|
||||
latex: "E_d = \\frac{\\% \\Delta Q}{\\% \\Delta P}",
|
||||
calculo: "Ed = -28.57% / 18.18% = -1.57",
|
||||
resultado: "Ed = -1.57",
|
||||
explicacion: "El signo negativo indica la relación inversa entre precio y cantidad (Ley de la Demanda)"
|
||||
},
|
||||
{
|
||||
paso: 9,
|
||||
descripcion: "Tomar valor absoluto para clasificar",
|
||||
calculo: "|Ed| = |-1.57| = 1.57"
|
||||
},
|
||||
{
|
||||
paso: 10,
|
||||
descripcion: "Clasificar según el valor de elasticidad",
|
||||
calculo: "|Ed| = 1.57 > 1",
|
||||
resultado: "DEMANDA ELÁSTICA",
|
||||
explicacion: "La cantidad demandada cambia en mayor proporción que el precio"
|
||||
}
|
||||
],
|
||||
|
||||
respuestaCorrecta: -1.57,
|
||||
interpretacion: `La elasticidad es -1.57 (elástica). Esto significa que por cada 1% que aumenta
|
||||
el precio del café, la cantidad demandada disminuye 1.57%. Como |Ed| > 1, la demanda es elástica:
|
||||
los consumidores son sensibles al precio. Esto tiene sentido porque el café gourmet tiene
|
||||
muchos sustitutos (café regular, té, otras marcas). Si la tienda sube precios, perderá
|
||||
muchas ventas. Para maximizar ingresos, debería considerar BAJAR el precio.`,
|
||||
|
||||
pistas: [
|
||||
"Recuerda usar el método del punto medio: divide por el promedio de valores inicial y final",
|
||||
"La elasticidad es (% cambio Q) / (% cambio P)",
|
||||
"Si |Ed| > 1, la demanda es elástica"
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
id: "ejercicio-2-calculadora-ingreso",
|
||||
tipo: "calculadora",
|
||||
titulo: "Calculadora de Elasticidad Ingreso",
|
||||
dificultad: "intermedio",
|
||||
tiempoEstimado: 12,
|
||||
enunciado: `En un país, cuando el ingreso promedio mensual es de $2000, los hogares consumen
|
||||
4 kg de carne de res al mes. Cuando el ingreso sube a $2500, el consumo aumenta a 6 kg.
|
||||
Calcula la elasticidad ingreso de la demanda y clasifica la carne de res.`,
|
||||
|
||||
datos: {
|
||||
ingresoInicial: 2000,
|
||||
ingresoFinal: 2500,
|
||||
cantidadInicial: 4,
|
||||
cantidadFinal: 6
|
||||
},
|
||||
|
||||
pasos: [
|
||||
{
|
||||
paso: 1,
|
||||
descripcion: "Identificar los datos",
|
||||
calculo: "I₁ = $2000, I₂ = $2500, Q₁ = 4 kg, Q₂ = 6 kg"
|
||||
},
|
||||
{
|
||||
paso: 2,
|
||||
descripcion: "Fórmula de elasticidad ingreso",
|
||||
formula: "Ei = (% cambio Q) / (% cambio I)",
|
||||
latex: "E_i = \\frac{\\% \\Delta Q}{\\% \\Delta I} = \\frac{\\frac{Q_2 - Q_1}{(Q_1 + Q_2)/2}}{\\frac{I_2 - I_1}{(I_1 + I_2)/2}}",
|
||||
calculo: "Método del punto medio"
|
||||
},
|
||||
{
|
||||
paso: 3,
|
||||
descripcion: "Calcular % cambio en cantidad",
|
||||
formula: "%ΔQ = (Q₂ - Q₁) / ((Q₁ + Q₂)/2)",
|
||||
calculo: "%ΔQ = (6 - 4) / ((4 + 6)/2) = 2 / 5 = 0.40 = 40%"
|
||||
},
|
||||
{
|
||||
paso: 4,
|
||||
descripcion: "Calcular % cambio en ingreso",
|
||||
formula: "%ΔI = (I₂ - I₁) / ((I₁ + I₂)/2)",
|
||||
calculo: "%ΔI = (2500 - 2000) / ((2000 + 2500)/2) = 500 / 2250 = 0.222 = 22.22%"
|
||||
},
|
||||
{
|
||||
paso: 5,
|
||||
descripcion: "Calcular elasticidad ingreso",
|
||||
formula: "Ei = %ΔQ / %ΔI",
|
||||
calculo: "Ei = 40% / 22.22% = 1.80"
|
||||
},
|
||||
{
|
||||
paso: 6,
|
||||
descripcion: "Clasificar el bien",
|
||||
calculo: "Ei = 1.80 > 1",
|
||||
resultado: "BIEN DE LUJO",
|
||||
explicacion: "El consumo de carne crece más que proporcionalmente al ingreso"
|
||||
}
|
||||
],
|
||||
|
||||
respuestaCorrecta: 1.80,
|
||||
interpretacion: `La elasticidad ingreso es 1.80, indicando que la carne de res es un BIEN DE LUJO
|
||||
en este contexto. Esto significa que cuando el ingreso aumenta 1%, el consumo de carne aumenta 1.8%.
|
||||
Esto es típico en economías donde la carne es un símbolo de estatus o donde existe una dieta
|
||||
base de alimentos más baratos (granos, vegetales) que los consumidores mejoran al subir de ingreso.`
|
||||
},
|
||||
|
||||
{
|
||||
id: "ejercicio-3-calculadora-cruzada",
|
||||
tipo: "calculadora",
|
||||
titulo: "Calculadora de Elasticidad Cruzada",
|
||||
dificultad: "intermedio",
|
||||
tiempoEstimado: 12,
|
||||
enunciado: `El precio del té (bien Y) sube de $3 a $4 por caja. Como resultado, la cantidad
|
||||
demandada de café (bien X) aumenta de 100 a 130 libras al mes. Calcula la elasticidad cruzada
|
||||
y determina si son sustitutos o complementos.`,
|
||||
|
||||
datos: {
|
||||
precioYInicial: 3,
|
||||
precioYFinal: 4,
|
||||
cantidadXInicial: 100,
|
||||
cantidadXFinal: 130
|
||||
},
|
||||
|
||||
pasos: [
|
||||
{
|
||||
paso: 1,
|
||||
descripcion: "Identificar los datos",
|
||||
calculo: "Py₁ = $3, Py₂ = $4, Qx₁ = 100, Qx₂ = 130"
|
||||
},
|
||||
{
|
||||
paso: 2,
|
||||
descripcion: "Fórmula de elasticidad cruzada",
|
||||
formula: "Exy = (% cambio Qx) / (% cambio Py)",
|
||||
latex: "E_{xy} = \\frac{\\% \\Delta Q_x}{\\% \\Delta P_y}",
|
||||
calculo: "Usar método del punto medio"
|
||||
},
|
||||
{
|
||||
paso: 3,
|
||||
descripcion: "Calcular % cambio en Qx",
|
||||
formula: "%ΔQx = (Qx₂ - Qx₁) / ((Qx₁ + Qx₂)/2)",
|
||||
calculo: "%ΔQx = (130 - 100) / ((100 + 130)/2) = 30 / 115 = 26.09%"
|
||||
},
|
||||
{
|
||||
paso: 4,
|
||||
descripcion: "Calcular % cambio en Py",
|
||||
formula: "%ΔPy = (Py₂ - Py₁) / ((Py₁ + Py₂)/2)",
|
||||
calculo: "%ΔPy = (4 - 3) / ((3 + 4)/2) = 1 / 3.5 = 28.57%"
|
||||
},
|
||||
{
|
||||
paso: 5,
|
||||
descripcion: "Calcular elasticidad cruzada",
|
||||
formula: "Exy = %ΔQx / %ΔPy",
|
||||
calculo: "Exy = 26.09% / 28.57% = 0.91"
|
||||
},
|
||||
{
|
||||
paso: 6,
|
||||
descripcion: "Determinar relación entre bienes",
|
||||
calculo: "Exy = 0.91 > 0",
|
||||
resultado: "BIENES SUSTITUTOS",
|
||||
explicacion: "Signo positivo indica que al subir precio de Y aumenta demanda de X"
|
||||
}
|
||||
],
|
||||
|
||||
respuestaCorrecta: 0.91,
|
||||
interpretacion: `La elasticidad cruzada es 0.91 (positiva), confirmando que café y té son SUSTITUTOS.
|
||||
Cuando el té se encarece, los consumidores sustituyen parcialmente su consumo por café.
|
||||
El valor menor a 1 indica que son sustitutos moderados, no perfectos. Los consumidores
|
||||
tienen cierta preferencia por uno u otro, pero sí responden a diferencias de precio.`
|
||||
}
|
||||
];
|
||||
|
||||
export const ejerciciosClasificacion = [
|
||||
{
|
||||
id: "clasificacion-1",
|
||||
tipo: "clasificacion",
|
||||
titulo: "Clasificar Bienes según Elasticidad Ingreso",
|
||||
dificultad: "intermedio",
|
||||
tiempoEstimado: 15,
|
||||
enunciado: `Analiza los siguientes casos y clasifica cada bien como: Normal Necesario,
|
||||
de Lujo, o Inferior. Justifica tu respuesta con el valor calculado de Ei.`,
|
||||
|
||||
casos: [
|
||||
{
|
||||
id: "caso-a",
|
||||
bien: "Arroz",
|
||||
datos: {
|
||||
ingresoInicial: 1000,
|
||||
ingresoFinal: 1500,
|
||||
cantidadInicial: 20,
|
||||
cantidadFinal: 22
|
||||
},
|
||||
pasos: [
|
||||
"%ΔQ = (22-20)/((20+22)/2) = 2/21 = 9.52%",
|
||||
"%ΔI = (1500-1000)/((1000+1500)/2) = 500/1250 = 40%",
|
||||
"Ei = 9.52% / 40% = 0.24"
|
||||
],
|
||||
respuesta: "NORMAL NECESARIO",
|
||||
justificacion: "0 < 0.24 < 1: El consumo aumenta poco respecto al ingreso"
|
||||
},
|
||||
{
|
||||
id: "caso-b",
|
||||
bien: "Viajes internacionales",
|
||||
datos: {
|
||||
ingresoInicial: 3000,
|
||||
ingresoFinal: 4500,
|
||||
cantidadInicial: 1,
|
||||
cantidadFinal: 4
|
||||
},
|
||||
pasos: [
|
||||
"%ΔQ = (4-1)/((1+4)/2) = 3/2.5 = 120%",
|
||||
"%ΔI = (4500-3000)/((3000+4500)/2) = 1500/3750 = 40%",
|
||||
"Ei = 120% / 40% = 3.0"
|
||||
],
|
||||
respuesta: "BIEN DE LUJO",
|
||||
justificacion: "Ei = 3.0 > 1: El consumo crece más que proporcionalmente al ingreso"
|
||||
},
|
||||
{
|
||||
id: "caso-c",
|
||||
bien: "Autobuses urbanos",
|
||||
datos: {
|
||||
ingresoInicial: 2000,
|
||||
ingresoFinal: 3500,
|
||||
cantidadInicial: 40,
|
||||
cantidadFinal: 20
|
||||
},
|
||||
pasos: [
|
||||
"%ΔQ = (20-40)/((40+20)/2) = -20/30 = -66.67%",
|
||||
"%ΔI = (3500-2000)/((2000+3500)/2) = 1500/2750 = 54.55%",
|
||||
"Ei = -66.67% / 54.55% = -1.22"
|
||||
],
|
||||
respuesta: "BIEN INFERIOR",
|
||||
justificacion: "Ei = -1.22 < 0: El consumo disminuye al aumentar el ingreso (la gente compra auto o usa taxi)"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
id: "clasificacion-2",
|
||||
tipo: "clasificacion",
|
||||
titulo: "Identificar Sustitutos y Complementos",
|
||||
dificultad: "intermedio",
|
||||
tiempoEstimado: 15,
|
||||
enunciado: `Para cada par de bienes, calcula la elasticidad cruzada y determina si son
|
||||
sustitutos, complementos o independientes.`,
|
||||
|
||||
casos: [
|
||||
{
|
||||
id: "caso-a",
|
||||
bienX: "Coca-Cola",
|
||||
bienY: "Pepsi",
|
||||
datos: {
|
||||
precioYInicial: 2,
|
||||
precioYFinal: 2.5,
|
||||
cantidadXInicial: 100,
|
||||
cantidadXFinal: 130
|
||||
},
|
||||
pasos: [
|
||||
"%ΔQx = (130-100)/((100+130)/2) = 30/115 = 26.09%",
|
||||
"%ΔPy = (2.5-2)/((2+2.5)/2) = 0.5/2.25 = 22.22%",
|
||||
"Exy = 26.09% / 22.22% = 1.17"
|
||||
],
|
||||
respuesta: "SUSTITUTOS",
|
||||
justificacion: "Exy = 1.17 > 0: Al subir Pepsi, aumenta demanda de Coca-Cola"
|
||||
},
|
||||
{
|
||||
id: "caso-b",
|
||||
bienX: "Autos",
|
||||
bienY: "Gasolina",
|
||||
datos: {
|
||||
precioYInicial: 4,
|
||||
precioYFinal: 6,
|
||||
cantidadXInicial: 1000,
|
||||
cantidadXFinal: 700
|
||||
},
|
||||
pasos: [
|
||||
"%ΔQx = (700-1000)/((1000+700)/2) = -300/850 = -35.29%",
|
||||
"%ΔPy = (6-4)/((4+6)/2) = 2/5 = 40%",
|
||||
"Exy = -35.29% / 40% = -0.88"
|
||||
],
|
||||
respuesta: "COMPLEMENTOS",
|
||||
justificacion: "Exy = -0.88 < 0: Al subir gasolina, disminuye demanda de autos"
|
||||
},
|
||||
{
|
||||
id: "caso-c",
|
||||
bienX: "Libros",
|
||||
bienY: "Manzanas",
|
||||
datos: {
|
||||
precioYInicial: 2,
|
||||
precioYFinal: 3,
|
||||
cantidadXInicial: 50,
|
||||
cantidadXFinal: 50
|
||||
},
|
||||
pasos: [
|
||||
"%ΔQx = (50-50)/((50+50)/2) = 0%",
|
||||
"%ΔPy = (3-2)/((2+3)/2) = 1/2.5 = 40%",
|
||||
"Exy = 0% / 40% = 0"
|
||||
],
|
||||
respuesta: "INDEPENDIENTES",
|
||||
justificacion: "Exy = 0: El precio de las manzanas no afecta demanda de libros"
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export const ejerciciosExamen = [
|
||||
{
|
||||
id: "examen-1",
|
||||
tipo: "examen",
|
||||
titulo: "Problema Tipo Examen - Análisis de Mercado",
|
||||
dificultad: "avanzado",
|
||||
tiempoEstimado: 25,
|
||||
enunciado: `La empresa "TechPhone" vende smartphones. El año pasado, con un precio de $800,
|
||||
vendieron 50,000 unidades. Este año, debido a la competencia, bajaron el precio a $720
|
||||
y vendieron 65,000 unidades.
|
||||
|
||||
a) Calcule la elasticidad precio de la demanda usando el método del punto medio.
|
||||
b) Clasifique la demanda y explique qué significa para la empresa.
|
||||
c) ¿Qué pasaría con los ingresos totales si TechPhone subiera el precio a $850?
|
||||
(Calcule los ingresos en ambos escenarios y compare)`,
|
||||
|
||||
solucion: {
|
||||
parteA: {
|
||||
pasos: [
|
||||
{
|
||||
descripcion: "Datos",
|
||||
calculo: "P₁ = $800, P₂ = $720, Q₁ = 50,000, Q₂ = 65,000"
|
||||
},
|
||||
{
|
||||
descripcion: "Calcular %ΔQ",
|
||||
calculo: "%ΔQ = (65,000-50,000)/((50,000+65,000)/2) = 15,000/57,500 = 26.09%"
|
||||
},
|
||||
{
|
||||
descripcion: "Calcular %ΔP",
|
||||
calculo: "%ΔP = (720-800)/((800+720)/2) = -80/760 = -10.53%"
|
||||
},
|
||||
{
|
||||
descripcion: "Calcular Ed",
|
||||
calculo: "Ed = 26.09% / -10.53% = -2.48",
|
||||
resultado: "|Ed| = 2.48"
|
||||
}
|
||||
],
|
||||
respuesta: "Ed = -2.48 (elástica)"
|
||||
},
|
||||
|
||||
parteB: {
|
||||
clasificacion: "Demanda ELÁSTICA (|Ed| = 2.48 > 1)",
|
||||
interpretacion: `La demanda es muy sensible al precio. Un cambio de 1% en el precio
|
||||
produce un cambio de 2.48% en la cantidad demandada (en sentido opuesto). Esto indica
|
||||
que existen muchos competidores y sustitutos en el mercado de smartphones.`,
|
||||
implicacionEmpresa: `TechPhone tiene poco poder de fijación de precios. Si sube precios,
|
||||
perderá muchos clientes a la competencia.`
|
||||
},
|
||||
|
||||
parteC: {
|
||||
escenarioActual: {
|
||||
precio: 720,
|
||||
cantidad: 65000,
|
||||
ingresoTotal: 720 * 65000
|
||||
},
|
||||
escenarioPropuesto: {
|
||||
precio: 850,
|
||||
cantidadEstimada: "Usar elasticidad para estimar",
|
||||
calculoCantidad: [
|
||||
"%ΔP = (850-720)/720 × 100 = 18.06%",
|
||||
"Como Ed = -2.48, %ΔQ = -2.48 × 18.06% = -44.79%",
|
||||
"Q nueva = 65,000 × (1 - 0.4479) = 35,887 unidades"
|
||||
],
|
||||
ingresoTotal: 850 * 35887
|
||||
},
|
||||
comparacion: {
|
||||
ingresoActual: 46800000,
|
||||
ingresoConSubida: 30503950,
|
||||
diferencia: -16296050,
|
||||
porcentaje: -34.8
|
||||
},
|
||||
conclusion: `Si TechPhone sube el precio a $850, sus ingresos caerían aproximadamente
|
||||
$16.3 millones (35% menos). Como la demanda es elástica, subir precios reduce los ingresos
|
||||
totales. La estrategia correcta sería BAJAR precios para aumentar ingresos.`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
id: "examen-2",
|
||||
tipo: "examen",
|
||||
titulo: "Caso Real - Bienes Inferiores en Recesión",
|
||||
dificultad: "avanzado",
|
||||
tiempoEstimado: 20,
|
||||
enunciado: `Durante una recesión económica, el ingreso promedio familiar cayó de $4000 a $3200
|
||||
mensuales. Como resultado:
|
||||
- Las ventas de carne de res cayeron de 8 kg a 5 kg por familia
|
||||
- Las ventas de fideos instantáneos subieron de 10 paquetes a 18 paquetes
|
||||
|
||||
a) Calcule la elasticidad ingreso para cada bien.
|
||||
b) Clasifique cada bien y explique el comportamiento observado.
|
||||
c) ¿Qué tipo de negocios prosperarían en una recesión según estos datos?`,
|
||||
|
||||
solucion: {
|
||||
parteA: {
|
||||
carneRes: {
|
||||
pasos: [
|
||||
"%ΔQ = (5-8)/((8+5)/2) = -3/6.5 = -46.15%",
|
||||
"%ΔI = (3200-4000)/((4000+3200)/2) = -800/3600 = -22.22%",
|
||||
"Ei = -46.15% / -22.22% = 2.08"
|
||||
],
|
||||
resultado: "Ei = 2.08"
|
||||
},
|
||||
fideos: {
|
||||
pasos: [
|
||||
"%ΔQ = (18-10)/((10+18)/2) = 8/14 = 57.14%",
|
||||
"%ΔI = (3200-4000)/((4000+3200)/2) = -800/3600 = -22.22%",
|
||||
"Ei = 57.14% / -22.22% = -2.57"
|
||||
],
|
||||
resultado: "Ei = -2.57"
|
||||
}
|
||||
},
|
||||
|
||||
parteB: {
|
||||
carneRes: {
|
||||
clasificacion: "BIEN DE LUJO",
|
||||
explicacion: `Ei = 2.08 > 1. Cuando el ingreso cayó 22%, el consumo de carne cayó 46%.
|
||||
El consumo es muy sensible al ingreso, cayendo más que proporcionalmente.`
|
||||
},
|
||||
fideos: {
|
||||
clasificacion: "BIEN INFERIOR",
|
||||
explicacion: `Ei = -2.57 < 0. Cuando el ingreso cayó 22%, el consumo de fideos subió 57%.
|
||||
Las familias sustituyeron carne por fideos al empobrecerse.`
|
||||
}
|
||||
},
|
||||
|
||||
parteC: {
|
||||
negociosProsperos: [
|
||||
"Tiendas de descuento y marcas genéricas",
|
||||
"Comida rápida económica",
|
||||
"Transporte público",
|
||||
"Productos de segunda mano",
|
||||
"Entretenimiento en casa (streaming vs cine)"
|
||||
],
|
||||
justificacion: `Los bienes inferiores ven aumentar su demanda en recesiones. Las empresas
|
||||
que venden estos bienes tienden a tener ventas estables o crecientes durante crisis económicas,
|
||||
mientras que las de bienes de lujo sufren.`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
id: "examen-3",
|
||||
tipo: "examen",
|
||||
titulo: "Problema Integrador - Todas las Elasticidades",
|
||||
dificultad: "avanzado",
|
||||
tiempoEstimado: 30,
|
||||
enunciado: `Una cadena de supermercados analiza el mercado de bebidas. Recopilan los siguientes datos:
|
||||
|
||||
CASO 1: Cuando el precio del jugo de naranja bajó de $5 a $4:
|
||||
- Ventas de jugo de naranja: de 1000 a 1400 litros
|
||||
- Ventas de jugo de manzana: de 800 a 600 litros
|
||||
|
||||
CASO 2: Cuando el ingreso promedio de clientes subió de $3000 a $3600:
|
||||
- Ventas de jugo de naranja: de 1000 a 1300 litros
|
||||
- Ventas de soda: de 2000 a 1600 litros
|
||||
|
||||
Resuelva:
|
||||
a) Elasticidad precio del jugo de naranja. ¿Es elástica o inelástica?
|
||||
b) Elasticidad cruzada entre jugo de naranja y manzana. ¿Qué relación tienen?
|
||||
c) Elasticidad ingreso del jugo de naranja. ¿Qué tipo de bien es?
|
||||
d) Elasticidad ingreso de la soda. ¿Qué tipo de bien es?
|
||||
e) Si el supermercado quiere maximizar ingresos por ventas de jugo de naranja,
|
||||
¿debería subir o bajar el precio? Justifique con números.`,
|
||||
|
||||
solucion: {
|
||||
parteA: {
|
||||
descripcion: "Elasticidad precio del jugo de naranja",
|
||||
pasos: [
|
||||
"P₁=$5, P₂=$4, Q₁=1000, Q₂=1400",
|
||||
"%ΔQ = (1400-1000)/((1000+1400)/2) = 400/1200 = 33.33%",
|
||||
"%ΔP = (4-5)/((5+4)/2) = -1/4.5 = -22.22%",
|
||||
"Ed = 33.33% / -22.22% = -1.5",
|
||||
"|Ed| = 1.5"
|
||||
],
|
||||
respuesta: "Ed = -1.5 → ELÁSTICA",
|
||||
interpretacion: "Por cada 1% que baja el precio, la cantidad demandada aumenta 1.5%"
|
||||
},
|
||||
|
||||
parteB: {
|
||||
descripcion: "Elasticidad cruzada (naranja X, manzana Y)",
|
||||
pasos: [
|
||||
"Py₁=$4 (asumiendo precio inicial de manzana), pero mejor usar %ΔPy del naranja",
|
||||
"Exy = (%ΔQx manzana) / (%ΔPy naranja)",
|
||||
"%ΔQx manzana = (600-800)/((800+600)/2) = -200/700 = -28.57%",
|
||||
"%ΔPy naranja = -22.22% (de la parte a)",
|
||||
"Exy = -28.57% / -22.22% = +1.29"
|
||||
],
|
||||
respuesta: "Exy = +1.29 → SUSTITUTOS",
|
||||
interpretacion: "Signo positivo indica sustitutos. Al bajar el precio del jugo de naranja, la gente compra menos manzana"
|
||||
},
|
||||
|
||||
parteC: {
|
||||
descripcion: "Elasticidad ingreso del jugo de naranja",
|
||||
pasos: [
|
||||
"I₁=$3000, I₂=$3600, Q₁=1000, Q₂=1300",
|
||||
"%ΔQ = (1300-1000)/((1000+1300)/2) = 300/1150 = 26.09%",
|
||||
"%ΔI = (3600-3000)/((3000+3600)/2) = 600/3300 = 18.18%",
|
||||
"Ei = 26.09% / 18.18% = 1.44"
|
||||
],
|
||||
respuesta: "Ei = 1.44 → BIEN DE LUJO",
|
||||
interpretacion: "Ei > 1 indica que el jugo de naranja es un bien de lujo. El consumo crece más que proporcionalmente al ingreso"
|
||||
},
|
||||
|
||||
parteD: {
|
||||
descripcion: "Elasticidad ingreso de la soda",
|
||||
pasos: [
|
||||
"I₁=$3000, I₂=$3600, Q₁=2000, Q₂=1600",
|
||||
"%ΔQ = (1600-2000)/((2000+1600)/2) = -400/1800 = -22.22%",
|
||||
"%ΔI = 18.18% (igual que arriba)",
|
||||
"Ei = -22.22% / 18.18% = -1.22"
|
||||
],
|
||||
respuesta: "Ei = -1.22 → BIEN INFERIOR",
|
||||
interpretacion: "Ei < 0 indica bien inferior. Al subir el ingreso, la gente compra menos soda (prefiere jugos naturales)"
|
||||
},
|
||||
|
||||
parteE: {
|
||||
descripcion: "Estrategia de precios para maximizar ingresos",
|
||||
analisis: {
|
||||
elasticidad: "Ed = -1.5 (elástica)",
|
||||
regla: "Cuando |Ed| > 1, subir precio reduce ingresos; bajar precio aumenta ingresos"
|
||||
},
|
||||
calculoComparativo: {
|
||||
escenario1: { precio: 5, cantidad: 1000, ingreso: 5000 },
|
||||
escenario2: { precio: 4, cantidad: 1400, ingreso: 5600 }
|
||||
},
|
||||
diferencia: 600,
|
||||
porcentaje: "+12%",
|
||||
respuesta: "BAJAR EL PRECIO",
|
||||
justificacion: `Al bajar el precio de $5 a $4, los ingresos aumentaron de $5000 a $5600 (+12%).
|
||||
Como la demanda es elástica, el aumento porcentual en cantidad (33%) supera la caída porcentual
|
||||
en precio (22%), resultando en mayores ingresos totales.`
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export const datosPractica = {
|
||||
bienesEjemplo: [
|
||||
{ nombre: "Gasolina", ed: 0.2, ei: 0.8, tipo: "Necesidad inelástica" },
|
||||
{ nombre: "Restaurantes", ed: 1.6, ei: 2.2, tipo: "Lujo elástico" },
|
||||
{ nombre: "Cine", ed: 3.0, ei: 1.8, tipo: "Entretenimiento elástico" },
|
||||
{ nombre: "Medicinas", ed: 0.1, ei: 0.2, tipo: "Necesidad muy inelástica" },
|
||||
{ nombre: "Viajes internacionales", ed: 4.0, ei: 3.5, tipo: "Lujo muy elástico" },
|
||||
{ nombre: "Sal", ed: 0.05, ei: 0.1, tipo: "Necesidad casi perfectamente inelástica" },
|
||||
{ nombre: "Cerveza", ed: 1.2, ei: 0.9, tipo: "Bien normal elástico" },
|
||||
{ nombre: "Transporte público", ed: 0.4, ei: -0.6, tipo: "Inferior inelástico" },
|
||||
{ nombre: "Marca genérica", ed: 2.5, ei: -1.2, tipo: "Inferior elástico" },
|
||||
{ nombre: "Vivienda", ed: 0.8, ei: 1.1, tipo: "Lujo/Necesidad borde" }
|
||||
],
|
||||
|
||||
formulasRapidas: {
|
||||
precioDemanda: {
|
||||
nombre: "Elasticidad Precio Demanda",
|
||||
latex: "E_d = \\frac{\\% \\Delta Q_d}{\\% \\Delta P}",
|
||||
signo: "Negativo (usar |Ed|)",
|
||||
interpretacion: {
|
||||
mayor1: "Elástica - sensible al precio",
|
||||
menor1: "Inelástica - poco sensible al precio",
|
||||
igual1: "Unitaria - cambio proporcional"
|
||||
}
|
||||
},
|
||||
ingreso: {
|
||||
nombre: "Elasticidad Ingreso",
|
||||
latex: "E_i = \\frac{\\% \\Delta Q}{\\% \\Delta I}",
|
||||
clasificacion: {
|
||||
mayor0: "Bien Normal",
|
||||
entre0y1: "Necesidad",
|
||||
mayor1: "Lujo",
|
||||
menor0: "Inferior"
|
||||
}
|
||||
},
|
||||
cruzada: {
|
||||
nombre: "Elasticidad Cruzada",
|
||||
latex: "E_{xy} = \\frac{\\% \\Delta Q_x}{\\% \\Delta P_y}",
|
||||
clasificacion: {
|
||||
mayor0: "Sustitutos",
|
||||
menor0: "Complementos",
|
||||
igual0: "Independientes"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
ejerciciosCalculadora: ejerciciosElasticidad,
|
||||
ejerciciosClasificacion: ejerciciosClasificacion,
|
||||
ejerciciosExamen: ejerciciosExamen,
|
||||
datosPractica
|
||||
};
|
||||
328
frontend/src/content/modulo3/tipos.ts
Normal file
328
frontend/src/content/modulo3/tipos.ts
Normal file
@@ -0,0 +1,328 @@
|
||||
export const tiposElasticidad = {
|
||||
id: "tipos-elasticidad",
|
||||
titulo: "Tipos de Elasticidad en Economía",
|
||||
|
||||
introduccion: {
|
||||
descripcion: `Además de la elasticidad precio de la demanda, existen otros tipos de elasticidad
|
||||
que miden la respuesta de la cantidad ante diferentes variables económicas. Cada tipo de elasticidad
|
||||
proporciona información valiosa sobre el comportamiento de consumidores y productores.`
|
||||
},
|
||||
|
||||
tipos: [
|
||||
{
|
||||
id: "elasticidad-precio-demanda",
|
||||
nombre: "Elasticidad Precio de la Demanda (Ed)",
|
||||
abreviatura: "Ed o Ep",
|
||||
descripcion: "Mide la sensibilidad de la cantidad demandada ante cambios en el precio del propio bien",
|
||||
|
||||
formula: {
|
||||
latex: "E_d = \\frac{\\% \\Delta Q_d}{\\% \\Delta P} = \\frac{\\Delta Q_d / Q_d}{\\Delta P / P}",
|
||||
verbal: "Porcentaje de cambio en cantidad demandada dividido por porcentaje de cambio en precio",
|
||||
nota: "Siempre es negativa (ley de la demanda), pero se usa valor absoluto para clasificar"
|
||||
},
|
||||
|
||||
interpretacion: {
|
||||
negativa: "Por convención, se reporta en valor absoluto (positivo)",
|
||||
elastico: "|Ed| > 1: La cantidad es muy sensible al precio",
|
||||
inelastico: "|Ed| < 1: La cantidad es poco sensible al precio",
|
||||
unitario: "|Ed| = 1: Cambio proporcional"
|
||||
},
|
||||
|
||||
ejemploNumerico: {
|
||||
titulo: "Ejemplo: Gasolina",
|
||||
datos: {
|
||||
precioInicial: 4.0,
|
||||
precioFinal: 4.4,
|
||||
cantidadInicial: 1000,
|
||||
cantidadFinal: 950
|
||||
},
|
||||
calculo: [
|
||||
"%ΔQ = (950 - 1000) / 1000 × 100 = -5%",
|
||||
"%ΔP = (4.4 - 4.0) / 4.0 × 100 = 10%",
|
||||
"Ed = -5% / 10% = -0.5",
|
||||
"|Ed| = 0.5 (INELÁSTICA)"
|
||||
],
|
||||
conclusion: "La gasolina tiene demanda inelástica a corto plazo porque es una necesidad"
|
||||
},
|
||||
|
||||
determinantes: [
|
||||
"Disponibilidad de sustitutos cercanos",
|
||||
"Naturaleza del bien (necesidad vs. lujo)",
|
||||
"Proporción del ingreso gastada en el bien",
|
||||
"Horizonte temporal (corto vs. largo plazo)",
|
||||
"Definición del mercado (amplio vs. específico)"
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
id: "elasticidad-ingreso-demanda",
|
||||
nombre: "Elasticidad Ingreso de la Demanda (Ei)",
|
||||
abreviatura: "Ei o Ey",
|
||||
descripcion: "Mide la sensibilidad de la cantidad demandada ante cambios en el ingreso del consumidor",
|
||||
|
||||
formula: {
|
||||
latex: "E_i = \\frac{\\% \\Delta Q_d}{\\% \\Delta I} = \\frac{\\Delta Q_d / Q_d}{\\Delta I / I}",
|
||||
verbal: "Porcentaje de cambio en cantidad demandada dividido por porcentaje de cambio en ingreso",
|
||||
donde: [
|
||||
{ variable: "Qd", significado: "Cantidad demandada" },
|
||||
{ variable: "I", significado: "Ingreso del consumidor" }
|
||||
]
|
||||
},
|
||||
|
||||
clasificacionBienes: [
|
||||
{
|
||||
tipo: "Bien Normal",
|
||||
condicion: "Ei > 0",
|
||||
descripcion: "La cantidad demandada aumenta cuando aumenta el ingreso",
|
||||
subtipos: [
|
||||
{ tipo: "Bien Necesario", rango: "0 < Ei < 1", ejemplo: "Alimentos básicos" },
|
||||
{ tipo: "Bien de Lujo", rango: "Ei > 1", ejemplo: "Viajes, joyas, autos deportivos" }
|
||||
]
|
||||
},
|
||||
{
|
||||
tipo: "Bien Inferior",
|
||||
condicion: "Ei < 0",
|
||||
descripcion: "La cantidad demandada disminuye cuando aumenta el ingreso",
|
||||
ejemplo: "Transporte público, fideos instantáneos, marca genérica"
|
||||
}
|
||||
],
|
||||
|
||||
ejemploNumerico: {
|
||||
titulo: "Ejemplo: Restaurantes de lujo",
|
||||
datos: {
|
||||
ingresoInicial: 50000,
|
||||
ingresoFinal: 60000,
|
||||
cantidadInicial: 12,
|
||||
cantidadFinal: 20
|
||||
},
|
||||
calculo: [
|
||||
"%ΔQ = (20 - 12) / 12 × 100 = 66.67%",
|
||||
"%ΔI = (60000 - 50000) / 50000 × 100 = 20%",
|
||||
"Ei = 66.67% / 20% = 3.33",
|
||||
"Ei > 1 → Bien de LUJO"
|
||||
],
|
||||
conclusion: "Los restaurantes de lujo son un bien de lujo porque su demanda crece más que proporcionalmente al ingreso"
|
||||
},
|
||||
|
||||
aplicacion: "Ayuda a predecir cómo cambiará la demanda en ciclos económicos (expansión/recesión)"
|
||||
},
|
||||
|
||||
{
|
||||
id: "elasticidad-cruzada",
|
||||
nombre: "Elasticidad Cruzada de la Demanda (Exy)",
|
||||
abreviatura: "Exy o Ec",
|
||||
descripcion: "Mide la sensibilidad de la cantidad demandada de un bien X ante cambios en el precio de otro bien Y",
|
||||
|
||||
formula: {
|
||||
latex: "E_{xy} = \\frac{\\% \\Delta Q_x}{\\% \\Delta P_y} = \\frac{\\Delta Q_x / Q_x}{\\Delta P_y / P_y}",
|
||||
verbal: "Porcentaje de cambio en cantidad demandada del bien X dividido por porcentaje de cambio en precio del bien Y",
|
||||
donde: [
|
||||
{ variable: "Qx", significado: "Cantidad demandada del bien X" },
|
||||
{ variable: "Py", significado: "Precio del bien Y" }
|
||||
]
|
||||
},
|
||||
|
||||
clasificacionBienes: [
|
||||
{
|
||||
tipo: "Bienes Sustitutos",
|
||||
condicion: "Exy > 0",
|
||||
signo: "Positiva",
|
||||
descripcion: "Si sube el precio de Y, aumenta la demanda de X",
|
||||
ejemplo: "Coca-Cola y Pepsi, café y té, mantequilla y margarina",
|
||||
logica: "Cuando el café sube de precio, la gente consume más té"
|
||||
},
|
||||
{
|
||||
tipo: "Bienes Complementarios",
|
||||
condicion: "Exy < 0",
|
||||
signo: "Negativa",
|
||||
descripcion: "Si sube el precio de Y, disminuye la demanda de X",
|
||||
ejemplo: "Autos y gasolina, computadores y software, pan y mantequilla",
|
||||
logica: "Si sube el precio de la gasolina, se demandan menos autos"
|
||||
},
|
||||
{
|
||||
tipo: "Bienes Independientes",
|
||||
condicion: "Exy = 0",
|
||||
signo: "Cero",
|
||||
descripcion: "El precio de Y no afecta la demanda de X",
|
||||
ejemplo: "Zapatos y tomates, libros y sillas",
|
||||
logica: "No existe relación de consumo entre ambos bienes"
|
||||
}
|
||||
],
|
||||
|
||||
ejemploNumerico: {
|
||||
titulo: "Ejemplo: Café (X) y Té (Y) - Sustitutos",
|
||||
datos: {
|
||||
precioTeInicial: 3,
|
||||
precioTeFinal: 3.6,
|
||||
cantidadCafeInicial: 100,
|
||||
cantidadCafeFinal: 120
|
||||
},
|
||||
calculo: [
|
||||
"%ΔQx = (120 - 100) / 100 × 100 = 20%",
|
||||
"%ΔPy = (3.6 - 3) / 3 × 100 = 20%",
|
||||
"Exy = 20% / 20% = 1.0",
|
||||
"Exy > 0 → BIENES SUSTITUTOS"
|
||||
],
|
||||
conclusion: "El café y el té son sustitutos porque al subir el precio del té, aumenta la demanda de café"
|
||||
},
|
||||
|
||||
magnitud: "Entre más grande sea el valor absoluto de Exy, más fuerte es la relación entre los bienes"
|
||||
},
|
||||
|
||||
{
|
||||
id: "elasticidad-precio-oferta",
|
||||
nombre: "Elasticidad Precio de la Oferta (Es o Eo)",
|
||||
abreviatura: "Es",
|
||||
descripcion: "Mide la sensibilidad de la cantidad ofrecida ante cambios en el precio del bien",
|
||||
|
||||
formula: {
|
||||
latex: "E_s = \\frac{\\% \\Delta Q_s}{\\% \\Delta P} = \\frac{\\Delta Q_s / Q_s}{\\Delta P / P}",
|
||||
verbal: "Porcentaje de cambio en cantidad ofrecida dividido por porcentaje de cambio en precio",
|
||||
nota: "Siempre es positiva (ley de la oferta)"
|
||||
},
|
||||
|
||||
interpretacion: [
|
||||
{
|
||||
rango: "Es > 1",
|
||||
clasificacion: "Oferta ELÁSTICA",
|
||||
significado: "La cantidad ofrecida es muy sensible al precio",
|
||||
ejemplo: "Bienes manufacturados que se pueden producir rápidamente"
|
||||
},
|
||||
{
|
||||
rango: "Es < 1",
|
||||
clasificacion: "Oferta INELÁSTICA",
|
||||
significado: "La cantidad ofrecida es poco sensible al precio",
|
||||
ejemplo: "Bienes agrícolas a corto plazo, bienes con capacidad limitada"
|
||||
},
|
||||
{
|
||||
rango: "Es = 1",
|
||||
clasificacion: "Oferta UNITARIA",
|
||||
significado: "Cambio proporcional en cantidad ofrecida"
|
||||
},
|
||||
{
|
||||
rango: "Es = 0",
|
||||
clasificacion: "Oferta PERFECTAMENTE INELÁSTICA",
|
||||
significado: "Cantidad fija sin importar el precio",
|
||||
ejemplo: "Obras de arte únicas, terrenos en una ubicación específica"
|
||||
},
|
||||
{
|
||||
rango: "Es = ∞",
|
||||
clasificacion: "Oferta PERFECTAMENTE ELÁSTICA",
|
||||
significado: "Los productores ofrecen cualquier cantidad al precio de mercado",
|
||||
ejemplo: "Industria con capacidad ilimitada y costos constantes"
|
||||
}
|
||||
],
|
||||
|
||||
horizonteTemporal: {
|
||||
titulo: "Elasticidad en Diferentes Horizontes Temporales",
|
||||
descripcion: "La elasticidad de la oferta varía según el tiempo disponible para ajustar la producción",
|
||||
|
||||
periodos: [
|
||||
{
|
||||
periodo: "Mercado Momentáneo o Very Short Run",
|
||||
tiempo: "Horas o días",
|
||||
caracteristicas: "Cantidad fija, Es = 0",
|
||||
ejemplo: "Pescado fresco del día, flores cortadas",
|
||||
curva: "Línea vertical"
|
||||
},
|
||||
{
|
||||
periodo: "Corto Plazo (Short Run)",
|
||||
tiempo: "Meses",
|
||||
caracteristicas: "Es inelástica pero > 0, algunos factores son fijos",
|
||||
ejemplo: "Agricultura (tierra fija), manufactura (planta fija)",
|
||||
curva: "Pendiente positiva empinada"
|
||||
},
|
||||
{
|
||||
periodo: "Largo Plazo (Long Run)",
|
||||
tiempo: "Años",
|
||||
caracteristicas: "Es más elástica, todos los factores son variables",
|
||||
ejemplo: "Pueden construirse nuevas fábricas, comprarse más tierras",
|
||||
curva: "Pendiente positiva más plana"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
ejemploNumerico: {
|
||||
titulo: "Ejemplo: Tomates (corto plazo vs largo plazo)",
|
||||
|
||||
cortoPlazo: {
|
||||
datos: {
|
||||
precioInicial: 2,
|
||||
precioFinal: 3,
|
||||
cantidadInicial: 1000,
|
||||
cantidadFinal: 1100
|
||||
},
|
||||
calculo: [
|
||||
"%ΔQs = (1100 - 1000) / 1000 × 100 = 10%",
|
||||
"%ΔP = (3 - 2) / 2 × 100 = 50%",
|
||||
"Es = 10% / 50% = 0.2 (INELÁSTICA)"
|
||||
],
|
||||
explicacion: "En el corto plazo no se pueden plantar más tomates, la oferta es rígida"
|
||||
},
|
||||
|
||||
largoPlazo: {
|
||||
datos: {
|
||||
precioInicial: 2,
|
||||
precioFinal: 3,
|
||||
cantidadInicial: 1000,
|
||||
cantidadFinal: 2000
|
||||
},
|
||||
calculo: [
|
||||
"%ΔQs = (2000 - 1000) / 1000 × 100 = 100%",
|
||||
"%ΔP = (3 - 2) / 2 × 100 = 50%",
|
||||
"Es = 100% / 50% = 2.0 (ELÁSTICA)"
|
||||
],
|
||||
explicacion: "En el largo plazo se pueden ampliar los cultivos, la oferta es flexible"
|
||||
}
|
||||
},
|
||||
|
||||
determinantes: [
|
||||
"Flexibilidad de los factores de producción",
|
||||
"Tiempo necesario para ajustar la producción",
|
||||
"Costos de almacenamiento",
|
||||
"Capacidad ociosa disponible",
|
||||
"Movilidad de los factores productivos"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
tablaComparativa: {
|
||||
titulo: "Tabla Comparativa de Tipos de Elasticidad",
|
||||
columnas: ["Tipo", "Fórmula", "Signo", "Interpretación Principal"],
|
||||
filas: [
|
||||
["Precio Demanda (Ed)", "%ΔQd / %ΔP", "Negativo (|Ed|)", "Sensibilidad al precio propio"],
|
||||
["Ingreso (Ei)", "%ΔQd / %ΔI", "Positivo/Negativo", "Clasifica bienes normales/inferiores"],
|
||||
["Cruzada (Exy)", "%ΔQx / %ΔPy", "Positivo/Negativo/Cero", "Identifica sustitutos/complementos"],
|
||||
["Precio Oferta (Es)", "%ΔQs / %ΔP", "Positivo", "Capacidad de respuesta de productores"]
|
||||
]
|
||||
},
|
||||
|
||||
ejercicioIntegrador: {
|
||||
titulo: "Ejercicio Integrador: Análisis Completo",
|
||||
escenario: `Una empresa vende smartphones. Observa que cuando el precio baja de $800 a $720,
|
||||
la cantidad demandada aumenta de 1000 a 1200 unidades. Además, cuando el ingreso promedio
|
||||
de los consumidores sube de $3000 a $3300, la cantidad demandada aumenta de 1000 a 1150 unidades.
|
||||
Finalmente, cuando el precio de los tablets (bien relacionado) sube de $500 a $600,
|
||||
la cantidad demandada de smartphones aumenta de 1000 a 1100 unidades.`,
|
||||
|
||||
preguntas: [
|
||||
{
|
||||
pregunta: "Calcular Ed (elasticidad precio)",
|
||||
respuesta: "|Ed| = 1.76 → Demanda ELÁSTICA",
|
||||
interpretacion: "Los smartphones son sensibles al precio"
|
||||
},
|
||||
{
|
||||
pregunta: "Calcular Ei (elasticidad ingreso)",
|
||||
respuesta: "Ei = 1.5 → Bien de LUJO",
|
||||
interpretacion: "La demanda crece más que proporcionalmente al ingreso"
|
||||
},
|
||||
{
|
||||
pregunta: "Calcular Exy (elasticidad cruzada con tablets)",
|
||||
respuesta: "Exy = 0.45 → BIENES SUSTITUTOS",
|
||||
interpretacion: "Tablets y smartphones son sustitutos débiles"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
export default tiposElasticidad;
|
||||
237
frontend/src/content/modulo4/costos.ts
Normal file
237
frontend/src/content/modulo4/costos.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
export interface Seccion {
|
||||
titulo: string;
|
||||
contenido: string;
|
||||
}
|
||||
|
||||
export interface Ejercicio {
|
||||
id: string;
|
||||
tipo: 'slider' | 'quiz' | 'juego' | 'tabla' | 'calculadora';
|
||||
titulo: string;
|
||||
descripcion: string;
|
||||
config: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ModuloContenido {
|
||||
titulo: string;
|
||||
contenido: Seccion[];
|
||||
ejercicios: Ejercicio[];
|
||||
}
|
||||
|
||||
export const costos: ModuloContenido = {
|
||||
titulo: 'Costos de Producción',
|
||||
contenido: [
|
||||
{
|
||||
titulo: 'Costos Fijos y Variables',
|
||||
contenido: `Los costos totales se componen de dos categorías fundamentales:
|
||||
|
||||
**Costos Fijos (CF)**
|
||||
Son costos que no varían con la cantidad producida. Se incurren incluso si Q = 0.
|
||||
|
||||
Ejemplos:
|
||||
- Alquiler de la planta
|
||||
- Seguros
|
||||
- Salarios de administración
|
||||
- Depreciación (método lineal)
|
||||
|
||||
**Costos Variables (CV)**
|
||||
Varían directamente con el nivel de producción. Si Q = 0, CV = 0.
|
||||
|
||||
Ejemplos:
|
||||
- Materias primas
|
||||
- Mano de obra directa
|
||||
- Energía consumida
|
||||
- Envases y embalajes
|
||||
|
||||
**Costo Total (CT)**
|
||||
$$CT = CF + CV$$
|
||||
|
||||
**Ejemplo numérico:**
|
||||
Una panadería tiene:
|
||||
- CF = $1,000/mes (alquiler, seguros)
|
||||
- CV = $5 por pan (harina, salario panadero)
|
||||
|
||||
| Q (panes) | CF | CV | CT |
|
||||
|-----------|----|----|----|
|
||||
| 0 | 1,000 | 0 | 1,000 |
|
||||
| 100 | 1,000 | 500 | 1,500 |
|
||||
| 200 | 1,000 | 1,000 | 2,000 |
|
||||
| 300 | 1,000 | 1,500 | 2,500 |
|
||||
| 400 | 1,000 | 2,000 | 3,000 |`
|
||||
},
|
||||
{
|
||||
titulo: 'Costos Medios',
|
||||
contenido: `Los costos medios (o unitarios) representan el costo por unidad producida:
|
||||
|
||||
**Costo Fijo Medio (CFMe)**
|
||||
$$CFMe = \frac{CF}{Q}$$
|
||||
|
||||
Característica: Siempre decreciente conforme aumenta Q (se "diluye" el costo fijo).
|
||||
|
||||
**Costo Variable Medio (CVMe)**
|
||||
$$CVMe = \frac{CV}{Q}$$
|
||||
|
||||
Forma típica: U invertida (primero decrece por economías de escala, luego crece por deseconomías).
|
||||
|
||||
**Costo Total Medio (CMe o CTM)**
|
||||
$$CMe = \frac{CT}{Q} = CFMe + CVMe$$
|
||||
|
||||
Forma típica: U invertida, con un mínimo donde CMe = CMg.
|
||||
|
||||
**Tabla de ejemplo:**
|
||||
| Q | CF | CV | CT | CFMe | CVMe | CMe |
|
||||
|---|----|----|----|------|------|-----|
|
||||
| 0 | 100 | 0 | 100 | - | - | - |
|
||||
| 1 | 100 | 50 | 150 | 100.0 | 50.0 | 150.0 |
|
||||
| 2 | 100 | 90 | 190 | 50.0 | 45.0 | 95.0 |
|
||||
| 3 | 100 | 120 | 220 | 33.3 | 40.0 | 73.3 |
|
||||
| 4 | 100 | 160 | 260 | 25.0 | 40.0 | 65.0 |
|
||||
| 5 | 100 | 250 | 350 | 20.0 | 50.0 | 70.0 |
|
||||
|
||||
Observa que CMe es mínimo (65.0) cuando está en su punto más bajo entre CFMe y CVMe.`
|
||||
},
|
||||
{
|
||||
titulo: 'Costo Marginal',
|
||||
contenido: `El **costo marginal (CMg)** es el incremento en el costo total al producir una unidad adicional:
|
||||
|
||||
$$CMg = \frac{\Delta CT}{\Delta Q} = \frac{dCT}{dQ}$$
|
||||
|
||||
**Importancia:**
|
||||
- Determina la decisión de producción óptima
|
||||
- Representa el costo de la última unidad producida
|
||||
- Es la derivada del costo total
|
||||
|
||||
**Relación fundamental:**
|
||||
$$CMg = \frac{\Delta CV}{\Delta Q}$$
|
||||
|
||||
(Dado que CF no varía con Q, solo CV afecta CMg)
|
||||
|
||||
**Ejemplo de cálculo:**
|
||||
| Q | CT | CMg |
|
||||
|---|----|-----|
|
||||
| 0 | 100 | - |
|
||||
| 1 | 150 | 50 |
|
||||
| 2 | 190 | 40 |
|
||||
| 3 | 220 | 30 |
|
||||
| 4 | 260 | 40 |
|
||||
| 5 | 350 | 90 |
|
||||
|
||||
**Propiedades matemáticas:**
|
||||
1. CMg intercepta CVMe y CMe en sus puntos mínimos
|
||||
2. Cuando CMg < CMe, CMe está decreciendo
|
||||
3. Cuando CMg > CMe, CMe está creciendo
|
||||
4. Cuando CMg = CMe, CMe está en su mínimo
|
||||
|
||||
**Intuición:** Si el costo de la siguiente unidad (CMg) es menor que el costo promedio actual, producirla reduce el costo medio.`
|
||||
},
|
||||
{
|
||||
titulo: 'Relación entre Curvas de Costos',
|
||||
contenido: `Las curvas de costos tienen relaciones matemáticas y económicas fundamentales:
|
||||
|
||||
**Gráfico conceptual:**
|
||||
|
||||
Las curvas de costos se relacionan de la siguiente manera:
|
||||
|
||||
| Elemento | Descripción |
|
||||
|----------|-------------|
|
||||
| **Eje vertical** | Costos |
|
||||
| **Eje horizontal** | Cantidad (Q) |
|
||||
| **Curva CMe** | Forma de U invertida |
|
||||
| **Curva CMg** | U invertida que intersecta a CMe en su punto más bajo |
|
||||
| **Punto de eficiencia** | Donde CMg = CMe (mínimo del costo medio) |
|
||||
|
||||
**Relaciones clave entre las curvas:**
|
||||
|
||||
1. **CFMe siempre decreciente:** A medida que aumenta Q, el costo fijo se distribuye en más unidades
|
||||
|
||||
2. **CMg corta a CVMe en su mínimo:**
|
||||
- Antes del punto de intersección: CMg < CVMe → CVMe decrece
|
||||
- Después del punto de intersección: CMg > CVMe → CVMe crece
|
||||
|
||||
3. **CMg corta a CMe en su mínimo:**
|
||||
- Cuando CMg < CMe: CMe está decreciendo
|
||||
- Cuando CMg > CMe: CMe está creciendo
|
||||
- Cuando CMg = CMe: CMe está en su punto mínimo (producción técnicamente más eficiente)
|
||||
|
||||
**Relaciones clave:**
|
||||
|
||||
1. **CFMe siempre decreciente**
|
||||
- Forma de hipérbola rectangular
|
||||
- Nunca intersecta a ninguna otra curva
|
||||
|
||||
2. **CMg corta a CVMe en su mínimo**
|
||||
- Antes: CMg < CVMe → CVMe decrece
|
||||
- Después: CMg > CVMe → CVMe crece
|
||||
|
||||
3. **CMg corta a CMe en su mínimo**
|
||||
- Punto de mínimo costo medio de producción
|
||||
- Producto técnicamente más eficiente
|
||||
|
||||
4. **Forma de las curvas:**
|
||||
- **CT**: Siempre creciente, convexa luego cóncava
|
||||
- **CMg**: U invertida, corta mínimos
|
||||
- **CMe**: U invertida, por encima de CVMe
|
||||
- **CVMe**: U invertida, por debajo de CMe
|
||||
|
||||
**Relación con producción:**
|
||||
El CMg mínimo corresponde al PMg máximo (ley de rendimientos decrecientes en acción). Cuando PMg decrece, CMg crece.`
|
||||
}
|
||||
],
|
||||
ejercicios: [
|
||||
{
|
||||
id: 'costos-calculadora',
|
||||
tipo: 'calculadora',
|
||||
titulo: 'Calculadora de Costos',
|
||||
descripcion: 'Ingresa CF, CV para cada nivel de producción y calcula automáticamente todos los costos medios y marginales',
|
||||
config: {
|
||||
columnas: ['Q', 'CF', 'CV', 'CT', 'CFMe', 'CVMe', 'CMe', 'CMg'],
|
||||
datosEditables: ['CF', 'CV'],
|
||||
calcularAutomatico: true,
|
||||
nivelMaximo: 10,
|
||||
mostrarGrafico: true,
|
||||
destacarMinimos: true
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'costos-relaciones',
|
||||
tipo: 'quiz',
|
||||
titulo: 'Relaciones entre Costos',
|
||||
descripcion: 'Identifica las relaciones correctas entre las curvas de costo',
|
||||
config: {
|
||||
preguntas: [
|
||||
{
|
||||
pregunta: '¿Dónde se intersectan CMg y CMe?',
|
||||
opciones: [
|
||||
'En el origen',
|
||||
'En el mínimo de CMe',
|
||||
'En el máximo de producción',
|
||||
'Nunca se intersectan'
|
||||
],
|
||||
respuestaCorrecta: 1
|
||||
},
|
||||
{
|
||||
pregunta: '¿Qué pasa con CMe cuando CMg < CMe?',
|
||||
opciones: [
|
||||
'CMe aumenta',
|
||||
'CMe disminuye',
|
||||
'CMe se mantiene constante',
|
||||
'CMe se vuelve negativo'
|
||||
],
|
||||
respuestaCorrecta: 1
|
||||
},
|
||||
{
|
||||
pregunta: '¿Por qué CFMe siempre decrece?',
|
||||
opciones: [
|
||||
'Porque CF aumenta',
|
||||
'Porque el costo fijo se distribuye en más unidades',
|
||||
'Porque CV disminuye',
|
||||
'Porque CT es constante'
|
||||
],
|
||||
respuestaCorrecta: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export default costos;
|
||||
249
frontend/src/content/modulo4/ejercicios.ts
Normal file
249
frontend/src/content/modulo4/ejercicios.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
export interface Seccion {
|
||||
titulo: string;
|
||||
contenido: string;
|
||||
}
|
||||
|
||||
export interface Ejercicio {
|
||||
id: string;
|
||||
titulo: string;
|
||||
tipo: 'calculadora' | 'simulador' | 'visualizacion' | 'tabla';
|
||||
descripcion: string;
|
||||
datos: Record<string, unknown>;
|
||||
solucion?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ModuloContenido {
|
||||
titulo: string;
|
||||
contenido: Seccion[];
|
||||
ejercicios: Ejercicio[];
|
||||
}
|
||||
|
||||
export const ejercicios: ModuloContenido = {
|
||||
titulo: 'Ejercicios Prácticos - Teoría del Productor',
|
||||
contenido: [
|
||||
{
|
||||
titulo: 'Guía de Ejercicios',
|
||||
contenido: `Esta sección contiene ejercicios prácticos para aplicar los conceptos de:
|
||||
- Funciones de producción
|
||||
- Cálculo de costos
|
||||
- Decisión óptima de producción
|
||||
- Análisis de excedentes
|
||||
|
||||
Cada ejercicio incluye:
|
||||
- Datos del problema
|
||||
- Paso a paso para resolver
|
||||
- Tablas interactivas
|
||||
- Visualizaciones gráficas
|
||||
- Respuestas y explicaciones`
|
||||
}
|
||||
],
|
||||
ejercicios: [
|
||||
{
|
||||
id: 'ejercicio-1-costos',
|
||||
titulo: 'Ejercicio 1: Calculadora de Costos',
|
||||
tipo: 'tabla',
|
||||
descripcion: 'Completa la tabla de costos a partir de los costos fijos y variables. Identifica el costo total medio mínimo y el costo marginal.',
|
||||
datos: {
|
||||
instrucciones: 'Completa la tabla calculando CT, CFMe, CVMe, CMe y CMg',
|
||||
costoFijo: 200,
|
||||
datosTabla: [
|
||||
{ Q: 0, CV: 0 },
|
||||
{ Q: 1, CV: 50 },
|
||||
{ Q: 2, CV: 90 },
|
||||
{ Q: 3, CV: 120 },
|
||||
{ Q: 4, CV: 160 },
|
||||
{ Q: 5, CV: 220 },
|
||||
{ Q: 6, CV: 300 },
|
||||
{ Q: 7, CV: 400 },
|
||||
{ Q: 8, CV: 520 }
|
||||
],
|
||||
columnasSolucion: ['Q', 'CF', 'CV', 'CT', 'CFMe', 'CVMe', 'CMe', 'CMg']
|
||||
},
|
||||
solucion: {
|
||||
tablaCompleta: [
|
||||
{ Q: 0, CF: 200, CV: 0, CT: 200, CFMe: '-', CVMe: '-', CMe: '-', CMg: '-' },
|
||||
{ Q: 1, CF: 200, CV: 50, CT: 250, CFMe: 200.0, CVMe: 50.0, CMe: 250.0, CMg: 50 },
|
||||
{ Q: 2, CF: 200, CV: 90, CT: 290, CFMe: 100.0, CVMe: 45.0, CMe: 145.0, CMg: 40 },
|
||||
{ Q: 3, CF: 200, CV: 120, CT: 320, CFMe: 66.7, CVMe: 40.0, CMe: 106.7, CMg: 30 },
|
||||
{ Q: 4, CF: 200, CV: 160, CT: 360, CFMe: 50.0, CVMe: 40.0, CMe: 90.0, CMg: 40 },
|
||||
{ Q: 5, CF: 200, CV: 220, CT: 420, CFMe: 40.0, CVMe: 44.0, CMe: 84.0, CMg: 60 },
|
||||
{ Q: 6, CF: 200, CV: 300, CT: 500, CFMe: 33.3, CVMe: 50.0, CMe: 83.3, CMg: 80 },
|
||||
{ Q: 7, CF: 200, CV: 400, CT: 600, CFMe: 28.6, CVMe: 57.1, CMe: 85.7, CMg: 100 },
|
||||
{ Q: 8, CF: 200, CV: 520, CT: 720, CFMe: 25.0, CVMe: 65.0, CMe: 90.0, CMg: 120 }
|
||||
],
|
||||
respuestasClave: {
|
||||
cmeMinimo: { Q: 6, valor: 83.3 },
|
||||
cmgEnQ4: 40,
|
||||
cmgEnQ6: 80,
|
||||
observacion: 'CMe es mínimo cuando CMg pasa de ser menor a mayor que CMe'
|
||||
},
|
||||
pasos: [
|
||||
'Paso 1: CT = CF + CV (CF siempre es 200)',
|
||||
'Paso 2: CFMe = CF/Q',
|
||||
'Paso 3: CVMe = CV/Q',
|
||||
'Paso 4: CMe = CT/Q (o CFMe + CVMe)',
|
||||
'Paso 5: CMg = ΔCT/ΔQ = CT(Q) - CT(Q-1)'
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'ejercicio-2-produccion-optima',
|
||||
titulo: 'Ejercicio 2: Simulador de Decisión de Producción',
|
||||
tipo: 'simulador',
|
||||
descripcion: 'Determina la cantidad óptima de producción dado un precio de mercado y decide si la empresa debe producir, cerrar temporalmente o salir del mercado.',
|
||||
datos: {
|
||||
escenario: {
|
||||
nombre: 'Panadería El Trigo de Oro',
|
||||
precioMercado: 70,
|
||||
costoFijo: 200,
|
||||
funcionCostos: [
|
||||
{ Q: 0, CT: 200 },
|
||||
{ Q: 1, CT: 250 },
|
||||
{ Q: 2, CT: 290 },
|
||||
{ Q: 3, CT: 320 },
|
||||
{ Q: 4, CT: 360 },
|
||||
{ Q: 5, CT: 420 },
|
||||
{ Q: 6, CT: 500 },
|
||||
{ Q: 7, CT: 600 },
|
||||
{ Q: 8, CT: 720 }
|
||||
]
|
||||
},
|
||||
preguntas: [
|
||||
'¿Cuál es la cantidad óptima de producción (Q*)?',
|
||||
'¿Cuál es el beneficio máximo?',
|
||||
'¿Debe producir la empresa o cerrar temporalmente?',
|
||||
'¿Qué sucedería si el precio baja a $40?'
|
||||
],
|
||||
opcionesPrecio: [40, 50, 60, 70, 80, 90]
|
||||
},
|
||||
solucion: {
|
||||
qOptima: 6,
|
||||
beneficioMaximo: -80,
|
||||
decision: 'Producir con pérdidas (menor que CF)',
|
||||
razonamiento: 'P ($70) > CVMe en Q=6 ($50), por lo que cubre costos variables. La pérdida de $80 es menor que CF ($200).',
|
||||
analisisPorPrecio: {
|
||||
'40': { qOptima: 3, beneficio: -200, decision: 'Indiferente (P = CVMe mínimo)', detalle: 'Pérdida = CF. Puede producir o cerrar.' },
|
||||
'50': { qOptima: 4, beneficio: -160, decision: 'Producir con pérdidas', detalle: 'P > CVMe, pérdida ($160) < CF ($200)' },
|
||||
'60': { qOptima: 5, beneficio: -120, decision: 'Producir con pérdidas', detalle: 'P > CVMe, pérdida ($120) < CF ($200)' },
|
||||
'70': { qOptima: 6, beneficio: -80, decision: 'Producir con pérdidas', detalle: 'P > CVMe, pérdida ($80) < CF ($200)' },
|
||||
'80': { qOptima: 6, beneficio: -20, decision: 'Producir con pérdidas', detalle: 'P > CVMe, pérdida ($20) < CF ($200)' },
|
||||
'90': { qOptima: 7, beneficio: 30, decision: 'Producir con beneficios', detalle: 'P > CMe, beneficio económico positivo' }
|
||||
},
|
||||
reglaDecision: {
|
||||
paso1: 'Encontrar Q donde P = CMg (o aproximadamente igual)',
|
||||
paso2: 'Calcular CVMe en esa Q',
|
||||
paso3: 'Si P >= CVMe: Producir. Si P < CVMe: Cerrar',
|
||||
paso4: 'Calcular beneficio: π = IT - CT = (P × Q) - CT'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'ejercicio-3-excedentes',
|
||||
titulo: 'Ejercicio 3: Visualización de Excedentes',
|
||||
tipo: 'visualizacion',
|
||||
descripcion: 'Calcula y visualiza el excedente del productor bajo diferentes escenarios de precio. Comprende la relación entre excedente, costos variables y beneficios.',
|
||||
datos: {
|
||||
escenario: {
|
||||
curvaCMg: [
|
||||
{ Q: 0, CMg: 0 },
|
||||
{ Q: 1, CMg: 10 },
|
||||
{ Q: 2, CMg: 15 },
|
||||
{ Q: 3, CMg: 20 },
|
||||
{ Q: 4, CMg: 25 },
|
||||
{ Q: 5, CMg: 35 },
|
||||
{ Q: 6, CMg: 50 },
|
||||
{ Q: 7, CMg: 70 },
|
||||
{ Q: 8, CMg: 95 }
|
||||
],
|
||||
costoFijo: 100,
|
||||
precioEjemplo: 50
|
||||
},
|
||||
tareas: [
|
||||
'Calcular el excedente del productor a P = $50',
|
||||
'Calcular el costo variable total',
|
||||
'Calcular el beneficio económico',
|
||||
'Visualizar las áreas correspondientes en el gráfico'
|
||||
],
|
||||
escenariosAdicionales: [
|
||||
{ precio: 25, descripcion: 'Punto de cierre' },
|
||||
{ precio: 50, descripcion: 'Producción con pérdidas' },
|
||||
{ precio: 70, descripcion: 'Beneficio positivo' }
|
||||
]
|
||||
},
|
||||
solucion: {
|
||||
escenarioPrincipal: {
|
||||
precio: 50,
|
||||
qOptima: 6,
|
||||
calculoExcedente: [
|
||||
{ unidad: 1, precio: 50, cmg: 10, excedente: 40 },
|
||||
{ unidad: 2, precio: 50, cmg: 15, excedente: 35 },
|
||||
{ unidad: 3, precio: 50, cmg: 20, excedente: 30 },
|
||||
{ unidad: 4, precio: 50, cmg: 25, excedente: 25 },
|
||||
{ unidad: 5, precio: 50, cmg: 35, excedente: 15 },
|
||||
{ unidad: 6, precio: 50, cmg: 50, excedente: 0 }
|
||||
],
|
||||
excedenteTotal: 145,
|
||||
ingresoTotal: 300,
|
||||
costoVariable: 155,
|
||||
costoTotal: 255,
|
||||
beneficio: 45
|
||||
},
|
||||
formulaVerificacion: {
|
||||
metodo1: 'EP = IT - CV = 300 - 155 = 145',
|
||||
metodo2: 'EP = Suma de excedentes = 40+35+30+25+15+0 = 145',
|
||||
metodo3: 'π = EP - CF = 145 - 100 = 45 (Beneficio)'
|
||||
},
|
||||
interpretacionAreas: {
|
||||
areaTotal: 'Rectángulo P × Q = 50 × 6 = 300 (IT)',
|
||||
areaCV: 'Área bajo CMg = 155 (CV)',
|
||||
areaEP: 'Área entre P y CMg = 145 (EP)',
|
||||
areaBeneficio: 'EP - CF = 145 - 100 = 45 (π)'
|
||||
},
|
||||
comparacionEscenarios: {
|
||||
'25': {
|
||||
qOptima: 4,
|
||||
excedente: 25,
|
||||
beneficio: -75,
|
||||
observacion: 'P = CVMe mínimo. EP mínimo positivo, pero π negativo. Indiferente entre producir o cerrar.'
|
||||
},
|
||||
'50': {
|
||||
qOptima: 6,
|
||||
excedente: 145,
|
||||
beneficio: 45,
|
||||
observacion: 'P > CVMe. EP cubre CF parcialmente, quedando beneficio positivo.'
|
||||
},
|
||||
'70': {
|
||||
qOptima: 7,
|
||||
excedente: 265,
|
||||
beneficio: 165,
|
||||
observacion: 'P >> CMe. EP cubre completamente CF y genera beneficio económico significativo.'
|
||||
}
|
||||
},
|
||||
graficoConceptual: `
|
||||
**Gráfico Conceptual del Excedente del Productor**
|
||||
|
||||
El excedente del productor se representa como el área triangular entre el precio de mercado y la curva de costo marginal.
|
||||
|
||||
| Elemento | Descripción |
|
||||
|----------|-------------|
|
||||
| Eje vertical | Precio ($) |
|
||||
| Eje horizontal | Cantidad (Q) |
|
||||
| Línea P = 50 | Precio de mercado horizontal |
|
||||
| Curva CMg | Costo marginal creciente |
|
||||
| Área EP | Excedente del productor (entre P y CMg) |
|
||||
| Punto óptimo (Q=6) | Donde P = CMg |
|
||||
|
||||
**Descripción del gráfico:**
|
||||
- A precio P = $50, la empresa produce Q = 6 unidades
|
||||
- La curva CMg representa el costo de cada unidad adicional
|
||||
- El área sombreada entre P = $50 y la curva CMg representa el excedente del productor
|
||||
- Este excedente es la ganancia total sobre el costo variable de producción
|
||||
|
||||
Área sombreada = Excedente del Productor
|
||||
Área bajo CMg = Costo Variable`
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export default ejercicios;
|
||||
301
frontend/src/content/modulo4/mercado.ts
Normal file
301
frontend/src/content/modulo4/mercado.ts
Normal file
@@ -0,0 +1,301 @@
|
||||
export interface Seccion {
|
||||
titulo: string;
|
||||
contenido: string;
|
||||
}
|
||||
|
||||
export interface Ejercicio {
|
||||
id: string;
|
||||
tipo: 'slider' | 'quiz' | 'juego' | 'tabla' | 'calculadora';
|
||||
titulo: string;
|
||||
descripcion: string;
|
||||
config: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ModuloContenido {
|
||||
titulo: string;
|
||||
contenido: Seccion[];
|
||||
ejercicios: Ejercicio[];
|
||||
}
|
||||
|
||||
export const mercado: ModuloContenido = {
|
||||
titulo: 'Competencia Perfecta',
|
||||
contenido: [
|
||||
{
|
||||
titulo: 'Características de Competencia Perfecta',
|
||||
contenido: `La **competencia perfecta** es una estructura de mercado teórica con cinco características fundamentales:
|
||||
|
||||
**1. Muchos compradores y vendedores**
|
||||
- Ningún agente individual puede influir en el precio
|
||||
- El mercado determina el precio (precio-aceptante)
|
||||
|
||||
**2. Producto homogéneo**
|
||||
- Los bienes son perfectamente sustituibles
|
||||
- No hay diferenciación de marca o calidad
|
||||
- Ejemplo: trigo, acciones, divisas
|
||||
|
||||
**3. Información perfecta**
|
||||
- Todos conocen precios, costos y tecnologías
|
||||
- No hay ventajas informativas
|
||||
- Transparencia total
|
||||
|
||||
**4. Libre entrada y salida**
|
||||
- Sin barreras legales o económicas
|
||||
- Empresas entran si hay beneficios
|
||||
- Empresas salen si hay pérdidas
|
||||
|
||||
**5. Movilidad perfecta de factores**
|
||||
- Los recursos pueden reasignarse sin fricción
|
||||
- Trabajo y capital fluyen hacia los mejores usos
|
||||
|
||||
**Implicaciones:**
|
||||
- La demanda percibida por cada empresa es perfectamente elástica (horizontal)
|
||||
- Precio = Ingreso Medio = Ingreso Marginal
|
||||
- $$P = IM = IMg$$
|
||||
|
||||
**Ejemplo real aproximado:**
|
||||
Mercados agrícolas, mercados de valores, mercado de cambio de divisas.`
|
||||
},
|
||||
{
|
||||
titulo: 'Maximización de Beneficios',
|
||||
contenido: `El objetivo de la empresa es maximizar el **beneficio económico (π)**:
|
||||
|
||||
$$\\pi = IT - CT$$
|
||||
|
||||
Donde:
|
||||
- **IT** = Ingreso Total = $P \\times Q$
|
||||
- **CT** = Costo Total (CF + CV)
|
||||
|
||||
**Condición de primer orden:**
|
||||
Para maximizar, la empresa produce donde:
|
||||
$$\\frac{d\\pi}{dQ} = 0 \\Rightarrow IMg = CMg$$
|
||||
|
||||
**En competencia perfecta:**
|
||||
- $IMg = P$ (precio constante)
|
||||
- Por lo tanto: **$P = CMg$**
|
||||
|
||||
**Interpretación:**
|
||||
La empresa produce hasta donde el ingreso de la última unidad (precio) iguala su costo (CMg).
|
||||
|
||||
**Ejemplo numérico:**
|
||||
Precio de mercado: $P = $50
|
||||
|
||||
| Q | CT | CMg | IT | π |
|
||||
|---|---|----|-----|----|---|
|
||||
| 0 | 100 | - | 0 | -100 |
|
||||
| 1 | 140 | 40 | 50 | -90 |
|
||||
| 2 | 180 | 40 | 100 | -80 |
|
||||
| 3 | 220 | 40 | 150 | -70 |
|
||||
| 4 | 270 | 50 | 200 | -70 |
|
||||
| 5 | 330 | 60 | 250 | -80 |
|
||||
| 6 | 400 | 70 | 300 | -100 |
|
||||
|
||||
La cantidad óptima es **Q = 4** (o Q = 3, ambas dan π = -70, máximo menos negativo).
|
||||
|
||||
**Nota importante:** Maximizar beneficios no siempre significa beneficios positivos. Puede significar "minimizar pérdidas".`
|
||||
},
|
||||
{
|
||||
titulo: 'Regla IMg = CMg',
|
||||
contenido: `La regla fundamental de producción establece que la empresa maximiza beneficios cuando:
|
||||
|
||||
$$IMg = CMg$$
|
||||
|
||||
**Justificación matemática:**
|
||||
Si $IMg > CMg$:
|
||||
- Producir una unidad más genera más ingreso que costo
|
||||
- Convendría aumentar Q
|
||||
|
||||
Si $IMg < CMg$:
|
||||
- La última unidad cuesta más de lo que genera
|
||||
- Convendría disminuir Q
|
||||
|
||||
**En competencia perfecta:**
|
||||
$$P = CMg$$
|
||||
|
||||
**Condición de segundo orden:**
|
||||
Para asegurar que es un máximo (no un mínimo):
|
||||
$$\\frac{d^2\\pi}{dQ^2} < 0 \\Rightarrow \\text{pendiente CMg} > \\text{pendiente IMg}$$
|
||||
|
||||
**Ejemplo gráfico conceptual:**
|
||||
|
||||
El gráfico muestra la maximización de beneficios en competencia perfecta:
|
||||
|
||||
| Elemento | Descripción |
|
||||
|----------|-------------|
|
||||
| **Eje vertical** | Precio ($) y Costos |
|
||||
| **Eje horizontal** | Cantidad (Q) |
|
||||
| **Curva CMg** | Forma de U invertida (primero decrece, luego crece) |
|
||||
| **Curva CMe** | Forma de U invertida, por encima de CMg en su mínimo |
|
||||
| **Línea P = IMg** | Línea horizontal a $50 (perfectamente elástica) |
|
||||
| **Punto óptimo (Q*)** | Intersección de CMg con P = IMg |
|
||||
|
||||
La empresa produce en el punto donde la curva CMg ascendente corta al precio.`
|
||||
},
|
||||
{
|
||||
titulo: 'Punto de Cierre a Corto Plazo',
|
||||
contenido: `A corto plazo, la empresa debe decidir si produce o cierra temporalmente:
|
||||
|
||||
**Decisión de cierre:**
|
||||
La empresa cierra si:
|
||||
$$P < CVMe_{min}$$
|
||||
|
||||
O equivalentemente:
|
||||
$$IT < CV$$
|
||||
|
||||
**Razón:**
|
||||
- Si produce: Pierde CF + pérdida variable
|
||||
- Si cierra: Pierde solo CF
|
||||
- Mejor cerrar si no cubre al menos los costos variables
|
||||
|
||||
**Punto de cierre:**
|
||||
$$P = CVMe_{min}$$
|
||||
|
||||
A este precio, la empresa es indiferente entre producir o cerrar. Pérdida = CF en ambos casos.
|
||||
|
||||
**Ejemplo:**
|
||||
Si $CVMe_{min} = $30 y $CF = $100$:
|
||||
|
||||
| Precio | Decisión | Pérdida si produce | Pérdida si cierra |
|
||||
|--------|----------|-------------------|-------------------|
|
||||
| $50 | Producir | Menor que $100 | $100 |
|
||||
| $30 | Indiferente | $100 | $100 |
|
||||
| $20 | Cerrar | Mayor que $100 | $100 |
|
||||
|
||||
**Importante:**
|
||||
Cerrar ≠ Salir del mercado. A corto plazo, la empresa mantiene sus activos (CF) pero no opera. La salida es decisión a largo plazo.`
|
||||
},
|
||||
{
|
||||
titulo: 'Punto de Equilibrio a Largo Plazo',
|
||||
contenido: `A largo plazo, todas las empresas pueden entrar o salir del mercado:
|
||||
|
||||
**Condición de equilibrio:**
|
||||
En el largo plazo, las empresas entran si hay beneficios económicos positivos y salen si hay pérdidas.
|
||||
|
||||
**Equilibrio de largo plazo:**
|
||||
$$P = CMe_{min}$$
|
||||
|
||||
En este punto:
|
||||
- $P = CMg = CMe_{min}$
|
||||
- Beneficio económico = 0 (beneficio contable normal)
|
||||
- No hay incentivos para entrar ni salir
|
||||
|
||||
**Proceso de ajuste:**
|
||||
|
||||
1. **Si P > CMe** (beneficios):
|
||||
- Entran nuevas empresas
|
||||
- Aumenta oferta del mercado
|
||||
- Baja el precio
|
||||
- Hasta P = CMe
|
||||
|
||||
2. **Si P < CMe** (pérdidas):
|
||||
- Salen empresas
|
||||
- Disminuye oferta del mercado
|
||||
- Sube el precio
|
||||
- Hasta P = CMe
|
||||
|
||||
**Ejemplo:**
|
||||
|
||||
El gráfico del equilibrio de largo plazo muestra:
|
||||
|
||||
| Elemento | Descripción |
|
||||
|----------|-------------|
|
||||
| **Eje vertical** | Costos |
|
||||
| **Eje horizontal** | Cantidad (Q) |
|
||||
| **Curva CMe** | Forma de U invertida |
|
||||
| **Curva CMg** | U invertida que corta a CMe en su punto mínimo |
|
||||
| **Precio de equilibrio** | Línea horizontal a $40 que pasa por el mínimo de CMe |
|
||||
| **Cantidad de equilibrio** | Punto donde P = CMg = CMe (mínimo de CMe) |
|
||||
|
||||
**Proceso de ajuste hacia el equilibrio:**
|
||||
1. Si P > CMe: Entran empresas, aumenta la oferta, baja el precio
|
||||
2. Si P < CMe: Salen empresas, disminuye la oferta, sube el precio
|
||||
3. Equilibrio: P = CMe_minimo, beneficio económico = 0
|
||||
|
||||
**Nota:** Beneficio económico cero no significa que la empresa no gana nada. Significa que gana exactamente su costo de oportunidad (lo que podría ganar en su mejor alternativa).`
|
||||
},
|
||||
{
|
||||
titulo: 'Excedente del Productor',
|
||||
contenido: `El **excedente del productor** es la diferencia entre lo que un productor recibe y el costo mínimo al que estaría dispuesto a vender.
|
||||
|
||||
**Definición:**
|
||||
$$EP = IT - CV = P \\times Q - CV$$
|
||||
|
||||
O equivalentemente:
|
||||
$$EP = \\sum (P - CMg) \\text{ para todas las unidades producidas}$$
|
||||
|
||||
**Interpretación:**
|
||||
Representa el beneficio sobre los costos variables, o el "alquiler económico" que obtiene el productor.
|
||||
|
||||
**Relación con beneficios:**
|
||||
$$\\pi = EP - CF$$
|
||||
|
||||
**Gráfico conceptual:**
|
||||
|
||||
El excedente del productor se representa gráficamente como:
|
||||
|
||||
| Elemento | Descripción |
|
||||
|----------|-------------|
|
||||
| **Eje vertical** | Precio |
|
||||
| **Eje horizontal** | Cantidad (Q) |
|
||||
| **Curva CMg** | Curva ascendente (costo marginal creciente) |
|
||||
| **Precio de mercado (P*)** | Línea horizontal |
|
||||
| **Excedente del productor (EP)** | Área entre P* y la curva CMg, desde 0 hasta Q* |
|
||||
| **Cantidad óptima (Q*)** | Punto donde P* = CMg |
|
||||
|
||||
**Cálculo del excedente:**
|
||||
El excedente es el área entre el precio de mercado y la curva de costo marginal, desde cero hasta la cantidad producida. Representa la ganancia sobre el costo variable mínimo necesario para producir cada unidad.
|
||||
|
||||
**Ejemplo numérico:**
|
||||
P = $50, Q = 10 unidades
|
||||
|
||||
| Unidad | CMg | Excedente unitario |
|
||||
|--------|-----|-------------------|
|
||||
| 1 | $10 | $40 |
|
||||
| 2 | $15 | $35 |
|
||||
| 3 | $20 | $30 |
|
||||
| 4 | $25 | $25 |
|
||||
| 5 | $30 | $20 |
|
||||
| 6 | $35 | $15 |
|
||||
| 7 | $40 | $10 |
|
||||
| 8 | $45 | $5 |
|
||||
| 9 | $50 | $0 |
|
||||
| 10 | $55 | -$5 (no produce) |
|
||||
|
||||
EP total = $180 (suma de excedentes de unidades 1-9)`
|
||||
}
|
||||
],
|
||||
ejercicios: [
|
||||
{
|
||||
id: 'competencia-decision',
|
||||
tipo: 'calculadora',
|
||||
titulo: 'Simulador de Decisión de Producción',
|
||||
descripcion: 'Dado un precio de mercado y curva de costos, encuentra la cantidad óptima y determina si debes producir o cerrar',
|
||||
config: {
|
||||
inputs: ['precioMercado', 'CF', 'funcionCosto'],
|
||||
outputs: ['QOptima', 'IT', 'CT', 'Beneficio', 'Decision'],
|
||||
criterios: [
|
||||
'Si P >= CMe: Beneficios positivos',
|
||||
'Si CVMe < P < CMe: Producir con pérdidas (menor que CF)',
|
||||
'Si P < CVMe: Cerrar temporalmente'
|
||||
],
|
||||
mostrarGrafico: true,
|
||||
destacarZona: true
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'excedente-visualizacion',
|
||||
tipo: 'juego',
|
||||
titulo: 'Visualización de Excedentes',
|
||||
descripcion: 'Interactúa con el gráfico para ver cómo cambia el excedente del productor al variar el precio y la cantidad',
|
||||
config: {
|
||||
tipoGrafico: 'area',
|
||||
mostrarAreas: ['excedenteProductor', 'costoVariable', 'beneficio'],
|
||||
interactivo: true,
|
||||
sliders: ['precio', 'cantidad'],
|
||||
calcularAutomatico: true,
|
||||
mostrarTabla: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export default mercado;
|
||||
157
frontend/src/content/modulo4/produccion.ts
Normal file
157
frontend/src/content/modulo4/produccion.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
export interface Seccion {
|
||||
titulo: string;
|
||||
contenido: string;
|
||||
}
|
||||
|
||||
export interface Ejercicio {
|
||||
id: string;
|
||||
tipo: 'slider' | 'quiz' | 'juego' | 'tabla' | 'calculadora';
|
||||
titulo: string;
|
||||
descripcion: string;
|
||||
config: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ModuloContenido {
|
||||
titulo: string;
|
||||
contenido: Seccion[];
|
||||
ejercicios: Ejercicio[];
|
||||
}
|
||||
|
||||
export const produccion: ModuloContenido = {
|
||||
titulo: 'Producción',
|
||||
contenido: [
|
||||
{
|
||||
titulo: 'Función de Producción',
|
||||
contenido: `La **función de producción** describe la relación técnica entre los factores de producción utilizados y la cantidad máxima de producto obtenida.
|
||||
|
||||
**Fórmula general:**
|
||||
$$Q = f(K, L)$$
|
||||
|
||||
Donde:
|
||||
- **Q** = Cantidad producida (output)
|
||||
- **K** = Capital (maquinaria, equipos, instalaciones)
|
||||
- **L** = Trabajo (horas-hombre, número de trabajadores)
|
||||
- **f** = Función de producción (tecnología)
|
||||
|
||||
**Ejemplo:** Una fábrica de pan utiliza hornos (K) y panaderos (L) para producir pan (Q).
|
||||
|
||||
**Formas comunes:**
|
||||
- **Lineal**: $Q = aK + bL$ (sustitutos perfectos)
|
||||
- **Cobb-Douglas**: $Q = A \cdot K^\alpha \cdot L^\beta$ (sustituibles)
|
||||
- **Leontief**: $Q = \min(aK, bL)$ (complementarios perfectos)`
|
||||
},
|
||||
{
|
||||
titulo: 'Producto Total, Marginal y Medio',
|
||||
contenido: `El análisis de producción distingue tres conceptos fundamentales:
|
||||
|
||||
**Producto Total (PT)**
|
||||
Cantidad total producida con una cantidad dada de factores.
|
||||
$$PT = f(L)$$ (manteniendo K constante)
|
||||
|
||||
**Producto Marginal (PMg)**
|
||||
Incremento en el producto total al aumentar en una unidad el factor variable.
|
||||
$$PMg_L = \frac{\Delta PT}{\Delta L}$$
|
||||
|
||||
**Producto Medio (PMe)**
|
||||
Producto por unidad de factor.
|
||||
$$PMe_L = \frac{PT}{L}$$
|
||||
|
||||
**Ejemplo numérico:**
|
||||
| L (trabajadores) | PT (panes) | PMg | PMe |
|
||||
|------------------|------------|-----|-----|
|
||||
| 0 | 0 | - | - |
|
||||
| 1 | 10 | 10 | 10.0 |
|
||||
| 2 | 24 | 14 | 12.0 |
|
||||
| 3 | 36 | 12 | 12.0 |
|
||||
| 4 | 44 | 8 | 11.0 |
|
||||
| 5 | 48 | 4 | 9.6 |
|
||||
| 6 | 48 | 0 | 8.0 |
|
||||
| 7 | 42 | -6 | 6.0 |
|
||||
|
||||
Observa que el PMg máximo (14) ocurre antes que el PMe máximo (12.0), y ambos antes del PT máximo (48).`
|
||||
},
|
||||
{
|
||||
titulo: 'Ley de Rendimientos Decrecientes',
|
||||
contenido: `La **ley de rendimientos decrecientes** (o ley de productividad marginal decreciente) establece que:
|
||||
|
||||
> *"Al mantener constantes todos los demás factores, si se va aumentando la cantidad de un factor variable, llega un punto a partir del cual los incrementos de producto son cada vez menores."*
|
||||
|
||||
**Condiciones:**
|
||||
- Tecnología constante
|
||||
- Al menos un factor fijo
|
||||
- Factores variables homogéneos
|
||||
|
||||
**Interpretación:**
|
||||
Inicialmente, al añadir trabajadores a una fábrica fija, el PMg aumenta (especialización). Pero una vez alcanzado el óptimo, cada trabajador adicional tiene menos capital y espacio, reduciendo su contribución marginal.
|
||||
|
||||
**Ejemplo gráfico conceptual:**
|
||||
El gráfico muestra la relación entre PMg y PMe:
|
||||
- PMg alcanza su máximo primero
|
||||
- Luego PMe alcanza su máximo (cuando PMg = PMe)
|
||||
- Finalmente PT alcanza su máximo (cuando PMg = 0)
|
||||
- Después PMg se vuelve negativo (Etapa III)
|
||||
|
||||
**Importancia económica:**
|
||||
Esta ley explica por qué las empresas no crecen indefinidamente y por qué existen costos crecientes a largo plazo.`
|
||||
},
|
||||
{
|
||||
titulo: 'Etapas de Producción',
|
||||
contenido: `El análisis del producto marginal y medio permite dividir la producción en tres etapas:
|
||||
|
||||
**Etapa I: Crecientes**
|
||||
- PMg > PMe (ambos creciendo inicialmente)
|
||||
- PMe está aumentando
|
||||
- La empresa no opera aquí: está desperdiciando capacidad fija
|
||||
- Fin: Cuando PMg = PMe (PMe máximo)
|
||||
|
||||
**Etapa II: Decrecientes**
|
||||
- 0 < PMg < PMe (ambos decrecientes)
|
||||
- PMe decreciente pero positivo
|
||||
- PMg positivo pero decreciente
|
||||
- **Zona racional de producción**
|
||||
- Fin: Cuando PMg = 0 (PT máximo)
|
||||
|
||||
**Etapa III: Negativos**
|
||||
- PMg < 0
|
||||
- PT decreciente
|
||||
- La empresa nunca opera aquí: tiene "demasiado" factor variable
|
||||
- Agregar más trabajo reduce la producción total
|
||||
|
||||
**Resumen de etapas:**
|
||||
|
||||
| Etapa | Características | Decisión |
|
||||
|-------|----------------|----------|
|
||||
| **I** | PMg > PMe, PMe creciente | No operar - desperdicio de capacidad |
|
||||
| **II** | 0 < PMg < PMe, ambos decrecientes | **Operar aquí** - zona racional |
|
||||
| **III** | PMg < 0, PT decreciente | No operar - demasiado factor variable |
|
||||
|
||||
**Decisión del productor:**
|
||||
La empresa racional operará en la **Etapa II**, donde PMg es positivo pero decreciente. La ubicación exacta depende de los precios de los factores y del producto.`
|
||||
}
|
||||
],
|
||||
ejercicios: [
|
||||
{
|
||||
id: 'produccion-tabla',
|
||||
tipo: 'tabla',
|
||||
titulo: 'Análisis de Productividad',
|
||||
descripcion: 'Completa la tabla de producción calculando PMg y PMe, identificando las tres etapas',
|
||||
config: {
|
||||
columnas: ['L', 'PT', 'PMg', 'PMe', 'Etapa'],
|
||||
datosIniciales: [
|
||||
{ L: 0, PT: 0, PMg: null, PMe: null, Etapa: '-' },
|
||||
{ L: 1, PT: 8, PMg: null, PMe: null, Etapa: '?' },
|
||||
{ L: 2, PT: 20, PMg: null, PMe: null, Etapa: '?' },
|
||||
{ L: 3, PT: 36, PMg: null, PMe: null, Etapa: '?' },
|
||||
{ L: 4, PT: 48, PMg: null, PMe: null, Etapa: '?' },
|
||||
{ L: 5, PT: 55, PMg: null, PMe: null, Etapa: '?' },
|
||||
{ L: 6, PT: 60, PMg: null, PMe: null, Etapa: '?' },
|
||||
{ L: 7, PT: 56, PMg: null, PMe: null, Etapa: '?' }
|
||||
],
|
||||
mostrarGrafico: true,
|
||||
identificarEtapas: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export default produccion;
|
||||
85
frontend/src/hooks/useEjercicioProgreso.ts
Normal file
85
frontend/src/hooks/useEjercicioProgreso.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { useProgressStore } from '../stores/progressStore';
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
|
||||
interface UseEjercicioProgresoOptions {
|
||||
moduloId: string;
|
||||
ejercicioId: string;
|
||||
onComplete?: (puntuacion?: number) => void;
|
||||
}
|
||||
|
||||
interface UseEjercicioProgresoReturn {
|
||||
guardarProgreso: (puntuacion: number) => Promise<void>;
|
||||
progresoGuardado: boolean;
|
||||
puntuacionAnterior: number | undefined;
|
||||
intentos: number;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export function useEjercicioProgreso({
|
||||
moduloId,
|
||||
ejercicioId,
|
||||
onComplete,
|
||||
}: UseEjercicioProgresoOptions): UseEjercicioProgresoReturn {
|
||||
const { saveProgreso, getProgresoEjercicio, modulos } = useProgressStore();
|
||||
const [progresoGuardado, setProgresoGuardado] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [puntuacionAnterior, setPuntuacionAnterior] = useState<number | undefined>(undefined);
|
||||
const [intentos, setIntentos] = useState(0);
|
||||
|
||||
// Cargar progreso existente de manera reactiva
|
||||
useEffect(() => {
|
||||
const progreso = getProgresoEjercicio(moduloId, ejercicioId);
|
||||
if (progreso) {
|
||||
setPuntuacionAnterior(progreso.puntuacion);
|
||||
setIntentos(progreso.intentos);
|
||||
} else {
|
||||
setPuntuacionAnterior(undefined);
|
||||
setIntentos(0);
|
||||
}
|
||||
}, [moduloId, ejercicioId, getProgresoEjercicio, modulos]);
|
||||
|
||||
const guardarProgreso = useCallback(async (puntuacion: number) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Guardar en el store (que ahora usa la API)
|
||||
await saveProgreso(moduloId, ejercicioId, puntuacion);
|
||||
|
||||
setProgresoGuardado(true);
|
||||
|
||||
// Actualizar estado local
|
||||
const progresoAnterior = getProgresoEjercicio(moduloId, ejercicioId);
|
||||
if (progresoAnterior) {
|
||||
setPuntuacionAnterior(progresoAnterior.puntuacion);
|
||||
setIntentos(progresoAnterior.intentos);
|
||||
} else {
|
||||
setPuntuacionAnterior(puntuacion);
|
||||
setIntentos(1);
|
||||
}
|
||||
|
||||
// Llamar callback si existe
|
||||
if (onComplete) {
|
||||
onComplete(puntuacion);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Error al guardar el progreso');
|
||||
console.error('Error saving progress:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [moduloId, ejercicioId, saveProgreso, onComplete, getProgresoEjercicio]);
|
||||
|
||||
return {
|
||||
guardarProgreso,
|
||||
progresoGuardado,
|
||||
puntuacionAnterior,
|
||||
intentos,
|
||||
isLoading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export default useEjercicioProgreso;
|
||||
@@ -1,82 +1,109 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import { useProgressStore } from '../stores/progressStore';
|
||||
import { Card } from '../components/ui/Card';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { progresoService } from '../services/api';
|
||||
import type { ModuloProgreso } from '../types';
|
||||
import { BookOpen, TrendingUp, User, LogOut, LayoutGrid } from 'lucide-react';
|
||||
import { ProgressBar } from '../components/progress/ProgressBar';
|
||||
import { ScoreDisplay } from '../components/progress/ScoreDisplay';
|
||||
import { BadgesSection } from '../components/progress/Badges';
|
||||
import { Loader } from '../components/ui/Loader';
|
||||
import { BookOpen, User, LogOut, LayoutGrid, Award, Star, Target, CheckCircle, FileText } from 'lucide-react';
|
||||
|
||||
const MODULOS_DEFAULT = [
|
||||
{ numero: 1, titulo: 'Fundamentos de Economía', descripcion: 'Introducción a los conceptos básicos' },
|
||||
{ numero: 2, titulo: 'Oferta, Demanda y Equilibrio', descripcion: 'Curvas de mercado' },
|
||||
{ numero: 3, titulo: 'Utilidad y Elasticidad', descripcion: 'Teoría del consumidor' },
|
||||
{ numero: 4, titulo: 'Teoría del Productor', descripcion: 'Costos y producción' },
|
||||
const MODULOS_CONFIG = [
|
||||
{ id: 'modulo1', numero: 1, titulo: 'Fundamentos de Economía', descripcion: 'Introducción a los conceptos básicos', totalEjercicios: 3 },
|
||||
{ id: 'modulo2', numero: 2, titulo: 'Oferta, Demanda y Equilibrio', descripcion: 'Curvas de mercado', totalEjercicios: 3 },
|
||||
{ id: 'modulo3', numero: 3, titulo: 'Utilidad y Elasticidad', descripcion: 'Teoría del consumidor', totalEjercicios: 3 },
|
||||
{ id: 'modulo4', numero: 4, titulo: 'Teoría del Productor', descripcion: 'Costos y producción', totalEjercicios: 3 },
|
||||
];
|
||||
|
||||
export function Dashboard() {
|
||||
const { usuario, logout } = useAuthStore();
|
||||
const [modulosProgreso, setModulosProgreso] = useState<ModuloProgreso[]>([]);
|
||||
const {
|
||||
puntuacionTotal,
|
||||
nivel,
|
||||
calcularPorcentajeModulo,
|
||||
getBadgesDesbloqueados,
|
||||
getBadgesBloqueados,
|
||||
modulos,
|
||||
loadProgreso,
|
||||
isLoading,
|
||||
error,
|
||||
} = useProgressStore();
|
||||
|
||||
useEffect(() => {
|
||||
loadProgreso();
|
||||
}, []);
|
||||
|
||||
const loadProgreso = async () => {
|
||||
try {
|
||||
const progresos = await progresoService.getProgreso();
|
||||
const modulos = MODULOS_DEFAULT.map((mod) => {
|
||||
const modProgresos = progresos.filter((p) => p.modulo_numero === mod.numero);
|
||||
const completados = modProgresos.filter((p) => p.completado).length;
|
||||
const total = 5; // Asumiendo 5 ejercicios por módulo
|
||||
return {
|
||||
numero: mod.numero,
|
||||
titulo: mod.titulo,
|
||||
porcentaje: Math.round((completados / total) * 100),
|
||||
ejerciciosCompletados: completados,
|
||||
totalEjercicios: total,
|
||||
};
|
||||
});
|
||||
setModulosProgreso(modulos);
|
||||
} catch {
|
||||
// Si hay error, mostrar progreso vacío
|
||||
setModulosProgreso(
|
||||
MODULOS_DEFAULT.map((mod) => ({
|
||||
numero: mod.numero,
|
||||
titulo: mod.titulo,
|
||||
porcentaje: 0,
|
||||
ejerciciosCompletados: 0,
|
||||
totalEjercicios: 5,
|
||||
}))
|
||||
);
|
||||
}
|
||||
};
|
||||
}, [loadProgreso]);
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
};
|
||||
|
||||
if (isLoading && Object.keys(modulos).length === 0) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Loader size="lg" className="mx-auto mb-4" />
|
||||
<p className="text-gray-600">Cargando tu progreso...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center max-w-md mx-auto px-4">
|
||||
<div className="text-red-500 mb-4">
|
||||
<svg className="w-16 h-16 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-2">Error al cargar el progreso</h2>
|
||||
<p className="text-gray-600 mb-4">{error}</p>
|
||||
<Button onClick={loadProgreso}>Reintentar</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Calcular progreso total
|
||||
const totalProgreso = Math.round(
|
||||
modulosProgreso.reduce((acc, mod) => acc + mod.porcentaje, 0) / modulosProgreso.length
|
||||
MODULOS_CONFIG.reduce((acc, mod) => {
|
||||
return acc + calcularPorcentajeModulo(mod.id, mod.totalEjercicios);
|
||||
}, 0) / MODULOS_CONFIG.length
|
||||
);
|
||||
|
||||
const badgesDesbloqueados = getBadgesDesbloqueados();
|
||||
const badgesBloqueados = getBadgesBloqueados();
|
||||
|
||||
// Calcular ejercicios completados por módulo
|
||||
const getEjerciciosCompletados = (moduloId: string) => {
|
||||
const modulo = modulos[moduloId];
|
||||
if (!modulo) return 0;
|
||||
return Object.values(modulo.ejercicios).filter(ej => ej.completado).length;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<header className="bg-white shadow-sm">
|
||||
<header className="bg-white shadow-sm border-b border-gray-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-primary rounded-lg flex items-center justify-center">
|
||||
<div className="w-10 h-10 bg-blue-600 rounded-lg flex items-center justify-center">
|
||||
<BookOpen className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<h1 className="text-xl font-bold text-gray-900">Economía</h1>
|
||||
<h1 className="text-xl font-bold text-gray-900">Economía Interactiva</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2 text-gray-600">
|
||||
<User className="w-5 h-5" />
|
||||
<span className="font-medium">{usuario?.nombre}</span>
|
||||
<span className="font-medium">{usuario?.nombre || 'Usuario'}</span>
|
||||
<span className="px-2 py-0.5 bg-blue-100 text-blue-700 text-xs rounded-full font-semibold">
|
||||
{nivel}
|
||||
</span>
|
||||
{usuario?.rol === 'admin' && (
|
||||
<span className="px-2 py-0.5 bg-purple-100 text-purple-700 text-xs rounded-full">
|
||||
<span className="px-2 py-0.5 bg-purple-100 text-purple-700 text-xs rounded-full font-semibold">
|
||||
Admin
|
||||
</span>
|
||||
)}
|
||||
@@ -91,86 +118,154 @@ export function Dashboard() {
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900">Tu progreso</h2>
|
||||
<p className="text-gray-600">Continúa donde lo dejaste</p>
|
||||
<p className="text-gray-600">Continúa donde lo dejaste y desbloquea nuevos logros</p>
|
||||
</div>
|
||||
|
||||
<Card className="mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900">Progreso total</h3>
|
||||
<p className="text-sm text-gray-500">{totalProgreso}% completado</p>
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<Card className="bg-gradient-to-br from-blue-500 to-blue-600 text-white border-none">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-blue-100 text-sm">Progreso total</p>
|
||||
<p className="text-3xl font-bold mt-1">{totalProgreso}%</p>
|
||||
</div>
|
||||
<Target className="w-12 h-12 text-blue-100 opacity-80" />
|
||||
</div>
|
||||
<div className="mt-4 w-full bg-white/20 rounded-full h-2">
|
||||
<div
|
||||
className="bg-white h-2 rounded-full transition-all duration-500"
|
||||
style={{ width: `${totalProgreso}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-blue-100">
|
||||
{totalProgreso === 100 ? '¡Has completado todos los módulos!' : 'Sigue así, vas por buen camino'}
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-br from-amber-500 to-orange-500 text-white border-none">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-orange-100 text-sm">Puntuación total</p>
|
||||
<p className="text-3xl font-bold mt-1">{puntuacionTotal.toLocaleString()}</p>
|
||||
</div>
|
||||
<Star className="w-12 h-12 text-orange-100 opacity-80" />
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-orange-100">
|
||||
Acumula puntos completando ejercicios para subir de nivel
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-br from-purple-500 to-pink-500 text-white border-none">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-purple-100 text-sm">Logros</p>
|
||||
<p className="text-3xl font-bold mt-1">
|
||||
{badgesDesbloqueados.length}/{badgesDesbloqueados.length + badgesBloqueados.length}
|
||||
</p>
|
||||
</div>
|
||||
<Award className="w-12 h-12 text-purple-100 opacity-80" />
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-purple-100">
|
||||
{badgesBloqueados.length === 0
|
||||
? '¡Todos los logros desbloqueados!'
|
||||
: `${badgesBloqueados.length} logros por desbloquear`}
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Columna izquierda - Módulos */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Puntuación y Nivel */}
|
||||
<ScoreDisplay
|
||||
puntos={puntuacionTotal}
|
||||
animar={false}
|
||||
showNivel={true}
|
||||
size="md"
|
||||
/>
|
||||
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold text-gray-900">Módulos</h2>
|
||||
{usuario?.rol === 'admin' && (
|
||||
<Link to="/admin">
|
||||
<Button variant="outline" size="sm">
|
||||
Panel de Admin
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{MODULOS_CONFIG.map((modulo) => {
|
||||
const porcentaje = calcularPorcentajeModulo(modulo.id, modulo.totalEjercicios);
|
||||
const completados = getEjerciciosCompletados(modulo.id);
|
||||
|
||||
return (
|
||||
<Link key={modulo.id} to={`/modulo/${modulo.numero}`}>
|
||||
<Card className="hover:shadow-lg transition-shadow cursor-pointer h-full">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<span className="text-blue-600 font-bold text-lg">{modulo.numero}</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900">{modulo.titulo}</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
{completados}/{modulo.totalEjercicios} ejercicios
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProgressBar
|
||||
porcentaje={porcentaje}
|
||||
moduloNumero={modulo.numero}
|
||||
showLabel={false}
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between text-sm mt-3">
|
||||
<span className="text-gray-500">{porcentaje}% completado</span>
|
||||
{porcentaje === 100 && (
|
||||
<span className="text-green-600 flex items-center gap-1 font-medium">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
Completado
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex justify-center gap-4">
|
||||
<Link to="/modulos">
|
||||
<Button variant="outline" size="lg">
|
||||
<LayoutGrid className="w-5 h-5 mr-2" />
|
||||
Ver todos los módulos
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/recursos">
|
||||
<Button variant="outline" size="lg">
|
||||
<FileText className="w-5 h-5 mr-2" />
|
||||
Material PDF
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-primary">{totalProgreso}%</div>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-3">
|
||||
<div
|
||||
className="bg-primary h-3 rounded-full transition-all duration-500"
|
||||
style={{ width: `${totalProgreso}%` }}
|
||||
|
||||
{/* Columna derecha - Logros */}
|
||||
<div>
|
||||
<BadgesSection
|
||||
badgesDesbloqueados={badgesDesbloqueados}
|
||||
badgesBloqueados={badgesBloqueados}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold text-gray-900">Módulos</h2>
|
||||
{usuario?.rol === 'admin' && (
|
||||
<Link to="/admin">
|
||||
<Button variant="outline" size="sm">
|
||||
Panel de Admin
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{modulosProgreso.map((modulo) => (
|
||||
<Link key={modulo.numero} to={`/modulo/${modulo.numero}`}>
|
||||
<Card className="hover:shadow-lg transition-shadow cursor-pointer">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<span className="text-primary font-bold">{modulo.numero}</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900">{modulo.titulo}</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
{modulo.ejerciciosCompletados}/{modulo.totalEjercicios} ejercicios
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-gray-200 rounded-full h-2 mb-2">
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all ${
|
||||
modulo.porcentaje === 100 ? 'bg-success' : 'bg-primary'
|
||||
}`}
|
||||
style={{ width: `${modulo.porcentaje}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-500">{modulo.porcentaje}% completado</span>
|
||||
{modulo.porcentaje === 100 && (
|
||||
<span className="text-success flex items-center gap-1">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
Completado
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 text-center">
|
||||
<Link to="/modulos">
|
||||
<Button variant="outline" size="lg">
|
||||
<LayoutGrid className="w-5 h-5 mr-2" />
|
||||
Ver todos los módulos
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Dashboard;
|
||||
|
||||
@@ -1,63 +1,213 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Card } from '../components/ui/Card';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { progresoService } from '../services/api';
|
||||
import type { Progreso } from '../types';
|
||||
import { ArrowLeft, CheckCircle, Play } from 'lucide-react';
|
||||
import { Loader } from '../components/ui/Loader';
|
||||
import { useProgressStore } from '../stores/progressStore';
|
||||
import { ScoreDisplay } from '../components/progress/ScoreDisplay';
|
||||
import {
|
||||
ArrowLeft,
|
||||
CheckCircle,
|
||||
Play,
|
||||
Lock,
|
||||
Trophy,
|
||||
TrendingUp,
|
||||
RotateCcw
|
||||
} from 'lucide-react';
|
||||
import type { EjercicioProgreso } from '../stores/progressStore';
|
||||
|
||||
const MODULOS_INFO: Record<number, { titulo: string; descripcion: string }> = {
|
||||
1: { titulo: 'Fundamentos de Economía', descripcion: 'Introducción a los conceptos básicos de economía' },
|
||||
2: { titulo: 'Oferta, Demanda y Equilibrio', descripcion: 'Curvas de oferta y demanda en el mercado' },
|
||||
3: { titulo: 'Utilidad y Elasticidad', descripcion: 'Teoría del consumidor y elasticidades' },
|
||||
4: { titulo: 'Teoría del Productor', descripcion: 'Costos de producción y competencia perfecta' },
|
||||
// Importar ejercicios reales
|
||||
import { FlujoCircular } from '../components/exercises/modulo1/FlujoCircular';
|
||||
import { QuizBienes } from '../components/exercises/modulo1/QuizBienes';
|
||||
import { SimuladorDisyuntivas } from '../components/exercises/modulo1/SimuladorDisyuntivas';
|
||||
import { ConstructorCurvas } from '../components/exercises/modulo2/ConstructorCurvas';
|
||||
import { IdentificarShocks } from '../components/exercises/modulo2/IdentificarShocks';
|
||||
import { SimuladorPrecios } from '../components/exercises/modulo2/SimuladorPrecios';
|
||||
import { ClasificadorBienes } from '../components/exercises/modulo3/ClasificadorBienes';
|
||||
import { CalculadoraElasticidad } from '../components/exercises/modulo3/CalculadoraElasticidad';
|
||||
import { EjerciciosExamen } from '../components/exercises/modulo3/EjerciciosExamen';
|
||||
import { CalculadoraCostos } from '../components/exercises/modulo4/CalculadoraCostos';
|
||||
import { SimuladorProduccion } from '../components/exercises/modulo4/SimuladorProduccion';
|
||||
import { VisualizadorExcedentes } from '../components/exercises/modulo4/VisualizadorExcedentes';
|
||||
|
||||
const MODULOS_INFO: Record<number, {
|
||||
id: string;
|
||||
titulo: string;
|
||||
descripcion: string;
|
||||
color: string;
|
||||
}> = {
|
||||
1: {
|
||||
id: 'modulo1',
|
||||
titulo: 'Fundamentos de Economía',
|
||||
descripcion: 'Introducción a los conceptos básicos de economía',
|
||||
color: 'from-blue-500 to-blue-600'
|
||||
},
|
||||
2: {
|
||||
id: 'modulo2',
|
||||
titulo: 'Oferta, Demanda y Equilibrio',
|
||||
descripcion: 'Curvas de oferta y demanda en el mercado',
|
||||
color: 'from-green-500 to-green-600'
|
||||
},
|
||||
3: {
|
||||
id: 'modulo3',
|
||||
titulo: 'Utilidad y Elasticidad',
|
||||
descripcion: 'Teoría del consumidor y elasticidades',
|
||||
color: 'from-purple-500 to-purple-600'
|
||||
},
|
||||
4: {
|
||||
id: 'modulo4',
|
||||
titulo: 'Teoría del Productor',
|
||||
descripcion: 'Costos de producción y competencia perfecta',
|
||||
color: 'from-orange-500 to-orange-600'
|
||||
},
|
||||
};
|
||||
|
||||
const EJERCICIOS_MOCK = [
|
||||
{ id: 'e1', titulo: 'Conceptos básicos', descripcion: 'Repasa los fundamentos de la economía' },
|
||||
{ id: 'e2', titulo: 'Agentes económicos', descripcion: 'Identifica los diferentes agentes en la economía' },
|
||||
{ id: 'e3', titulo: 'Factores de producción', descripcion: 'Aprende sobre tierra, trabajo y capital' },
|
||||
{ id: 'e4', titulo: 'Flujo circular', descripcion: 'Comprende el flujo de bienes y dinero' },
|
||||
{ id: 'e5', titulo: 'Evaluación final', descripcion: 'Pon a prueba todo lo aprendido' },
|
||||
];
|
||||
const EJERCICIOS_POR_MODULO: Record<number, Array<{
|
||||
id: string;
|
||||
titulo: string;
|
||||
descripcion: string;
|
||||
componente: React.ComponentType<{ ejercicioId: string; onComplete?: (puntuacion: number) => void }>;
|
||||
}>> = {
|
||||
1: [
|
||||
{ id: 'simulador-disyuntivas', titulo: 'Simulador de Disyuntivas', descripcion: 'Explora las decisiones económicas fundamentales', componente: SimuladorDisyuntivas },
|
||||
{ id: 'quiz-bienes', titulo: 'Quiz de Bienes', descripcion: 'Identifica diferentes tipos de bienes', componente: QuizBienes },
|
||||
{ id: 'flujo-circular', titulo: 'Flujo Circular', descripcion: 'Comprende el flujo de bienes y dinero en la economía', componente: FlujoCircular },
|
||||
],
|
||||
2: [
|
||||
{ id: 'constructor-curvas', titulo: 'Constructor de Curvas', descripcion: 'Construye curvas de oferta y demanda', componente: ConstructorCurvas },
|
||||
{ id: 'identificar-shocks', titulo: 'Identificar Shocks', descripcion: 'Reconoce cambios en el mercado', componente: IdentificarShocks },
|
||||
{ id: 'simulador-precios', titulo: 'Simulador de Precios', descripcion: 'Simula el equilibrio de precios', componente: SimuladorPrecios },
|
||||
],
|
||||
3: [
|
||||
{ id: 'clasificador-bienes', titulo: 'Clasificador de Bienes', descripcion: 'Clasifica bienes según su elasticidad', componente: ClasificadorBienes },
|
||||
{ id: 'calculadora-elasticidad', titulo: 'Calculadora de Elasticidad', descripcion: 'Calcula elasticidades de demanda', componente: CalculadoraElasticidad },
|
||||
{ id: 'ejercicios-examen', titulo: 'Ejercicios de Examen', descripcion: 'Pon a prueba tus conocimientos', componente: EjerciciosExamen },
|
||||
],
|
||||
4: [
|
||||
{ id: 'calculadora-costos', titulo: 'Calculadora de Costos', descripcion: 'Calcula costos de producción', componente: CalculadoraCostos },
|
||||
{ id: 'simulador-produccion', titulo: 'Simulador de Producción', descripcion: 'Simula la producción óptima', componente: SimuladorProduccion },
|
||||
{ id: 'visualizador-excedentes', titulo: 'Visualizador de Excedentes', descripcion: 'Visualiza excedentes del consumidor y productor', componente: VisualizadorExcedentes },
|
||||
],
|
||||
};
|
||||
|
||||
export function Modulo() {
|
||||
const { numero } = useParams<{ numero: string }>();
|
||||
const num = parseInt(numero || '1', 10);
|
||||
const [progresos, setProgresos] = useState<Progreso[]>([]);
|
||||
|
||||
const {
|
||||
puntuacionTotal,
|
||||
getProgresoEjercicio,
|
||||
saveProgreso,
|
||||
calcularPorcentajeModulo,
|
||||
loadProgreso,
|
||||
isLoading,
|
||||
error,
|
||||
} = useProgressStore();
|
||||
|
||||
const moduloInfo = MODULOS_INFO[num] || MODULOS_INFO[1];
|
||||
const ejercicios = EJERCICIOS_MOCK;
|
||||
const [ejercicioActivo, setEjercicioActivo] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadProgreso();
|
||||
}, [num]);
|
||||
}, [loadProgreso]);
|
||||
|
||||
const loadProgreso = async () => {
|
||||
const moduloInfo = MODULOS_INFO[num] || MODULOS_INFO[1];
|
||||
const ejercicios = EJERCICIOS_POR_MODULO[num] || [];
|
||||
const porcentaje = calcularPorcentajeModulo(moduloInfo.id, ejercicios.length);
|
||||
|
||||
const getProgresoEjercicioLocal = (ejercicioId: string): EjercicioProgreso | undefined => {
|
||||
return getProgresoEjercicio(moduloInfo.id, ejercicioId);
|
||||
};
|
||||
|
||||
const handleCompleteEjercicio = async (ejercicioId: string, puntuacion: number) => {
|
||||
try {
|
||||
const data = await progresoService.getProgreso();
|
||||
setProgresos(data);
|
||||
} catch {
|
||||
// Silencio
|
||||
await saveProgreso(moduloInfo.id, ejercicioId, puntuacion);
|
||||
setEjercicioActivo(null);
|
||||
} catch (err) {
|
||||
console.error('Error al guardar progreso:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const getProgresoForEjercicio = (ejercicioId: string) => {
|
||||
return progresos.find(
|
||||
(p) => p.modulo_numero === num && p.ejercicio_id === ejercicioId
|
||||
);
|
||||
const completados = ejercicios.filter(
|
||||
(e) => getProgresoEjercicioLocal(e.id)?.completado
|
||||
).length;
|
||||
|
||||
// Determinar si un ejercicio está bloqueado (el primero siempre desbloqueado)
|
||||
const isEjercicioBloqueado = (index: number): boolean => {
|
||||
if (index === 0) return false;
|
||||
// Ejercicio anterior completado?
|
||||
const ejercicioAnterior = ejercicios[index - 1];
|
||||
return !getProgresoEjercicioLocal(ejercicioAnterior.id)?.completado;
|
||||
};
|
||||
|
||||
const completados = ejercicios.filter(
|
||||
(e) => getProgresoForEjercicio(e.id)?.completado
|
||||
).length;
|
||||
const porcentaje = Math.round((completados / ejercicios.length) * 100);
|
||||
if (ejercicioActivo) {
|
||||
const ejercicio = ejercicios.find(e => e.id === ejercicioActivo);
|
||||
if (!ejercicio) return null;
|
||||
|
||||
const EjercicioComponent = ejercicio.componente;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<header className="bg-white shadow-sm border-b border-gray-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<button
|
||||
onClick={() => setEjercicioActivo(null)}
|
||||
className="inline-flex items-center text-blue-600 hover:text-blue-700 font-medium"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Volver al módulo
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">{ejercicio.titulo}</h1>
|
||||
<p className="text-gray-600">{ejercicio.descripcion}</p>
|
||||
</div>
|
||||
|
||||
<EjercicioComponent
|
||||
ejercicioId={ejercicio.id}
|
||||
onComplete={(puntuacion: number) => handleCompleteEjercicio(ejercicio.id, puntuacion)}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Loader size="lg" className="mx-auto mb-4" />
|
||||
<p className="text-gray-600">Cargando ejercicios...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center max-w-md mx-auto px-4">
|
||||
<div className="text-red-500 mb-4">
|
||||
<svg className="w-16 h-16 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-2">Error al cargar el progreso</h2>
|
||||
<p className="text-gray-600 mb-4">{error}</p>
|
||||
<Button onClick={loadProgreso}>Reintentar</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<header className="bg-white shadow-sm">
|
||||
<header className="bg-white shadow-sm border-b border-gray-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<Link to="/" className="inline-flex items-center text-primary hover:underline">
|
||||
<Link to="/" className="inline-flex items-center text-blue-600 hover:text-blue-700 font-medium">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Volver al Dashboard
|
||||
</Link>
|
||||
@@ -67,7 +217,7 @@ export function Modulo() {
|
||||
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="w-14 h-14 bg-gradient-to-br from-primary to-blue-600 rounded-xl flex items-center justify-center text-white text-2xl font-bold shadow-lg">
|
||||
<div className={`w-16 h-16 bg-gradient-to-br ${moduloInfo.color} rounded-xl flex items-center justify-center text-white text-2xl font-bold shadow-lg`}>
|
||||
{num}
|
||||
</div>
|
||||
<div>
|
||||
@@ -76,76 +226,153 @@ export function Modulo() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="bg-gradient-to-r from-primary to-blue-600 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<Card className={`bg-gradient-to-r ${moduloInfo.color} text-white border-none`}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<p className="text-blue-100">Tu progreso en este módulo</p>
|
||||
<p className="text-white/80 text-sm">Tu progreso en este módulo</p>
|
||||
<p className="text-3xl font-bold mt-1">{porcentaje}%</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-blue-100">{completados}/{ejercicios.length} ejercicios</p>
|
||||
<p className="text-white/80 text-sm">Ejercicios</p>
|
||||
<p className="text-xl font-bold">{completados}/{ejercicios.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 w-full bg-white/20 rounded-full h-2">
|
||||
<div
|
||||
className="bg-white h-2 rounded-full transition-all"
|
||||
style={{ width: `${porcentaje}%` }}
|
||||
|
||||
<div className="w-full bg-white/20 rounded-full h-3 overflow-hidden">
|
||||
<motion.div
|
||||
className="bg-white h-full rounded-full"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${porcentaje}%` }}
|
||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-4">Ejercicios</h2>
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold text-gray-900">Ejercicios</h2>
|
||||
<ScoreDisplay puntos={puntuacionTotal} animar={false} showNivel={false} size="sm" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-4">
|
||||
{ejercicios.map((ejercicio, index) => {
|
||||
const progreso = getProgresoForEjercicio(ejercicio.id);
|
||||
const progreso = getProgresoEjercicioLocal(ejercicio.id);
|
||||
const completado = progreso?.completado || false;
|
||||
const bloqueado = isEjercicioBloqueado(index);
|
||||
|
||||
return (
|
||||
<Card key={ejercicio.id} className="hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
||||
completado ? 'bg-success text-white' : 'bg-gray-100 text-gray-500'
|
||||
}`}>
|
||||
{completado ? (
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
) : (
|
||||
<span className="font-medium">{index + 1}</span>
|
||||
)}
|
||||
</div>
|
||||
<motion.div
|
||||
key={ejercicio.id}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
>
|
||||
<Card className={`transition-all ${
|
||||
bloqueado
|
||||
? 'opacity-60 bg-gray-50'
|
||||
: 'hover:shadow-md cursor-pointer'
|
||||
}`}
|
||||
onClick={() => !bloqueado && setEjercicioActivo(ejercicio.id)}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-12 h-12 rounded-full flex items-center justify-center ${
|
||||
completado
|
||||
? 'bg-green-500 text-white'
|
||||
: bloqueado
|
||||
? 'bg-gray-200 text-gray-400'
|
||||
: 'bg-blue-100 text-blue-600'
|
||||
}`}>
|
||||
{completado ? (
|
||||
<CheckCircle className="w-6 h-6" />
|
||||
) : bloqueado ? (
|
||||
<Lock className="w-5 h-5" />
|
||||
) : (
|
||||
<span className="font-bold">{index + 1}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-gray-900">{ejercicio.titulo}</h3>
|
||||
<p className="text-sm text-gray-500">{ejercicio.descripcion}</p>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className={`font-semibold ${
|
||||
completado ? 'text-gray-900' : bloqueado ? 'text-gray-500' : 'text-gray-900'
|
||||
}`}>
|
||||
{ejercicio.titulo}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500">{ejercicio.descripcion}</p>
|
||||
{completado && progreso && progreso.puntuacion > 0 && (
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-xs font-medium text-green-600">
|
||||
Mejor puntuación: {progreso.puntuacion} pts
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
({progreso.intentos} {progreso.intentos === 1 ? 'intento' : 'intentos'})
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button size="sm">
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
{completado ? 'Repetir' : 'Comenzar'}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={bloqueado}
|
||||
variant={completado ? 'outline' : 'primary'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
!bloqueado && setEjercicioActivo(ejercicio.id);
|
||||
}}
|
||||
>
|
||||
{completado ? (
|
||||
<>
|
||||
<RotateCcw className="w-4 h-4 mr-2" />
|
||||
Repetir
|
||||
</>
|
||||
) : bloqueado ? (
|
||||
'Bloqueado'
|
||||
) : (
|
||||
<>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
Comenzar
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{porcentaje === 100 && (
|
||||
<Card className="mt-6 bg-success/10 border border-success">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-success rounded-full flex items-center justify-center">
|
||||
<CheckCircle className="w-6 h-6 text-white" />
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
<Card className="mt-6 bg-gradient-to-r from-green-500 to-emerald-600 text-white border-none">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-14 h-14 bg-white/20 rounded-full flex items-center justify-center">
|
||||
<Trophy className="w-7 h-7" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-bold text-lg">¡Felicitaciones!</h3>
|
||||
<p className="text-green-100">
|
||||
Has completado todos los ejercicios de este módulo.
|
||||
{num < 4 ? ' ¡Continúa con el siguiente módulo!' : ' ¡Has completado todos los módulos!'}
|
||||
</p>
|
||||
</div>
|
||||
{num < 4 && (
|
||||
<Link to={`/modulo/${num + 1}`}>
|
||||
<Button variant="primary" className="bg-white text-green-600 hover:bg-green-50">
|
||||
Siguiente módulo
|
||||
<TrendingUp className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-success">¡Felicitaciones!</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
Has completado todos los ejercicios de este módulo.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Modulo;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Card } from '../components/ui/Card';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { ArrowRight, ArrowLeft } from 'lucide-react';
|
||||
import { useProgresoStore } from '../stores/progresoStore';
|
||||
import { ArrowRight, ArrowLeft, CheckCircle, Lock, Play } from 'lucide-react';
|
||||
|
||||
const MODULOS = [
|
||||
{
|
||||
@@ -9,28 +10,45 @@ const MODULOS = [
|
||||
titulo: 'Fundamentos de Economía',
|
||||
descripcion: 'Aprende los conceptos básicos: definición de economía, agentes económicos, factores de producción y el flujo circular de la economía.',
|
||||
temas: ['Definición de economía', 'Agentes económicos', 'Factores de producción', 'Flujo circular'],
|
||||
totalEjercicios: 5,
|
||||
bloqueado: false,
|
||||
},
|
||||
{
|
||||
numero: 2,
|
||||
titulo: 'Oferta, Demanda y Equilibrio',
|
||||
descripcion: 'Domina las curvas de oferta y demanda, aprende cómo se determinan los precios y entiende los controles de mercado.',
|
||||
temas: ['Curva de demanda', 'Curva de oferta', 'Equilibrio de mercado', 'Controles de precios'],
|
||||
totalEjercicios: 5,
|
||||
bloqueado: false,
|
||||
},
|
||||
{
|
||||
numero: 3,
|
||||
titulo: 'Utilidad y Elasticidad',
|
||||
descripcion: 'Explora la teoría del consumidor, aprende a calcular elasticidades y clasifica diferentes tipos de bienes.',
|
||||
temas: ['Utilidad marginal', 'Elasticidad precio', 'Elasticidad ingreso', 'Clasificación de bienes'],
|
||||
totalEjercicios: 5,
|
||||
bloqueado: false,
|
||||
},
|
||||
{
|
||||
numero: 4,
|
||||
titulo: 'Teoría del Productor',
|
||||
descripcion: 'Comprende los costos de producción, la toma de decisiones del productor y los fundamentos de la competencia perfecta.',
|
||||
temas: ['Costos de producción', 'Producción y costos', 'Competencia perfecta', 'Maximización de beneficios'],
|
||||
totalEjercicios: 5,
|
||||
bloqueado: false,
|
||||
},
|
||||
];
|
||||
|
||||
export function Modulos() {
|
||||
const { progresoModulos } = useProgresoStore();
|
||||
|
||||
const getModuloProgress = (moduloNumero: number, totalEjercicios: number) => {
|
||||
const progreso = progresoModulos.find(p => p.moduloNumero === moduloNumero);
|
||||
const completados = progreso?.ejercicios.filter(e => e.completado).length || 0;
|
||||
const porcentaje = Math.round((completados / totalEjercicios) * 100);
|
||||
return { completados, porcentaje };
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<header className="bg-white shadow-sm">
|
||||
@@ -52,42 +70,90 @@ export function Modulos() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{MODULOS.map((modulo) => (
|
||||
<Card key={modulo.numero} className="hover:shadow-lg transition-shadow">
|
||||
<div className="flex flex-col md:flex-row md:items-center gap-6">
|
||||
<div className="flex items-center gap-4 md:w-32">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-primary to-blue-600 rounded-2xl flex items-center justify-center text-white text-2xl font-bold shadow-lg">
|
||||
{modulo.numero}
|
||||
{MODULOS.map((modulo) => {
|
||||
const { completados, porcentaje } = getModuloProgress(modulo.numero, modulo.totalEjercicios);
|
||||
const estaCompletado = porcentaje === 100;
|
||||
|
||||
return (
|
||||
<Card key={modulo.numero} className={`hover:shadow-lg transition-shadow ${
|
||||
modulo.bloqueado ? 'opacity-75' : ''
|
||||
}`}>
|
||||
<div className="flex flex-col md:flex-row md:items-center gap-6">
|
||||
<div className="flex items-center gap-4 md:w-32">
|
||||
<div className={`w-16 h-16 rounded-2xl flex items-center justify-center text-white text-2xl font-bold shadow-lg ${
|
||||
estaCompletado
|
||||
? 'bg-gradient-to-br from-success to-green-600'
|
||||
: 'bg-gradient-to-br from-primary to-blue-600'
|
||||
}`}>
|
||||
{modulo.numero}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h2 className="text-xl font-bold text-gray-900">{modulo.titulo}</h2>
|
||||
{estaCompletado && (
|
||||
<span className="flex items-center gap-1 text-success text-sm">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
Completado
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-gray-600 mb-4">{modulo.descripcion}</p>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{modulo.temas.map((tema) => (
|
||||
<span
|
||||
key={tema}
|
||||
className="px-3 py-1 bg-gray-100 text-gray-700 text-sm rounded-full"
|
||||
>
|
||||
{tema}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all ${
|
||||
estaCompletado ? 'bg-success' : 'bg-primary'
|
||||
}`}
|
||||
style={{ width: `${porcentaje}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
{completados}/{modulo.totalEjercicios} ejercicios completados ({porcentaje}%)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="md:text-right">
|
||||
{modulo.bloqueado ? (
|
||||
<Button disabled>
|
||||
<Lock className="w-4 h-4 mr-2" />
|
||||
Bloqueado
|
||||
</Button>
|
||||
) : (
|
||||
<Link to={`/modulo/${modulo.numero}`}>
|
||||
<Button>
|
||||
{estaCompletado ? (
|
||||
<>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
Revisar
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
Entrar
|
||||
</>
|
||||
)}
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-2">{modulo.titulo}</h2>
|
||||
<p className="text-gray-600 mb-4">{modulo.descripcion}</p>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{modulo.temas.map((tema) => (
|
||||
<span
|
||||
key={tema}
|
||||
className="px-3 py-1 bg-gray-100 text-gray-700 text-sm rounded-full"
|
||||
>
|
||||
{tema}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="md:text-right">
|
||||
<Link to={`/modulo/${modulo.numero}`}>
|
||||
<Button>
|
||||
Entrar
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
134
frontend/src/pages/Recursos.tsx
Normal file
134
frontend/src/pages/Recursos.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import { Card } from '../components/ui/Card';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { FileText, Download, BookOpen, ArrowLeft } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const recursos = [
|
||||
{
|
||||
id: 1,
|
||||
titulo: 'Resumen Clase 1 - Fundamentos de Economía',
|
||||
descripcion: 'Definición de economía, agentes económicos, factores de producción y flujo circular',
|
||||
archivo: '/pdfs/resumen_clase_1.pdf',
|
||||
modulo: 'Módulo 1',
|
||||
icono: FileText
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
titulo: 'Resumen Clase 2 - Oferta, Demanda y Equilibrio',
|
||||
descripcion: 'Ley de la demanda, ley de la oferta, equilibrio de mercado y controles de precios',
|
||||
archivo: '/pdfs/resumen_clase_2.pdf',
|
||||
modulo: 'Módulo 2',
|
||||
icono: FileText
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
titulo: 'Resumen Clase 3 - Elasticidad',
|
||||
descripcion: 'Tipos de elasticidad, cálculos y clasificación de bienes según elasticidad',
|
||||
archivo: '/pdfs/resumen_clase_3.pdf',
|
||||
modulo: 'Módulo 3',
|
||||
icono: FileText
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
titulo: 'Resumen Clase 4 - Teoría del Productor',
|
||||
descripcion: 'Costos, producción, competencia perfecta y maximización de beneficios',
|
||||
archivo: '/pdfs/resumen_clase_4.pdf',
|
||||
modulo: 'Módulo 4',
|
||||
icono: FileText
|
||||
}
|
||||
];
|
||||
|
||||
export function RecursosPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-8">
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center text-gray-600 hover:text-blue-600 mb-4"
|
||||
>
|
||||
<ArrowLeft size={20} className="mr-2" />
|
||||
Volver al Dashboard
|
||||
</Link>
|
||||
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Recursos de Estudio</h1>
|
||||
<p className="text-gray-600">
|
||||
Material académico en PDF para consultar offline
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Info Card */}
|
||||
<Card className="mb-8 bg-blue-50 border-blue-200">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-3 bg-blue-100 rounded-lg">
|
||||
<BookOpen className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-blue-900 mb-1">Material de Apoyo</h2>
|
||||
<p className="text-blue-800 text-sm">
|
||||
Estos documentos PDF contienen el contenido teórico de cada módulo.
|
||||
Úsalos como referencia mientras realizas los ejercicios interactivos.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Recursos Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{recursos.map((recurso) => (
|
||||
<Card key={recurso.id} className="hover:shadow-lg transition-shadow">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-3 bg-gray-100 rounded-lg">
|
||||
<recurso.icono className="w-8 h-8 text-gray-600" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="mb-2">
|
||||
<span className="inline-block px-2 py-1 text-xs font-medium bg-gray-100 text-gray-600 rounded">
|
||||
{recurso.modulo}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
{recurso.titulo}
|
||||
</h3>
|
||||
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
{recurso.descripcion}
|
||||
</p>
|
||||
|
||||
<a
|
||||
href={recurso.archivo}
|
||||
download
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button variant="outline" className="w-full">
|
||||
<Download size={18} className="mr-2" />
|
||||
Descargar PDF
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-12 text-center">
|
||||
<p className="text-gray-500 text-sm">
|
||||
¿Tienes dudas sobre el contenido? Revisa los ejercicios interactivos en cada módulo.
|
||||
</p>
|
||||
<Link to="/modulos">
|
||||
<Button className="mt-4">
|
||||
Ver Módulos
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RecursosPage;
|
||||
364
frontend/src/pages/modulos/Modulo1Page.tsx
Normal file
364
frontend/src/pages/modulos/Modulo1Page.tsx
Normal file
@@ -0,0 +1,364 @@
|
||||
// @ts-nocheck
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Card } from '../../components/ui/Card';
|
||||
import { Button } from '../../components/ui/Button';
|
||||
import { progresoService } from '../../services/api';
|
||||
import type { Progreso } from '../../types';
|
||||
import { ArrowLeft, CheckCircle, Play, BookOpen, Trophy, ChevronRight } from 'lucide-react';
|
||||
|
||||
// Importar contenido del módulo 1
|
||||
import { introduccion, agentes, factores } from '../../content/modulo1';
|
||||
import { ejercicios as modulo1Ejercicios } from '../../content/modulo1/ejercicios';
|
||||
|
||||
// Importar componentes de ejercicios
|
||||
import { SimuladorDisyuntivas, QuizBienes, FlujoCircular } from '../../components/exercises/modulo1';
|
||||
|
||||
const TABS = ['Contenido', 'Ejercicios'] as const;
|
||||
type Tab = typeof TABS[number];
|
||||
|
||||
interface EjercicioConfig {
|
||||
id: string;
|
||||
titulo: string;
|
||||
descripcion: string;
|
||||
componente: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Modulo1Page() {
|
||||
const { numero } = useParams<{ numero: string }>();
|
||||
const [activeTab, setActiveTab] = useState<Tab>('Contenido');
|
||||
const [activeSeccion, setActiveSeccion] = useState<'introduccion' | 'agentes' | 'factores'>('introduccion');
|
||||
const [activeEjercicio, setActiveEjercicio] = useState<string | null>(null);
|
||||
const [progresos, setProgresos] = useState<Progreso[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Cargar progreso al montar
|
||||
useEffect(() => {
|
||||
loadProgreso();
|
||||
}, []);
|
||||
|
||||
const loadProgreso = async () => {
|
||||
try {
|
||||
const data = await progresoService.getProgreso();
|
||||
setProgresos(data);
|
||||
} catch {
|
||||
// Silenciar error
|
||||
}
|
||||
};
|
||||
|
||||
const getProgresoForEjercicio = (ejercicioId: string) => {
|
||||
return progresos.find(
|
||||
(p) => p.modulo_numero === 1 && p.ejercicio_id === ejercicioId
|
||||
);
|
||||
};
|
||||
|
||||
const handleCompleteEjercicio = async (ejercicioId: string, puntuacion: number) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await progresoService.saveProgreso(ejercicioId, puntuacion);
|
||||
await loadProgreso();
|
||||
} catch {
|
||||
// Silenciar error
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Configuración de ejercicios con sus componentes
|
||||
const ejerciciosConfig: EjercicioConfig[] = [
|
||||
{
|
||||
id: 'simulador-disyuntivas',
|
||||
titulo: modulo1Ejercicios.ejercicios[0].titulo,
|
||||
descripcion: modulo1Ejercicios.ejercicios[0].descripcion,
|
||||
componente: (
|
||||
<SimuladorDisyuntivas
|
||||
ejercicioId="simulador-disyuntivas"
|
||||
onComplete={(puntuacion) => handleCompleteEjercicio('simulador-disyuntivas', puntuacion)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'quiz-clasificacion-bienes',
|
||||
titulo: modulo1Ejercicios.ejercicios[1].titulo,
|
||||
descripcion: modulo1Ejercicios.ejercicios[1].descripcion,
|
||||
componente: (
|
||||
<QuizBienes
|
||||
ejercicioId="quiz-clasificacion-bienes"
|
||||
onComplete={(puntuacion) => handleCompleteEjercicio('quiz-clasificacion-bienes', puntuacion)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'juego-flujo-circular',
|
||||
titulo: modulo1Ejercicios.ejercicios[2].titulo,
|
||||
descripcion: modulo1Ejercicios.ejercicios[2].descripcion,
|
||||
componente: (
|
||||
<FlujoCircular
|
||||
ejercicioId="juego-flujo-circular"
|
||||
onComplete={(puntuacion) => handleCompleteEjercicio('juego-flujo-circular', puntuacion)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const seccionesContenido = {
|
||||
introduccion: {
|
||||
titulo: introduccion.titulo,
|
||||
data: introduccion,
|
||||
},
|
||||
agentes: {
|
||||
titulo: agentes.titulo,
|
||||
data: agentes,
|
||||
},
|
||||
factores: {
|
||||
titulo: factores.titulo,
|
||||
data: factores,
|
||||
},
|
||||
};
|
||||
|
||||
const currentSeccion = seccionesContenido[activeSeccion];
|
||||
|
||||
// Calcular progreso del módulo
|
||||
const ejerciciosCompletados = ejerciciosConfig.filter(
|
||||
(e) => getProgresoForEjercicio(e.id)?.completado
|
||||
).length;
|
||||
const porcentajeProgreso = Math.round((ejerciciosCompletados / ejerciciosConfig.length) * 100);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header */}
|
||||
<header className="bg-white shadow-sm sticky top-0 z-10">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link to="/modulos" className="inline-flex items-center text-blue-600 hover:underline">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Volver a Módulos
|
||||
</Link>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600">Progreso:</span>
|
||||
<div className="w-32 bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-green-500 h-2 rounded-full transition-all"
|
||||
style={{ width: `${porcentajeProgreso}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-700">{porcentajeProgreso}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Título del módulo */}
|
||||
<div className="bg-gradient-to-r from-blue-600 to-blue-800 text-white">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-16 h-16 bg-white/20 rounded-xl flex items-center justify-center text-3xl font-bold">
|
||||
1
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Módulo 1: Fundamentos de Economía</h1>
|
||||
<p className="text-blue-100 mt-1">
|
||||
Introducción a los conceptos básicos, agentes económicos y factores de producción
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<div className="flex gap-2 border-b border-gray-200">
|
||||
{TABS.map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => {
|
||||
setActiveTab(tab);
|
||||
setActiveEjercicio(null);
|
||||
}}
|
||||
className={`px-6 py-3 font-medium text-sm transition-colors relative ${
|
||||
activeTab === tab
|
||||
? 'text-blue-600'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{tab === 'Contenido' && <BookOpen className="w-4 h-4 inline mr-2" />}
|
||||
{tab === 'Ejercicios' && <Trophy className="w-4 h-4 inline mr-2" />}
|
||||
{tab}
|
||||
{activeTab === tab && (
|
||||
<motion.div
|
||||
layoutId="activeTab"
|
||||
className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Contenido según tab activo */}
|
||||
<AnimatePresence mode="wait">
|
||||
{activeTab === 'Contenido' ? (
|
||||
<motion.div
|
||||
key="contenido"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
className="mt-6 grid grid-cols-1 lg:grid-cols-4 gap-6"
|
||||
>
|
||||
{/* Navegación de secciones */}
|
||||
<div className="lg:col-span-1">
|
||||
<Card className="sticky top-24">
|
||||
<h3 className="font-semibold text-gray-900 mb-4">Secciones</h3>
|
||||
<nav className="space-y-2">
|
||||
{(Object.keys(seccionesContenido) as Array<keyof typeof seccionesContenido>).map((key) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setActiveSeccion(key)}
|
||||
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors flex items-center justify-between ${
|
||||
activeSeccion === key
|
||||
? 'bg-blue-50 text-blue-700 font-medium'
|
||||
: 'text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{seccionesContenido[key].titulo}
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Contenido de la sección */}
|
||||
<div className="lg:col-span-3 space-y-6">
|
||||
<Card>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-6">{currentSeccion.titulo}</h2>
|
||||
<div className="space-y-6">
|
||||
{currentSeccion.data.contenido.map((seccion, index) => (
|
||||
<div key={index} className="border-b border-gray-100 last:border-0 pb-6 last:pb-0">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-3">{seccion.titulo}</h3>
|
||||
<div className="prose prose-blue max-w-none">
|
||||
{seccion.contenido.split('\n\n').map((parrafo, pIndex) => (
|
||||
<p key={pIndex} className="text-gray-600 mb-4 leading-relaxed whitespace-pre-line">
|
||||
{parrafo}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Ejercicios relacionados con la sección */}
|
||||
{currentSeccion.data.ejercicios && currentSeccion.data.ejercicios.length > 0 && (
|
||||
<Card className="bg-blue-50 border-blue-200">
|
||||
<h3 className="font-semibold text-blue-900 mb-3">Ejercicios Relacionados</h3>
|
||||
<p className="text-blue-700 text-sm mb-4">
|
||||
Practica lo aprendido con estos ejercicios interactivos
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => setActiveTab('Ejercicios')}
|
||||
variant="outline"
|
||||
className="border-blue-300 text-blue-700 hover:bg-blue-100"
|
||||
>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
Ir a Ejercicios
|
||||
</Button>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="ejercicios"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
className="mt-6"
|
||||
>
|
||||
{activeEjercicio ? (
|
||||
// Vista de ejercicio activo
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<Button variant="ghost" onClick={() => setActiveEjercicio(null)}>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Volver a ejercicios
|
||||
</Button>
|
||||
{loading && <span className="text-sm text-gray-500">Guardando progreso...</span>}
|
||||
</div>
|
||||
{ejerciciosConfig.find((e) => e.id === activeEjercicio)?.componente}
|
||||
</div>
|
||||
) : (
|
||||
// Lista de ejercicios
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{ejerciciosConfig.map((ejercicio, index) => {
|
||||
const progreso = getProgresoForEjercicio(ejercicio.id);
|
||||
const completado = progreso?.completado || false;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={ejercicio.id}
|
||||
className="hover:shadow-lg transition-shadow cursor-pointer"
|
||||
onClick={() => setActiveEjercicio(ejercicio.id)}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div
|
||||
className={`w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0 ${
|
||||
completado ? 'bg-green-100 text-green-600' : 'bg-blue-100 text-blue-600'
|
||||
}`}
|
||||
>
|
||||
{completado ? (
|
||||
<CheckCircle className="w-6 h-6" />
|
||||
) : (
|
||||
<span className="text-xl font-bold">{index + 1}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-gray-900">{ejercicio.titulo}</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">{ejercicio.descripcion}</p>
|
||||
{completado && progreso && (
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<span className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded-full">
|
||||
Completado
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{progreso.puntuacion} pts
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button className="w-full mt-4" size="sm">
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
{completado ? 'Repetir' : 'Comenzar'}
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mensaje de completado */}
|
||||
{ejerciciosCompletados === ejerciciosConfig.length && ejerciciosConfig.length > 0 && (
|
||||
<Card className="mt-6 bg-green-50 border-green-200">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<Trophy className="w-6 h-6 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-green-900">¡Felicitaciones!</h3>
|
||||
<p className="text-green-700 text-sm">
|
||||
Has completado todos los ejercicios de este módulo.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Modulo1Page;
|
||||
499
frontend/src/pages/modulos/Modulo2Page.tsx
Normal file
499
frontend/src/pages/modulos/Modulo2Page.tsx
Normal file
@@ -0,0 +1,499 @@
|
||||
// @ts-nocheck
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Card } from '../../components/ui/Card';
|
||||
import { Button } from '../../components/ui/Button';
|
||||
import { progresoService } from '../../services/api';
|
||||
import type { Progreso } from '../../types';
|
||||
import { ArrowLeft, CheckCircle, Play, BookOpen, Trophy, ChevronRight } from 'lucide-react';
|
||||
|
||||
// Importar contenido del módulo 2
|
||||
import { default as demandaContent } from '../../content/modulo2/demanda';
|
||||
import { default as ofertaContent } from '../../content/modulo2/oferta';
|
||||
import { default as equilibrioContent } from '../../content/modulo2/equilibrio';
|
||||
|
||||
// Importar componentes de ejercicios
|
||||
import { ConstructorCurvas, SimuladorPrecios, IdentificarShocks } from '../../components/exercises/modulo2';
|
||||
|
||||
const TABS = ['Contenido', 'Ejercicios'] as const;
|
||||
type Tab = typeof TABS[number];
|
||||
|
||||
interface EjercicioConfig {
|
||||
id: string;
|
||||
titulo: string;
|
||||
descripcion: string;
|
||||
componente: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Modulo2Page() {
|
||||
const { numero } = useParams<{ numero: string }>();
|
||||
const [activeTab, setActiveTab] = useState<Tab>('Contenido');
|
||||
const [activeSeccion, setActiveSeccion] = useState<'demanda' | 'oferta' | 'equilibrio'>('demanda');
|
||||
const [activeEjercicio, setActiveEjercicio] = useState<string | null>(null);
|
||||
const [progresos, setProgresos] = useState<Progreso[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadProgreso();
|
||||
}, []);
|
||||
|
||||
const loadProgreso = async () => {
|
||||
try {
|
||||
const data = await progresoService.getProgreso();
|
||||
setProgresos(data);
|
||||
} catch {
|
||||
// Silenciar error
|
||||
}
|
||||
};
|
||||
|
||||
const getProgresoForEjercicio = (ejercicioId: string) => {
|
||||
return progresos.find(
|
||||
(p) => p.modulo_numero === 2 && p.ejercicio_id === ejercicioId
|
||||
);
|
||||
};
|
||||
|
||||
const handleCompleteEjercicio = async (ejercicioId: string, puntuacion: number) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await progresoService.saveProgreso(ejercicioId, puntuacion);
|
||||
await loadProgreso();
|
||||
} catch {
|
||||
// Silenciar error
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Configuración de ejercicios
|
||||
const ejerciciosConfig: EjercicioConfig[] = [
|
||||
{
|
||||
id: 'constructor-curvas',
|
||||
titulo: 'Constructor de Curvas de Oferta y Demanda',
|
||||
descripcion: 'Construye curvas de oferta y demanda arrastrando puntos para entender sus pendientes y movimientos.',
|
||||
componente: (
|
||||
<ConstructorCurvas
|
||||
ejercicioId="constructor-curvas"
|
||||
onComplete={(puntuacion) => handleCompleteEjercicio('constructor-curvas', puntuacion)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'simulador-precios',
|
||||
titulo: 'Simulador de Precios Intervenidos',
|
||||
descripcion: 'Ajusta precios máximos y mínimos para observar sus efectos en el mercado: escasez, superávit, y pérdida de bienestar.',
|
||||
componente: (
|
||||
<SimuladorPrecios
|
||||
ejercicioId="simulador-precios"
|
||||
onComplete={(puntuacion) => handleCompleteEjercicio('simulador-precios', puntuacion)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'identificar-shocks',
|
||||
titulo: 'Identificador de Shocks del Mercado',
|
||||
descripcion: 'Analiza escenarios económicos reales e identifica si afectan la oferta, la demanda, ambas, y cómo cambian precio y cantidad de equilibrio.',
|
||||
componente: (
|
||||
<IdentificarShocks
|
||||
ejercicioId="identificar-shocks"
|
||||
onComplete={(puntuacion) => handleCompleteEjercicio('identificar-shocks', puntuacion)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// Estructura de contenido del módulo 2
|
||||
const seccionesContenido = {
|
||||
demanda: {
|
||||
titulo: 'Ley de la Demanda',
|
||||
contenido: [
|
||||
{
|
||||
titulo: demandaContent.definicion.titulo,
|
||||
texto: demandaContent.definicion.definicion,
|
||||
elementos: demandaContent.definicion.elementosClave,
|
||||
},
|
||||
{
|
||||
titulo: demandaContent.ley.titulo,
|
||||
texto: demandaContent.ley.enunciado,
|
||||
efectos: demandaContent.ley.efectos,
|
||||
},
|
||||
{
|
||||
titulo: 'Factores que Desplazan la Demanda',
|
||||
texto: 'Los siguientes factores causan desplazamientos de la curva de demanda:',
|
||||
factores: demandaContent.factores,
|
||||
},
|
||||
],
|
||||
},
|
||||
oferta: {
|
||||
titulo: 'Ley de la Oferta',
|
||||
contenido: [
|
||||
{
|
||||
titulo: ofertaContent.definicion.titulo,
|
||||
texto: ofertaContent.definicion.definicion,
|
||||
elementos: ofertaContent.definicion.elementosClave,
|
||||
},
|
||||
{
|
||||
titulo: ofertaContent.ley.titulo,
|
||||
texto: ofertaContent.ley.enunciado,
|
||||
razones: ofertaContent.ley.razones,
|
||||
},
|
||||
{
|
||||
titulo: 'Factores que Desplazan la Oferta',
|
||||
texto: 'Los siguientes factores causan desplazamientos de la curva de oferta:',
|
||||
factores: ofertaContent.factores,
|
||||
},
|
||||
],
|
||||
},
|
||||
equilibrio: {
|
||||
titulo: 'Equilibrio de Mercado',
|
||||
contenido: [
|
||||
{
|
||||
titulo: equilibrioContent.definicion.titulo,
|
||||
texto: equilibrioContent.definicion.definicion,
|
||||
caracteristicas: equilibrioContent.definicion.caracteristicas,
|
||||
},
|
||||
{
|
||||
titulo: 'Excedentes del Mercado',
|
||||
texto: 'En el equilibrio se generan beneficios para consumidores y productores:',
|
||||
excedentes: [
|
||||
equilibrioContent.excedentes.excedenteConsumidor,
|
||||
equilibrioContent.excedentes.excedenteProductor,
|
||||
],
|
||||
},
|
||||
{
|
||||
titulo: 'Controles de Precio',
|
||||
texto: 'Los gobiernos pueden intervenir el mercado estableciendo precios máximos o mínimos:',
|
||||
controles: [
|
||||
equilibrioContent.controles.precioMaximo,
|
||||
equilibrioContent.controles.precioMinimo,
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const currentSeccion = seccionesContenido[activeSeccion];
|
||||
|
||||
// Calcular progreso
|
||||
const ejerciciosCompletados = ejerciciosConfig.filter(
|
||||
(e) => getProgresoForEjercicio(e.id)?.completado
|
||||
).length;
|
||||
const porcentajeProgreso = Math.round((ejerciciosCompletados / ejerciciosConfig.length) * 100);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header */}
|
||||
<header className="bg-white shadow-sm sticky top-0 z-10">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link to="/modulos" className="inline-flex items-center text-blue-600 hover:underline">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Volver a Módulos
|
||||
</Link>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600">Progreso:</span>
|
||||
<div className="w-32 bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-green-500 h-2 rounded-full transition-all"
|
||||
style={{ width: `${porcentajeProgreso}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-700">{porcentajeProgreso}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Título del módulo */}
|
||||
<div className="bg-gradient-to-r from-green-600 to-green-800 text-white">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-16 h-16 bg-white/20 rounded-xl flex items-center justify-center text-3xl font-bold">
|
||||
2
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Módulo 2: Oferta, Demanda y Equilibrio</h1>
|
||||
<p className="text-green-100 mt-1">
|
||||
Curvas de oferta y demanda, equilibrio de mercado y controles de precio
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<div className="flex gap-2 border-b border-gray-200">
|
||||
{TABS.map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => {
|
||||
setActiveTab(tab);
|
||||
setActiveEjercicio(null);
|
||||
}}
|
||||
className={`px-6 py-3 font-medium text-sm transition-colors relative ${
|
||||
activeTab === tab
|
||||
? 'text-blue-600'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{tab === 'Contenido' && <BookOpen className="w-4 h-4 inline mr-2" />}
|
||||
{tab === 'Ejercicios' && <Trophy className="w-4 h-4 inline mr-2" />}
|
||||
{tab}
|
||||
{activeTab === tab && (
|
||||
<motion.div
|
||||
layoutId="activeTab2"
|
||||
className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Contenido según tab activo */}
|
||||
<AnimatePresence mode="wait">
|
||||
{activeTab === 'Contenido' ? (
|
||||
<motion.div
|
||||
key="contenido"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
className="mt-6 grid grid-cols-1 lg:grid-cols-4 gap-6"
|
||||
>
|
||||
{/* Navegación de secciones */}
|
||||
<div className="lg:col-span-1">
|
||||
<Card className="sticky top-24">
|
||||
<h3 className="font-semibold text-gray-900 mb-4">Secciones</h3>
|
||||
<nav className="space-y-2">
|
||||
{(Object.keys(seccionesContenido) as Array<keyof typeof seccionesContenido>).map((key) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setActiveSeccion(key)}
|
||||
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors flex items-center justify-between ${
|
||||
activeSeccion === key
|
||||
? 'bg-green-50 text-green-700 font-medium'
|
||||
: 'text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{seccionesContenido[key].titulo}
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Contenido de la sección */}
|
||||
<div className="lg:col-span-3 space-y-6">
|
||||
<Card>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-6">{currentSeccion.titulo}</h2>
|
||||
<div className="space-y-6">
|
||||
{currentSeccion.contenido.map((item, index) => (
|
||||
<div key={index} className="border-b border-gray-100 last:border-0 pb-6 last:pb-0">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-3">{item.titulo}</h3>
|
||||
<p className="text-gray-600 mb-4 leading-relaxed">{item.texto}</p>
|
||||
|
||||
{/* Mostrar elementos clave si existen */}
|
||||
{item.elementos && (
|
||||
<ul className="list-disc list-inside space-y-2 text-gray-600">
|
||||
{item.elementos.map((el: any, i: number) => (
|
||||
<li key={i}>
|
||||
<strong>{el.elemento}:</strong> {el.descripcion}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{/* Mostrar efectos/razones si existen */}
|
||||
{item.efectos && (
|
||||
<div className="space-y-3">
|
||||
{item.efectos.map((efecto: any, i: number) => (
|
||||
<div key={i} className="bg-gray-50 p-3 rounded-lg">
|
||||
<h4 className="font-medium text-gray-800">{efecto.nombre}</h4>
|
||||
<p className="text-sm text-gray-600">{efecto.descripcion}</p>
|
||||
<p className="text-sm text-blue-600 mt-1">Ejemplo: {efecto.ejemplo}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mostrar razones si existen */}
|
||||
{item.razones && (
|
||||
<div className="space-y-3">
|
||||
{item.razones.map((razon: any, i: number) => (
|
||||
<div key={i} className="bg-gray-50 p-3 rounded-lg">
|
||||
<h4 className="font-medium text-gray-800">{razon.nombre}</h4>
|
||||
<p className="text-sm text-gray-600">{razon.descripcion}</p>
|
||||
<p className="text-sm text-green-600 mt-1">Ejemplo: {razon.ejemplo}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mostrar factores si existen */}
|
||||
{item.factores && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{item.factores.map((factor: any, i: number) => (
|
||||
<div key={i} className="bg-blue-50 p-3 rounded-lg border border-blue-100">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-lg">{factor.icono}</span>
|
||||
<h4 className="font-medium text-blue-900">{factor.nombre}</h4>
|
||||
</div>
|
||||
<p className="text-sm text-blue-700">{factor.descripcion}</p>
|
||||
<p className="text-sm text-blue-600 mt-1 italic">{factor.ejemplo}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mostrar características si existen */}
|
||||
{item.caracteristicas && (
|
||||
<ul className="list-disc list-inside space-y-2 text-gray-600">
|
||||
{item.caracteristicas.map((car: any, i: number) => (
|
||||
<li key={i}>
|
||||
<strong>{car.caracteristica}:</strong> {car.explicacion}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{/* Mostrar excedentes si existen */}
|
||||
{item.excedentes && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{item.excedentes.map((exc: any, i: number) => (
|
||||
<div key={i} className="bg-purple-50 p-3 rounded-lg border border-purple-100">
|
||||
<h4 className="font-medium text-purple-900">{exc.nombre}</h4>
|
||||
<p className="text-sm text-purple-700">{exc.definicion}</p>
|
||||
<p className="text-sm text-purple-600 mt-1">Fórmula: {exc.formula}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mostrar controles si existen */}
|
||||
{item.controles && (
|
||||
<div className="space-y-3">
|
||||
{item.controles.map((control: any, i: number) => (
|
||||
<div key={i} className="bg-orange-50 p-3 rounded-lg border border-orange-100">
|
||||
<h4 className="font-medium text-orange-900">{control.nombre}</h4>
|
||||
<p className="text-sm text-orange-700">{control.definicion}</p>
|
||||
<p className="text-sm text-orange-600 mt-1">
|
||||
Condición: {control.condicionEfectivo}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-green-50 border-green-200">
|
||||
<h3 className="font-semibold text-green-900 mb-3">Ejercicios Relacionados</h3>
|
||||
<p className="text-green-700 text-sm mb-4">
|
||||
Pon a prueba tus conocimientos con ejercicios interactivos sobre oferta, demanda y equilibrio
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => setActiveTab('Ejercicios')}
|
||||
variant="outline"
|
||||
className="border-green-300 text-green-700 hover:bg-green-100"
|
||||
>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
Ir a Ejercicios
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="ejercicios"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
className="mt-6"
|
||||
>
|
||||
{activeEjercicio ? (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<Button variant="ghost" onClick={() => setActiveEjercicio(null)}>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Volver a ejercicios
|
||||
</Button>
|
||||
{loading && <span className="text-sm text-gray-500">Guardando progreso...</span>}
|
||||
</div>
|
||||
{ejerciciosConfig.find((e) => e.id === activeEjercicio)?.componente}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{ejerciciosConfig.map((ejercicio, index) => {
|
||||
const progreso = getProgresoForEjercicio(ejercicio.id);
|
||||
const completado = progreso?.completado || false;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={ejercicio.id}
|
||||
className="hover:shadow-lg transition-shadow cursor-pointer"
|
||||
onClick={() => setActiveEjercicio(ejercicio.id)}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div
|
||||
className={`w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0 ${
|
||||
completado ? 'bg-green-100 text-green-600' : 'bg-green-100 text-green-600'
|
||||
}`}
|
||||
>
|
||||
{completado ? (
|
||||
<CheckCircle className="w-6 h-6" />
|
||||
) : (
|
||||
<span className="text-xl font-bold">{index + 1}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-gray-900">{ejercicio.titulo}</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">{ejercicio.descripcion}</p>
|
||||
{completado && progreso && (
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<span className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded-full">
|
||||
Completado
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{progreso.puntuacion} pts
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button className="w-full mt-4" size="sm">
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
{completado ? 'Repetir' : 'Comenzar'}
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ejerciciosCompletados === ejerciciosConfig.length && ejerciciosConfig.length > 0 && (
|
||||
<Card className="mt-6 bg-green-50 border-green-200">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<Trophy className="w-6 h-6 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-green-900">¡Felicitaciones!</h3>
|
||||
<p className="text-green-700 text-sm">
|
||||
Has completado todos los ejercicios de este módulo.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Modulo2Page;
|
||||
610
frontend/src/pages/modulos/Modulo3Page.tsx
Normal file
610
frontend/src/pages/modulos/Modulo3Page.tsx
Normal file
@@ -0,0 +1,610 @@
|
||||
// @ts-nocheck
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Card } from '../../components/ui/Card';
|
||||
import { Button } from '../../components/ui/Button';
|
||||
import { progresoService } from '../../services/api';
|
||||
import type { Progreso } from '../../types';
|
||||
import { ArrowLeft, CheckCircle, Play, BookOpen, Trophy, ChevronRight } from 'lucide-react';
|
||||
|
||||
// Importar contenido del módulo 3
|
||||
import { conceptosElasticidad } from '../../content/modulo3/conceptos';
|
||||
import { tiposElasticidad } from '../../content/modulo3/tipos';
|
||||
import { clasificacionBienes } from '../../content/modulo3/clasificacion';
|
||||
import { ejercicios as modulo3Ejercicios } from '../../content/modulo3/ejercicios';
|
||||
|
||||
// Importar componentes de ejercicios
|
||||
import { CalculadoraElasticidad, ClasificadorBienes, EjerciciosExamen } from '../../components/exercises/modulo3';
|
||||
|
||||
const TABS = ['Contenido', 'Ejercicios'] as const;
|
||||
type Tab = typeof TABS[number];
|
||||
|
||||
interface EjercicioConfig {
|
||||
id: string;
|
||||
titulo: string;
|
||||
descripcion: string;
|
||||
componente: React.ReactNode;
|
||||
}
|
||||
|
||||
interface ContenidoItem {
|
||||
titulo: string;
|
||||
texto: string;
|
||||
interpretacion?: string;
|
||||
formula?: { latex?: string; ecuacion?: string } | string;
|
||||
interpretaciones?: Array<{
|
||||
rango: string;
|
||||
clasificacion: string;
|
||||
significado?: string;
|
||||
descripcion?: string;
|
||||
ejemplo?: string;
|
||||
}>;
|
||||
factores?: Array<{
|
||||
factor?: string;
|
||||
nombre?: string;
|
||||
efecto?: string;
|
||||
descripcion?: string;
|
||||
explicacion?: string;
|
||||
}>;
|
||||
reglas?: Array<{
|
||||
elasticidad: string;
|
||||
efectoPrecioArriba: string;
|
||||
efectoPrecioAbajo: string;
|
||||
}>;
|
||||
clasificacion?: Array<{
|
||||
tipo: string;
|
||||
descripcion: string;
|
||||
condicion: string;
|
||||
subtipos?: Array<{ tipo: string; rango: string }>;
|
||||
}>;
|
||||
categorias?: Array<{
|
||||
tipo: string;
|
||||
descripcion: string;
|
||||
condicion: string;
|
||||
ejemplos?: string[];
|
||||
}>;
|
||||
matriz?: Array<{
|
||||
combinacion: string;
|
||||
ejemplo: string;
|
||||
caracteristicas: string;
|
||||
}>;
|
||||
determinantes?: string[];
|
||||
}
|
||||
|
||||
export function Modulo3Page() {
|
||||
const { numero } = useParams<{ numero: string }>();
|
||||
const [activeTab, setActiveTab] = useState<Tab>('Contenido');
|
||||
const [activeSeccion, setActiveSeccion] = useState<'conceptos' | 'tipos' | 'clasificacion'>('conceptos');
|
||||
const [activeEjercicio, setActiveEjercicio] = useState<string | null>(null);
|
||||
const [progresos, setProgresos] = useState<Progreso[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadProgreso();
|
||||
}, []);
|
||||
|
||||
const loadProgreso = async () => {
|
||||
try {
|
||||
const data = await progresoService.getProgreso();
|
||||
setProgresos(data);
|
||||
} catch {
|
||||
// Silenciar error
|
||||
}
|
||||
};
|
||||
|
||||
const getProgresoForEjercicio = (ejercicioId: string) => {
|
||||
return progresos.find(
|
||||
(p) => p.modulo_numero === 3 && p.ejercicio_id === ejercicioId
|
||||
);
|
||||
};
|
||||
|
||||
const handleCompleteEjercicio = async (ejercicioId: string, puntuacion: number) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await progresoService.saveProgreso(ejercicioId, puntuacion);
|
||||
await loadProgreso();
|
||||
} catch {
|
||||
// Silenciar error
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Configuración de ejercicios
|
||||
const ejerciciosConfig: EjercicioConfig[] = [
|
||||
{
|
||||
id: 'calculadora-elasticidad',
|
||||
titulo: 'Calculadora de Elasticidad',
|
||||
descripcion: 'Calcula paso a paso la elasticidad precio, ingreso y cruzada con ejemplos prácticos.',
|
||||
componente: (
|
||||
<CalculadoraElasticidad
|
||||
ejercicioId="calculadora-elasticidad"
|
||||
onComplete={(puntuacion) => handleCompleteEjercicio('calculadora-elasticidad', puntuacion)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'clasificador-bienes',
|
||||
titulo: 'Clasificador de Bienes',
|
||||
descripcion: 'Clasifica bienes según su elasticidad ingreso y cruzada. Identifica normales, inferiores, lujos, sustitutos y complementos.',
|
||||
componente: (
|
||||
<ClasificadorBienes
|
||||
ejercicioId="clasificador-bienes"
|
||||
onComplete={(puntuacion) => handleCompleteEjercicio('clasificador-bienes', puntuacion)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'ejercicios-examen',
|
||||
titulo: 'Ejercicios Tipo Examen',
|
||||
descripcion: 'Resuelve problemas integradores de elasticidad con dificultad de examen.',
|
||||
componente: (
|
||||
<EjerciciosExamen
|
||||
ejercicioId="ejercicios-examen"
|
||||
onComplete={(puntuacion) => handleCompleteEjercicio('ejercicios-examen', puntuacion)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// Estructura de contenido del módulo 3
|
||||
const seccionesContenido: {
|
||||
conceptos: { titulo: string; contenido: ContenidoItem[] };
|
||||
tipos: { titulo: string; contenido: ContenidoItem[] };
|
||||
clasificacion: { titulo: string; contenido: ContenidoItem[] };
|
||||
} = {
|
||||
conceptos: {
|
||||
titulo: 'Conceptos Fundamentales',
|
||||
contenido: [
|
||||
{
|
||||
titulo: conceptosElasticidad.definicionElasticidad.titulo,
|
||||
texto: conceptosElasticidad.definicionElasticidad.definicion,
|
||||
interpretacion: conceptosElasticidad.definicionElasticidad.interpretacionIntuitiva,
|
||||
formula: conceptosElasticidad.definicionElasticidad.formulaGeneral,
|
||||
},
|
||||
{
|
||||
titulo: conceptosElasticidad.elasticidadPrecioDemanda.titulo,
|
||||
texto: conceptosElasticidad.elasticidadPrecioDemanda.definicion,
|
||||
interpretaciones: conceptosElasticidad.elasticidadPrecioDemanda.interpretacion,
|
||||
},
|
||||
{
|
||||
titulo: conceptosElasticidad.determinantesElasticidad.titulo,
|
||||
texto: 'Los siguientes factores determinan la elasticidad de un bien:',
|
||||
factores: conceptosElasticidad.determinantesElasticidad.factores,
|
||||
},
|
||||
{
|
||||
titulo: conceptosElasticidad.relacionIngresoTotal.titulo,
|
||||
texto: conceptosElasticidad.relacionIngresoTotal.definicion,
|
||||
reglas: conceptosElasticidad.relacionIngresoTotal.reglas,
|
||||
},
|
||||
],
|
||||
},
|
||||
tipos: {
|
||||
titulo: 'Tipos de Elasticidad',
|
||||
contenido: [
|
||||
{
|
||||
titulo: 'Elasticidad Precio de la Demanda (Ed)',
|
||||
texto: tiposElasticidad.tipos[0].descripcion,
|
||||
formula: tiposElasticidad.tipos[0].formula,
|
||||
determinantes: tiposElasticidad.tipos[0].determinantes,
|
||||
},
|
||||
{
|
||||
titulo: 'Elasticidad Ingreso de la Demanda (Ei)',
|
||||
texto: tiposElasticidad.tipos[1].descripcion,
|
||||
formula: tiposElasticidad.tipos[1].formula,
|
||||
clasificacion: tiposElasticidad.tipos[1].clasificacionBienes,
|
||||
},
|
||||
{
|
||||
titulo: 'Elasticidad Cruzada (Exy)',
|
||||
texto: tiposElasticidad.tipos[2].descripcion,
|
||||
formula: tiposElasticidad.tipos[2].formula,
|
||||
categorias: tiposElasticidad.tipos[2].clasificacionBienes,
|
||||
},
|
||||
{
|
||||
titulo: 'Elasticidad Precio de la Oferta (Es)',
|
||||
texto: tiposElasticidad.tipos[3].descripcion,
|
||||
formula: tiposElasticidad.tipos[3].formula,
|
||||
interpretacion: tiposElasticidad.tipos[3].interpretacion,
|
||||
},
|
||||
],
|
||||
},
|
||||
clasificacion: {
|
||||
titulo: 'Clasificación de Bienes',
|
||||
contenido: [
|
||||
{
|
||||
titulo: 'Según Elasticidad Ingreso',
|
||||
texto: clasificacionBienes.clasificacionPorIngreso.descripcion,
|
||||
formula: clasificacionBienes.clasificacionPorIngreso.formulaReferencia,
|
||||
categorias: clasificacionBienes.clasificacionPorIngreso.categorias,
|
||||
},
|
||||
{
|
||||
titulo: 'Según Elasticidad Cruzada',
|
||||
texto: clasificacionBienes.clasificacionPorElasticidadCruzada.descripcion,
|
||||
formula: clasificacionBienes.clasificacionPorElasticidadCruzada.formulaReferencia,
|
||||
categorias: clasificacionBienes.clasificacionPorElasticidadCruzada.categorias,
|
||||
},
|
||||
{
|
||||
titulo: 'Matriz de Clasificación Completa',
|
||||
texto: clasificacionBienes.matrizClasificacionCompleta.descripcion,
|
||||
matriz: clasificacionBienes.matrizClasificacionCompleta.matriz,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const currentSeccion = seccionesContenido[activeSeccion];
|
||||
|
||||
// Calcular progreso
|
||||
const ejerciciosCompletados = ejerciciosConfig.filter(
|
||||
(e) => getProgresoForEjercicio(e.id)?.completado
|
||||
).length;
|
||||
const porcentajeProgreso = Math.round((ejerciciosCompletados / ejerciciosConfig.length) * 100);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header */}
|
||||
<header className="bg-white shadow-sm sticky top-0 z-10">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link to="/modulos" className="inline-flex items-center text-blue-600 hover:underline">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Volver a Módulos
|
||||
</Link>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600">Progreso:</span>
|
||||
<div className="w-32 bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-green-500 h-2 rounded-full transition-all"
|
||||
style={{ width: `${porcentajeProgreso}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-700">{porcentajeProgreso}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Título del módulo */}
|
||||
<div className="bg-gradient-to-r from-purple-600 to-purple-800 text-white">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-16 h-16 bg-white/20 rounded-xl flex items-center justify-center text-3xl font-bold">
|
||||
3
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Módulo 3: Elasticidad</h1>
|
||||
<p className="text-purple-100 mt-1">
|
||||
Tipos de elasticidad, clasificación de bienes y análisis de sensibilidad
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<div className="flex gap-2 border-b border-gray-200">
|
||||
{TABS.map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => {
|
||||
setActiveTab(tab);
|
||||
setActiveEjercicio(null);
|
||||
}}
|
||||
className={`px-6 py-3 font-medium text-sm transition-colors relative ${
|
||||
activeTab === tab
|
||||
? 'text-blue-600'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{tab === 'Contenido' && <BookOpen className="w-4 h-4 inline mr-2" />}
|
||||
{tab === 'Ejercicios' && <Trophy className="w-4 h-4 inline mr-2" />}
|
||||
{tab}
|
||||
{activeTab === tab && (
|
||||
<motion.div
|
||||
layoutId="activeTab3"
|
||||
className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Contenido según tab activo */}
|
||||
<AnimatePresence mode="wait">
|
||||
{activeTab === 'Contenido' ? (
|
||||
<motion.div
|
||||
key="contenido"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
className="mt-6 grid grid-cols-1 lg:grid-cols-4 gap-6"
|
||||
>
|
||||
{/* Navegación de secciones */}
|
||||
<div className="lg:col-span-1">
|
||||
<Card className="sticky top-24">
|
||||
<h3 className="font-semibold text-gray-900 mb-4">Secciones</h3>
|
||||
<nav className="space-y-2">
|
||||
{(Object.keys(seccionesContenido) as Array<keyof typeof seccionesContenido>).map((key) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setActiveSeccion(key)}
|
||||
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors flex items-center justify-between ${
|
||||
activeSeccion === key
|
||||
? 'bg-purple-50 text-purple-700 font-medium'
|
||||
: 'text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{seccionesContenido[key].titulo}
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Contenido de la sección */}
|
||||
<div className="lg:col-span-3 space-y-6">
|
||||
<Card>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-6">{currentSeccion.titulo}</h2>
|
||||
<div className="space-y-6">
|
||||
{currentSeccion.contenido.map((item, index) => (
|
||||
<div key={index} className="border-b border-gray-100 last:border-0 pb-6 last:pb-0">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-3">{item.titulo}</h3>
|
||||
<p className="text-gray-600 mb-4 leading-relaxed">{item.texto}</p>
|
||||
|
||||
{/* Mostrar interpretación si existe */}
|
||||
{item.interpretacion && (
|
||||
<div className="bg-blue-50 p-4 rounded-lg mb-4">
|
||||
<p className="text-blue-800 text-sm">{item.interpretacion}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mostrar fórmula si existe */}
|
||||
{item.formula && (
|
||||
<div className="bg-gray-100 p-4 rounded-lg mb-4">
|
||||
<pre className="text-sm text-gray-800 overflow-x-auto">
|
||||
{item.formula.latex || item.formula.ecuacion || item.formula}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mostrar interpretaciones de elasticidad */}
|
||||
{item.interpretaciones && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{item.interpretaciones.map((interp: any, i: number) => (
|
||||
<div key={i} className={`p-3 rounded-lg border ${
|
||||
interp.rango === '|Ed| > 1' || interp.rango === 'Es > 1'
|
||||
? 'bg-green-50 border-green-200'
|
||||
: interp.rango === '|Ed| < 1' || interp.rango === 'Es < 1'
|
||||
? 'bg-yellow-50 border-yellow-200'
|
||||
: 'bg-blue-50 border-blue-200'
|
||||
}`}>
|
||||
<h4 className="font-medium text-gray-800">{interp.clasificacion}</h4>
|
||||
<p className="text-sm text-gray-600">{interp.significado || interp.descripcion}</p>
|
||||
{interp.ejemplo && (
|
||||
<p className="text-sm text-gray-500 mt-1">Ejemplo: {interp.ejemplo}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mostrar factores determinantes */}
|
||||
{item.factores && (
|
||||
<div className="space-y-2">
|
||||
{item.factores.map((factor: any, i: number) => (
|
||||
<div key={i} className="flex items-start gap-3 p-3 bg-gray-50 rounded-lg">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-gray-800">{factor.factor || factor.nombre}</h4>
|
||||
<p className="text-sm text-gray-600">{factor.efecto || factor.descripcion}</p>
|
||||
{factor.explicacion && (
|
||||
<p className="text-sm text-gray-500 mt-1">{factor.explicacion}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mostrar reglas de ingreso total */}
|
||||
{item.reglas && (
|
||||
<div className="space-y-3">
|
||||
{item.reglas.map((regla: any, i: number) => (
|
||||
<div key={i} className="bg-purple-50 p-3 rounded-lg border border-purple-200">
|
||||
<h4 className="font-medium text-purple-900">{regla.elasticidad}</h4>
|
||||
<div className="text-sm text-purple-700 mt-1 space-y-1">
|
||||
<p>Precio ↑: {regla.efectoPrecioArriba}</p>
|
||||
<p>Precio ↓: {regla.efectoPrecioAbajo}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mostrar clasificación de bienes por ingreso */}
|
||||
{item.clasificacion && item.titulo?.includes('Ingreso') && (
|
||||
<div className="space-y-3">
|
||||
{item.clasificacion.map((cat: any, i: number) => (
|
||||
<div key={i} className={`p-3 rounded-lg border ${
|
||||
cat.condicion?.includes('> 0') && !cat.condicion?.includes('<')
|
||||
? 'bg-green-50 border-green-200'
|
||||
: 'bg-red-50 border-red-200'
|
||||
}`}>
|
||||
<h4 className="font-medium text-gray-800">{cat.tipo}</h4>
|
||||
<p className="text-sm text-gray-600">{cat.descripcion}</p>
|
||||
<p className="text-sm font-medium mt-1">Condición: {cat.condicion}</p>
|
||||
{cat.subtipos && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{cat.subtipos.map((sub: any, j: number) => (
|
||||
<div key={j} className="text-sm text-gray-600 pl-3 border-l-2 border-gray-300">
|
||||
<strong>{sub.tipo}:</strong> {sub.rango}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mostrar clasificación por elasticidad cruzada */}
|
||||
{item.categorias && item.titulo?.includes('Cruzada') && (
|
||||
<div className="space-y-3">
|
||||
{item.categorias.map((cat: any, i: number) => (
|
||||
<div key={i} className={`p-3 rounded-lg border ${
|
||||
cat.condicion?.includes('> 0')
|
||||
? 'bg-green-50 border-green-200'
|
||||
: cat.condicion?.includes('< 0')
|
||||
? 'bg-red-50 border-red-200'
|
||||
: 'bg-gray-50 border-gray-200'
|
||||
}`}>
|
||||
<h4 className="font-medium text-gray-800">{cat.tipo}</h4>
|
||||
<p className="text-sm text-gray-600">{cat.descripcion}</p>
|
||||
<p className="text-sm font-medium mt-1">Exy {cat.condicion}</p>
|
||||
{cat.ejemplos && (
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Ejemplos: {cat.ejemplos.slice(0, 3).join(', ')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mostrar matriz de clasificación */}
|
||||
{item.matriz && (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full text-sm">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-700">Combinación</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-700">Ejemplo</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-700">Características</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{item.matriz.map((fila: any, i: number) => (
|
||||
<tr key={i}>
|
||||
<td className="px-3 py-2 font-medium text-gray-800">{fila.combinacion}</td>
|
||||
<td className="px-3 py-2 text-gray-600">{fila.ejemplo}</td>
|
||||
<td className="px-3 py-2 text-gray-600">{fila.caracteristicas}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-purple-50 border-purple-200">
|
||||
<h3 className="font-semibold text-purple-900 mb-3">Ejercicios Relacionados</h3>
|
||||
<p className="text-purple-700 text-sm mb-4">
|
||||
Practica el cálculo y clasificación de elasticidad con ejercicios interactivos
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => setActiveTab('Ejercicios')}
|
||||
variant="outline"
|
||||
className="border-purple-300 text-purple-700 hover:bg-purple-100"
|
||||
>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
Ir a Ejercicios
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="ejercicios"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
className="mt-6"
|
||||
>
|
||||
{activeEjercicio ? (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<Button variant="ghost" onClick={() => setActiveEjercicio(null)}>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Volver a ejercicios
|
||||
</Button>
|
||||
{loading && <span className="text-sm text-gray-500">Guardando progreso...</span>}
|
||||
</div>
|
||||
{ejerciciosConfig.find((e) => e.id === activeEjercicio)?.componente}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{ejerciciosConfig.map((ejercicio, index) => {
|
||||
const progreso = getProgresoForEjercicio(ejercicio.id);
|
||||
const completado = progreso?.completado || false;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={ejercicio.id}
|
||||
className="hover:shadow-lg transition-shadow cursor-pointer"
|
||||
onClick={() => setActiveEjercicio(ejercicio.id)}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div
|
||||
className={`w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0 ${
|
||||
completado ? 'bg-green-100 text-green-600' : 'bg-purple-100 text-purple-600'
|
||||
}`}
|
||||
>
|
||||
{completado ? (
|
||||
<CheckCircle className="w-6 h-6" />
|
||||
) : (
|
||||
<span className="text-xl font-bold">{index + 1}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-gray-900">{ejercicio.titulo}</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">{ejercicio.descripcion}</p>
|
||||
{completado && progreso && (
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<span className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded-full">
|
||||
Completado
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{progreso.puntuacion} pts
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button className="w-full mt-4" size="sm">
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
{completado ? 'Repetir' : 'Comenzar'}
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ejerciciosCompletados === ejerciciosConfig.length && ejerciciosConfig.length > 0 && (
|
||||
<Card className="mt-6 bg-green-50 border-green-200">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<Trophy className="w-6 h-6 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-green-900">¡Felicitaciones!</h3>
|
||||
<p className="text-green-700 text-sm">
|
||||
Has completado todos los ejercicios de este módulo.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Modulo3Page;
|
||||
423
frontend/src/pages/modulos/Modulo4Page.tsx
Normal file
423
frontend/src/pages/modulos/Modulo4Page.tsx
Normal file
@@ -0,0 +1,423 @@
|
||||
// @ts-nocheck
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Card } from '../../components/ui/Card';
|
||||
import { Button } from '../../components/ui/Button';
|
||||
import { progresoService } from '../../services/api';
|
||||
import type { Progreso } from '../../types';
|
||||
import { ArrowLeft, CheckCircle, Play, BookOpen, Trophy, ChevronRight } from 'lucide-react';
|
||||
|
||||
// Importar contenido del módulo 4
|
||||
import { produccion } from '../../content/modulo4/produccion';
|
||||
import { costos } from '../../content/modulo4/costos';
|
||||
import { mercado } from '../../content/modulo4/mercado';
|
||||
|
||||
// Importar componentes de ejercicios
|
||||
import { CalculadoraCostos, SimuladorProduccion, VisualizadorExcedentes } from '../../components/exercises/modulo4';
|
||||
|
||||
const TABS = ['Contenido', 'Ejercicios'] as const;
|
||||
type Tab = typeof TABS[number];
|
||||
|
||||
interface EjercicioConfig {
|
||||
id: string;
|
||||
titulo: string;
|
||||
descripcion: string;
|
||||
componente: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Modulo4Page() {
|
||||
const { numero: _numero } = useParams<{ numero: string }>();
|
||||
const [activeTab, setActiveTab] = useState<Tab>('Contenido');
|
||||
const [activeSeccion, setActiveSeccion] = useState<'produccion' | 'costos' | 'mercado'>('produccion');
|
||||
const [activeEjercicio, setActiveEjercicio] = useState<string | null>(null);
|
||||
const [progresos, setProgresos] = useState<Progreso[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadProgreso();
|
||||
}, []);
|
||||
|
||||
const loadProgreso = async () => {
|
||||
try {
|
||||
const data = await progresoService.getProgreso();
|
||||
setProgresos(data);
|
||||
} catch {
|
||||
// Silenciar error
|
||||
}
|
||||
};
|
||||
|
||||
const getProgresoForEjercicio = (ejercicioId: string) => {
|
||||
return progresos.find(
|
||||
(p) => p.modulo_numero === 4 && p.ejercicio_id === ejercicioId
|
||||
);
|
||||
};
|
||||
|
||||
const handleCompleteEjercicio = async (ejercicioId: string, puntuacion: number) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await progresoService.saveProgreso(ejercicioId, puntuacion);
|
||||
await loadProgreso();
|
||||
} catch {
|
||||
// Silenciar error
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Configuración de ejercicios
|
||||
const ejerciciosConfig: EjercicioConfig[] = [
|
||||
{
|
||||
id: 'calculadora-costos',
|
||||
titulo: 'Calculadora de Costos',
|
||||
descripcion: 'Ingresa CF, CV para cada nivel de producción y calcula automáticamente todos los costos medios y marginales.',
|
||||
componente: (
|
||||
<CalculadoraCostos
|
||||
ejercicioId="calculadora-costos"
|
||||
onComplete={() => handleCompleteEjercicio('calculadora-costos', 100)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'simulador-produccion',
|
||||
titulo: 'Simulador de Producción',
|
||||
descripcion: 'Dado un precio de mercado y curva de costos, encuentra la cantidad óptima y determina si debes producir o cerrar.',
|
||||
componente: (
|
||||
<SimuladorProduccion
|
||||
ejercicioId="simulador-produccion"
|
||||
onComplete={() => handleCompleteEjercicio('simulador-produccion', 100)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'visualizador-excedentes',
|
||||
titulo: 'Visualizador de Excedentes',
|
||||
descripcion: 'Interactúa con el gráfico para ver cómo cambia el excedente del productor al variar el precio y la cantidad.',
|
||||
componente: (
|
||||
<VisualizadorExcedentes
|
||||
ejercicioId="visualizador-excedentes"
|
||||
onComplete={() => handleCompleteEjercicio('visualizador-excedentes', 100)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// Estructura de contenido del módulo 4
|
||||
const seccionesContenido = {
|
||||
produccion: {
|
||||
titulo: produccion.titulo,
|
||||
contenido: produccion.contenido,
|
||||
},
|
||||
costos: {
|
||||
titulo: costos.titulo,
|
||||
contenido: costos.contenido,
|
||||
},
|
||||
mercado: {
|
||||
titulo: mercado.titulo,
|
||||
contenido: mercado.contenido,
|
||||
},
|
||||
};
|
||||
|
||||
const currentSeccion = seccionesContenido[activeSeccion];
|
||||
|
||||
// Calcular progreso
|
||||
const ejerciciosCompletados = ejerciciosConfig.filter(
|
||||
(e) => getProgresoForEjercicio(e.id)?.completado
|
||||
).length;
|
||||
const porcentajeProgreso = Math.round((ejerciciosCompletados / ejerciciosConfig.length) * 100);
|
||||
|
||||
// Función auxiliar para renderizar contenido markdown-like
|
||||
const renderContenido = (texto: string) => {
|
||||
const partes = texto.split(/(\*\*.*?\*\*|\$.*?\$|`.*?`|\n\n)/);
|
||||
return partes.map((parte, index) => {
|
||||
if (parte.startsWith('**') && parte.endsWith('**')) {
|
||||
return <strong key={index}>{parte.slice(2, -2)}</strong>;
|
||||
}
|
||||
if (parte.startsWith('$') && parte.endsWith('$')) {
|
||||
return <code key={index} className="bg-gray-100 px-1 rounded">{parte.slice(1, -1)}</code>;
|
||||
}
|
||||
if (parte.startsWith('`') && parte.endsWith('`')) {
|
||||
return <code key={index} className="bg-gray-100 px-1 rounded text-red-600">{parte.slice(1, -1)}</code>;
|
||||
}
|
||||
if (parte === '\n\n') {
|
||||
return <br key={index} />;
|
||||
}
|
||||
return <span key={index}>{parte}</span>;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header */}
|
||||
<header className="bg-white shadow-sm sticky top-0 z-10">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link to="/modulos" className="inline-flex items-center text-blue-600 hover:underline">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Volver a Módulos
|
||||
</Link>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600">Progreso:</span>
|
||||
<div className="w-32 bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-green-500 h-2 rounded-full transition-all"
|
||||
style={{ width: `${porcentajeProgreso}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-700">{porcentajeProgreso}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Título del módulo */}
|
||||
<div className="bg-gradient-to-r from-orange-600 to-orange-800 text-white">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-16 h-16 bg-white/20 rounded-xl flex items-center justify-center text-3xl font-bold">
|
||||
4
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Módulo 4: Teoría del Productor</h1>
|
||||
<p className="text-orange-100 mt-1">
|
||||
Producción, costos y competencia perfecta
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<div className="flex gap-2 border-b border-gray-200">
|
||||
{TABS.map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => {
|
||||
setActiveTab(tab);
|
||||
setActiveEjercicio(null);
|
||||
}}
|
||||
className={`px-6 py-3 font-medium text-sm transition-colors relative ${
|
||||
activeTab === tab
|
||||
? 'text-blue-600'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{tab === 'Contenido' && <BookOpen className="w-4 h-4 inline mr-2" />}
|
||||
{tab === 'Ejercicios' && <Trophy className="w-4 h-4 inline mr-2" />}
|
||||
{tab}
|
||||
{activeTab === tab && (
|
||||
<motion.div
|
||||
layoutId="activeTab4"
|
||||
className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Contenido según tab activo */}
|
||||
<AnimatePresence mode="wait">
|
||||
{activeTab === 'Contenido' ? (
|
||||
<motion.div
|
||||
key="contenido"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
className="mt-6 grid grid-cols-1 lg:grid-cols-4 gap-6"
|
||||
>
|
||||
{/* Navegación de secciones */}
|
||||
<div className="lg:col-span-1">
|
||||
<Card className="sticky top-24">
|
||||
<h3 className="font-semibold text-gray-900 mb-4">Secciones</h3>
|
||||
<nav className="space-y-2">
|
||||
{(Object.keys(seccionesContenido) as Array<keyof typeof seccionesContenido>).map((key) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setActiveSeccion(key)}
|
||||
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors flex items-center justify-between ${
|
||||
activeSeccion === key
|
||||
? 'bg-orange-50 text-orange-700 font-medium'
|
||||
: 'text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{seccionesContenido[key].titulo}
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Contenido de la sección */}
|
||||
<div className="lg:col-span-3 space-y-6">
|
||||
<Card>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-6">{currentSeccion.titulo}</h2>
|
||||
<div className="space-y-6">
|
||||
{currentSeccion.contenido.map((seccion, index) => (
|
||||
<div key={index} className="border-b border-gray-100 last:border-0 pb-6 last:pb-0">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-3">{seccion.titulo}</h3>
|
||||
<div className="prose prose-orange max-w-none">
|
||||
{seccion.contenido.split('\n\n').map((parrafo, pIndex) => {
|
||||
// Detectar si es una tabla
|
||||
if (parrafo.includes('|') && parrafo.includes('---')) {
|
||||
const lineas = parrafo.split('\n').filter(l => l.trim() && !l.includes('---'));
|
||||
if (lineas.length >= 2) {
|
||||
return (
|
||||
<div key={pIndex} className="overflow-x-auto my-4">
|
||||
<table className="min-w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-50">
|
||||
{lineas[0].split('|').filter(Boolean).map((cell, cIndex) => (
|
||||
<th key={cIndex} className="px-3 py-2 text-left font-medium border">
|
||||
{cell.trim()}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{lineas.slice(1).map((linea, lIndex) => (
|
||||
<tr key={lIndex} className="border-b">
|
||||
{linea.split('|').filter(Boolean).map((cell, cIndex) => (
|
||||
<td key={cIndex} className="px-3 py-2 border">
|
||||
{cell.trim()}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Detectar si es código
|
||||
if (parrafo.startsWith('```')) {
|
||||
return (
|
||||
<pre key={pIndex} className="bg-gray-900 text-white p-4 rounded-lg overflow-x-auto my-4">
|
||||
<code>{parrafo.replace(/```/g, '').trim()}</code>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<p key={pIndex} className="text-gray-600 mb-4 leading-relaxed whitespace-pre-line">
|
||||
{renderContenido(parrafo)}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-orange-50 border-orange-200">
|
||||
<h3 className="font-semibold text-orange-900 mb-3">Ejercicios Relacionados</h3>
|
||||
<p className="text-orange-700 text-sm mb-4">
|
||||
Practica con simuladores interactivos de costos, producción y excedentes
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => setActiveTab('Ejercicios')}
|
||||
variant="outline"
|
||||
className="border-orange-300 text-orange-700 hover:bg-orange-100"
|
||||
>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
Ir a Ejercicios
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="ejercicios"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
className="mt-6"
|
||||
>
|
||||
{activeEjercicio ? (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<Button variant="ghost" onClick={() => setActiveEjercicio(null)}>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Volver a ejercicios
|
||||
</Button>
|
||||
{loading && <span className="text-sm text-gray-500">Guardando progreso...</span>}
|
||||
</div>
|
||||
{ejerciciosConfig.find((e) => e.id === activeEjercicio)?.componente}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{ejerciciosConfig.map((ejercicio, index) => {
|
||||
const progreso = getProgresoForEjercicio(ejercicio.id);
|
||||
const completado = progreso?.completado || false;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={ejercicio.id}
|
||||
className="hover:shadow-lg transition-shadow cursor-pointer"
|
||||
onClick={() => setActiveEjercicio(ejercicio.id)}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div
|
||||
className={`w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0 ${
|
||||
completado ? 'bg-green-100 text-green-600' : 'bg-orange-100 text-orange-600'
|
||||
}`}
|
||||
>
|
||||
{completado ? (
|
||||
<CheckCircle className="w-6 h-6" />
|
||||
) : (
|
||||
<span className="text-xl font-bold">{index + 1}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-gray-900">{ejercicio.titulo}</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">{ejercicio.descripcion}</p>
|
||||
{completado && progreso && (
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<span className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded-full">
|
||||
Completado
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{progreso.puntuacion} pts
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button className="w-full mt-4" size="sm">
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
{completado ? 'Repetir' : 'Comenzar'}
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ejerciciosCompletados === ejerciciosConfig.length && ejerciciosConfig.length > 0 && (
|
||||
<Card className="mt-6 bg-green-50 border-green-200">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<Trophy className="w-6 h-6 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-green-900">¡Felicitaciones!</h3>
|
||||
<p className="text-green-700 text-sm">
|
||||
Has completado todos los ejercicios de este módulo.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Modulo4Page;
|
||||
4
frontend/src/pages/modulos/index.ts
Normal file
4
frontend/src/pages/modulos/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { Modulo1Page } from './Modulo1Page';
|
||||
export { Modulo2Page } from './Modulo2Page';
|
||||
export { Modulo3Page } from './Modulo3Page';
|
||||
export { Modulo4Page } from './Modulo4Page';
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
|
||||
import type { LoginRequest, LoginResponse, Usuario, Progreso, Modulo } from '../types';
|
||||
import type { LoginRequest, LoginResponse, Usuario, Progreso, Modulo, ProgresoModulo, Badge, ResumenProgreso } from '../types';
|
||||
|
||||
const API_BASE_URL = '/api';
|
||||
|
||||
@@ -96,8 +96,8 @@ export const progresoService = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async saveProgreso(progreso: Progreso): Promise<Progreso> {
|
||||
const response = await api.post<Progreso>('/progreso', progreso);
|
||||
async saveProgreso(data: { modulo_numero: number; ejercicio_id: string; puntuacion: number; completado?: boolean }): Promise<Progreso> {
|
||||
const response = await api.post<Progreso>('/progreso', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -105,16 +105,36 @@ export const progresoService = {
|
||||
const response = await api.get<Progreso[]>(`/admin/usuarios/${userId}/progreso`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getResumen(): Promise<ResumenProgreso> {
|
||||
const response = await api.get<ResumenProgreso>('/progreso/resumen');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getResumenProgreso(): Promise<ResumenProgreso> {
|
||||
const response = await api.get<ResumenProgreso>('/progreso/resumen');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getProgresoModulos(): Promise<ProgresoModulo[]> {
|
||||
const response = await api.get<ProgresoModulo[]>('/progreso/modulos');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getBadges(): Promise<Badge[]> {
|
||||
const response = await api.get<Badge[]>('/progreso/badges');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export const moduloService = {
|
||||
async getModulos(): Promise<Modulo[]> {
|
||||
const response = await api.get<Modulo[]>('/modulos');
|
||||
const response = await api.get<Modulo[]>('/contenido/modulos');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getModulo(numero: number): Promise<Modulo> {
|
||||
const response = await api.get<Modulo>(`/modulos/${numero}`);
|
||||
const response = await api.get<Modulo>(`/contenido/modulos/${numero}`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -66,7 +66,10 @@ export const useAuthStore = create<AuthState>()(
|
||||
}),
|
||||
{
|
||||
name: 'auth-storage',
|
||||
partialize: (state) => ({ isAuthenticated: state.isAuthenticated }),
|
||||
partialize: (state) => ({
|
||||
isAuthenticated: state.isAuthenticated,
|
||||
usuario: state.usuario
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
134
frontend/src/stores/progresoStore.ts
Normal file
134
frontend/src/stores/progresoStore.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import type { ProgresoEjercicio, ProgresoModulo, Badge, NivelUsuario } from '../types';
|
||||
import { progresoService } from '../services/api';
|
||||
|
||||
interface ProgresoState {
|
||||
progresoModulos: ProgresoModulo[];
|
||||
badges: Badge[];
|
||||
totalPuntuacion: number;
|
||||
nivel: NivelUsuario;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Actions
|
||||
loadProgreso: () => Promise<void>;
|
||||
saveProgreso: (ejercicioId: string, puntuacion: number, moduloNumero: number) => Promise<void>;
|
||||
getProgresoModulo: (moduloNumero: number) => ProgresoModulo | undefined;
|
||||
getEjercicioCompletado: (ejercicioId: string, moduloNumero: number) => boolean;
|
||||
getPuntuacionEjercicio: (ejercicioId: string, moduloNumero: number) => number;
|
||||
getPorcentajeModulo: (moduloNumero: number, totalEjercicios: number) => number;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
export const useProgresoStore = create<ProgresoState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
progresoModulos: [],
|
||||
badges: [],
|
||||
totalPuntuacion: 0,
|
||||
nivel: 'Novato',
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
loadProgreso: async () => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const resumen = await progresoService.getResumen();
|
||||
set({
|
||||
progresoModulos: resumen.moduloProgreso,
|
||||
badges: resumen.badges,
|
||||
totalPuntuacion: resumen.totalPuntuacion,
|
||||
nivel: resumen.nivel,
|
||||
isLoading: false,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : 'Error al cargar progreso';
|
||||
set({ error: message, isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
saveProgreso: async (ejercicioId: string, puntuacion: number, moduloNumero: number) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
await progresoService.saveProgreso({
|
||||
modulo_numero: moduloNumero,
|
||||
ejercicio_id: ejercicioId,
|
||||
puntuacion,
|
||||
completado: true,
|
||||
});
|
||||
|
||||
// Update local state
|
||||
const { progresoModulos } = get();
|
||||
const moduloIndex = progresoModulos.findIndex(m => m.moduloNumero === moduloNumero);
|
||||
|
||||
if (moduloIndex >= 0) {
|
||||
const nuevoEjercicio: ProgresoEjercicio = {
|
||||
ejercicioId,
|
||||
completado: true,
|
||||
puntuacion,
|
||||
fecha: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const moduloActualizado = { ...progresoModulos[moduloIndex] };
|
||||
const ejercicioExistente = moduloActualizado.ejercicios.findIndex(
|
||||
e => e.ejercicioId === ejercicioId
|
||||
);
|
||||
|
||||
if (ejercicioExistente >= 0) {
|
||||
moduloActualizado.ejercicios[ejercicioExistente] = nuevoEjercicio;
|
||||
} else {
|
||||
moduloActualizado.ejercicios.push(nuevoEjercicio);
|
||||
}
|
||||
|
||||
const nuevosProgresos = [...progresoModulos];
|
||||
nuevosProgresos[moduloIndex] = moduloActualizado;
|
||||
|
||||
set({ progresoModulos: nuevosProgresos, isLoading: false });
|
||||
}
|
||||
|
||||
// Reload full progress to get updated badges and level
|
||||
await get().loadProgreso();
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : 'Error al guardar progreso';
|
||||
set({ error: message, isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
getProgresoModulo: (moduloNumero: number) => {
|
||||
return get().progresoModulos.find(m => m.moduloNumero === moduloNumero);
|
||||
},
|
||||
|
||||
getEjercicioCompletado: (ejercicioId: string, moduloNumero: number) => {
|
||||
const modulo = get().getProgresoModulo(moduloNumero);
|
||||
if (!modulo) return false;
|
||||
const ejercicio = modulo.ejercicios.find(e => e.ejercicioId === ejercicioId);
|
||||
return ejercicio?.completado || false;
|
||||
},
|
||||
|
||||
getPuntuacionEjercicio: (ejercicioId: string, moduloNumero: number) => {
|
||||
const modulo = get().getProgresoModulo(moduloNumero);
|
||||
if (!modulo) return 0;
|
||||
const ejercicio = modulo.ejercicios.find(e => e.ejercicioId === ejercicioId);
|
||||
return ejercicio?.puntuacion || 0;
|
||||
},
|
||||
|
||||
getPorcentajeModulo: (moduloNumero: number, totalEjercicios: number) => {
|
||||
const modulo = get().getProgresoModulo(moduloNumero);
|
||||
if (!modulo || totalEjercicios === 0) return 0;
|
||||
const completados = modulo.ejercicios.filter(e => e.completado).length;
|
||||
return Math.round((completados / totalEjercicios) * 100);
|
||||
},
|
||||
|
||||
clearError: () => set({ error: null }),
|
||||
}),
|
||||
{
|
||||
name: 'progreso-storage',
|
||||
partialize: (state) => ({
|
||||
progresoModulos: state.progresoModulos,
|
||||
totalPuntuacion: state.totalPuntuacion,
|
||||
nivel: state.nivel,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
266
frontend/src/stores/progressStore.ts
Normal file
266
frontend/src/stores/progressStore.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
import { create } from 'zustand';
|
||||
import { progresoService } from '../services/api';
|
||||
import type { Progreso, Badge, NivelUsuario } from '../types';
|
||||
|
||||
export interface EjercicioProgreso {
|
||||
ejercicioId: string;
|
||||
completado: boolean;
|
||||
puntuacion: number;
|
||||
fechaCompletado?: string;
|
||||
intentos: number;
|
||||
}
|
||||
|
||||
export interface ModuloProgreso {
|
||||
moduloId: string;
|
||||
ejercicios: Record<string, EjercicioProgreso>;
|
||||
completado: boolean;
|
||||
}
|
||||
|
||||
interface ProgressState {
|
||||
modulos: Record<string, ModuloProgreso>;
|
||||
puntuacionTotal: number;
|
||||
badges: Badge[];
|
||||
nivel: NivelUsuario;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Acciones
|
||||
loadProgreso: () => Promise<void>;
|
||||
saveProgreso: (moduloId: string, ejercicioId: string, puntuacion: number) => Promise<void>;
|
||||
getProgresoEjercicio: (moduloId: string, ejercicioId: string) => EjercicioProgreso | undefined;
|
||||
getProgresoModulo: (moduloId: string) => ModuloProgreso | undefined;
|
||||
resetProgreso: (moduloId?: string) => void;
|
||||
calcularPorcentajeModulo: (moduloId: string, totalEjercicios: number) => number;
|
||||
getBadgesDesbloqueados: () => Badge[];
|
||||
getBadgesBloqueados: () => Badge[];
|
||||
getProgreso: (moduloId: string, ejercicioId: string) => EjercicioProgreso | undefined;
|
||||
}
|
||||
|
||||
function calcularNivel(puntuacion: number): NivelUsuario {
|
||||
if (puntuacion >= 2000) return 'Maestro';
|
||||
if (puntuacion >= 1000) return 'Experto';
|
||||
if (puntuacion >= 300) return 'Aprendiz';
|
||||
return 'Novato';
|
||||
}
|
||||
|
||||
function transformarProgresoAPI(progresoAPI: Progreso[]): Record<string, ModuloProgreso> {
|
||||
const modulos: Record<string, ModuloProgreso> = {};
|
||||
|
||||
progresoAPI.forEach((p) => {
|
||||
const moduloId = `modulo${p.modulo_numero}`;
|
||||
|
||||
if (!modulos[moduloId]) {
|
||||
modulos[moduloId] = {
|
||||
moduloId,
|
||||
ejercicios: {},
|
||||
completado: false,
|
||||
};
|
||||
}
|
||||
|
||||
modulos[moduloId].ejercicios[p.ejercicio_id] = {
|
||||
ejercicioId: p.ejercicio_id,
|
||||
completado: p.completado,
|
||||
puntuacion: p.puntuacion,
|
||||
fechaCompletado: new Date().toISOString(),
|
||||
intentos: 1,
|
||||
};
|
||||
});
|
||||
|
||||
return modulos;
|
||||
}
|
||||
|
||||
export const useProgressStore = create<ProgressState>()(
|
||||
(set, get) => ({
|
||||
modulos: {},
|
||||
puntuacionTotal: 0,
|
||||
badges: [],
|
||||
nivel: 'Novato',
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
loadProgreso: async () => {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const [progresoData, resumenData] = await Promise.all([
|
||||
progresoService.getProgreso(),
|
||||
progresoService.getResumenProgreso(),
|
||||
]);
|
||||
|
||||
const modulos = transformarProgresoAPI(progresoData);
|
||||
|
||||
set({
|
||||
modulos,
|
||||
puntuacionTotal: resumenData?.totalPuntuacion ?? 0,
|
||||
badges: resumenData?.badges ?? [],
|
||||
nivel: resumenData?.nivel ?? 'Novato',
|
||||
isLoading: false,
|
||||
});
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Error al cargar el progreso',
|
||||
isLoading: false,
|
||||
});
|
||||
console.error('Error loading progreso:', error);
|
||||
}
|
||||
},
|
||||
|
||||
saveProgreso: async (moduloId: string, ejercicioId: string, puntuacion: number) => {
|
||||
const moduloNumero = parseInt(moduloId.replace('modulo', ''), 10);
|
||||
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
// Guardar en la API
|
||||
await progresoService.saveProgreso({
|
||||
modulo_numero: moduloNumero,
|
||||
ejercicio_id: ejercicioId,
|
||||
puntuacion,
|
||||
completado: true,
|
||||
});
|
||||
|
||||
// Actualizar estado local
|
||||
set((state) => {
|
||||
const modulo = state.modulos[moduloId] || {
|
||||
moduloId,
|
||||
ejercicios: {},
|
||||
completado: false,
|
||||
};
|
||||
|
||||
const ejercicioExistente = modulo.ejercicios[ejercicioId];
|
||||
const esMejorPuntuacion = !ejercicioExistente || puntuacion > ejercicioExistente.puntuacion;
|
||||
|
||||
const nuevoEjercicio: EjercicioProgreso = {
|
||||
ejercicioId,
|
||||
completado: true,
|
||||
puntuacion: esMejorPuntuacion ? puntuacion : ejercicioExistente.puntuacion,
|
||||
fechaCompletado: new Date().toISOString(),
|
||||
intentos: (ejercicioExistente?.intentos || 0) + 1,
|
||||
};
|
||||
|
||||
const nuevosEjercicios = {
|
||||
...modulo.ejercicios,
|
||||
[ejercicioId]: nuevoEjercicio,
|
||||
};
|
||||
|
||||
const moduloActualizado: ModuloProgreso = {
|
||||
...modulo,
|
||||
ejercicios: nuevosEjercicios,
|
||||
};
|
||||
|
||||
// Recalcular puntuación total
|
||||
let nuevaPuntuacionTotal = 0;
|
||||
Object.values(state.modulos).forEach((mod) => {
|
||||
if (mod.moduloId !== moduloId) {
|
||||
Object.values(mod.ejercicios).forEach((ej) => {
|
||||
if (ej.completado) {
|
||||
nuevaPuntuacionTotal += ej.puntuacion;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Agregar ejercicios del módulo actual
|
||||
Object.values(nuevosEjercicios).forEach((ej) => {
|
||||
if (ej.completado) {
|
||||
nuevaPuntuacionTotal += ej.puntuacion;
|
||||
}
|
||||
});
|
||||
|
||||
const nuevoNivel = calcularNivel(nuevaPuntuacionTotal);
|
||||
|
||||
return {
|
||||
modulos: {
|
||||
...state.modulos,
|
||||
[moduloId]: moduloActualizado,
|
||||
},
|
||||
puntuacionTotal: nuevaPuntuacionTotal,
|
||||
nivel: nuevoNivel,
|
||||
isLoading: false,
|
||||
};
|
||||
});
|
||||
|
||||
// Recargar resumen para obtener badges actualizados
|
||||
const resumen = await progresoService.getResumenProgreso();
|
||||
set({
|
||||
badges: resumen?.badges ?? [],
|
||||
nivel: resumen?.nivel ?? 'Novato',
|
||||
});
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Error al guardar el progreso',
|
||||
isLoading: false,
|
||||
});
|
||||
console.error('Error saving progreso:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
getProgresoEjercicio: (moduloId: string, ejercicioId: string) => {
|
||||
const state = get();
|
||||
return state.modulos[moduloId]?.ejercicios[ejercicioId];
|
||||
},
|
||||
|
||||
getProgresoModulo: (moduloId: string) => {
|
||||
const state = get();
|
||||
return state.modulos[moduloId];
|
||||
},
|
||||
|
||||
resetProgreso: (moduloId?: string) => {
|
||||
if (moduloId) {
|
||||
set((state) => {
|
||||
const { [moduloId]: _, ...restoModulos } = state.modulos;
|
||||
|
||||
// Recalcular puntuación
|
||||
let nuevaPuntuacionTotal = 0;
|
||||
Object.values(restoModulos).forEach((mod) => {
|
||||
Object.values(mod.ejercicios).forEach((ej) => {
|
||||
if (ej.completado) {
|
||||
nuevaPuntuacionTotal += ej.puntuacion;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
modulos: restoModulos,
|
||||
puntuacionTotal: nuevaPuntuacionTotal,
|
||||
nivel: calcularNivel(nuevaPuntuacionTotal),
|
||||
};
|
||||
});
|
||||
} else {
|
||||
set({
|
||||
modulos: {},
|
||||
puntuacionTotal: 0,
|
||||
nivel: 'Novato',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
calcularPorcentajeModulo: (moduloId: string, totalEjercicios: number): number => {
|
||||
const state = get();
|
||||
const modulo = state.modulos[moduloId];
|
||||
if (!modulo || totalEjercicios === 0) return 0;
|
||||
|
||||
const ejerciciosCompletados = Object.values(modulo.ejercicios).filter(
|
||||
(ej) => ej.completado
|
||||
).length;
|
||||
|
||||
return Math.round((ejerciciosCompletados / totalEjercicios) * 100);
|
||||
},
|
||||
|
||||
getBadgesDesbloqueados: () => {
|
||||
return get().badges.filter(b => b.desbloqueado);
|
||||
},
|
||||
|
||||
getBadgesBloqueados: () => {
|
||||
return get().badges.filter(b => !b.desbloqueado);
|
||||
},
|
||||
|
||||
getProgreso: (moduloId: string, ejercicioId: string) => {
|
||||
const state = get();
|
||||
return state.modulos[moduloId]?.ejercicios[ejercicioId];
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default useProgressStore;
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user