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 := protected.Group("/progreso")
|
||||||
{
|
{
|
||||||
progreso.GET("", progresoHandler.GetProgreso)
|
progreso.GET("", progresoHandler.GetProgreso)
|
||||||
progreso.GET("/modulo/:numero", progresoHandler.GetProgresoModulo)
|
progreso.POST("", progresoHandler.SaveProgreso)
|
||||||
progreso.PUT("/:ejercicioId", progresoHandler.UpdateProgreso)
|
|
||||||
progreso.GET("/resumen", progresoHandler.GetResumen)
|
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) {
|
func seedEjercicios(ctx context.Context, pool *pgxpool.Pool) {
|
||||||
ejercicios := []struct {
|
ejercicios := []struct {
|
||||||
ID string
|
ID string
|
||||||
ModuloNumero int
|
ModuloNumero int
|
||||||
Titulo string
|
Titulo string
|
||||||
Tipo string
|
Tipo string
|
||||||
Contenido string
|
Contenido string
|
||||||
Orden int
|
Orden int
|
||||||
}{
|
}{
|
||||||
// Módulo 1
|
// Módulo 1
|
||||||
{"m1e1", 1, "Simulador de Disyuntivas", "interactivo", `{"tipo":"slider","descripcion":"Elige cuanto producir de cada bien"}`, 1},
|
{"m1e1", 1, "Simulador de Disyuntivas", "interactivo", `{"tipo":"slider","descripcion":"Elige cuanto producir de cada bien"}`, 1},
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -34,7 +33,7 @@ func (h *ProgresoHandler) GetProgreso(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
progresos, err := h.progresoRepo.GetByUsuario(c.Request.Context(), userID.(uuid.UUID))
|
progresos, err := h.progresoRepo.GetProgresoByUsuarioID(userID.(uuid.UUID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al obtener progreso"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al obtener progreso"})
|
||||||
return
|
return
|
||||||
@@ -47,72 +46,32 @@ func (h *ProgresoHandler) GetProgreso(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, progresos)
|
c.JSON(http.StatusOK, progresos)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProgresoModulo godoc
|
// SaveProgreso godoc
|
||||||
// @Summary Obtener progreso por módulo
|
// @Summary Guardar/actualizar progreso
|
||||||
// @Description Obtiene el progreso del usuario en un módulo específico
|
// @Description Guarda o actualiza el progreso de un ejercicio
|
||||||
// @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
|
|
||||||
// @Tags progreso
|
// @Tags progreso
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param ejercicioId path int true "ID del ejercicio"
|
// @Param progreso body models.Progreso true "Datos del progreso"
|
||||||
// @Param progreso body models.ProgresoUpdate true "Datos del progreso"
|
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Success 200 {object} map[string]string
|
// @Success 200 {object} map[string]string
|
||||||
// @Router /api/progreso/{ejercicioId} [put]
|
// @Router /api/progreso [post]
|
||||||
func (h *ProgresoHandler) UpdateProgreso(c *gin.Context) {
|
func (h *ProgresoHandler) SaveProgreso(c *gin.Context) {
|
||||||
userID, exists := c.Get("user_id")
|
userID, exists := c.Get("user_id")
|
||||||
if !exists {
|
if !exists {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "No autorizado"})
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "No autorizado"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ejercicioID, err := strconv.Atoi(c.Param("ejercicioId"))
|
var progreso models.Progreso
|
||||||
if err != nil {
|
if err := c.ShouldBindJSON(&progreso); 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 {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al guardar progreso: " + err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al guardar progreso: " + err.Error()})
|
||||||
return
|
return
|
||||||
@@ -123,11 +82,11 @@ func (h *ProgresoHandler) UpdateProgreso(c *gin.Context) {
|
|||||||
|
|
||||||
// GetResumen godoc
|
// GetResumen godoc
|
||||||
// @Summary Obtener resumen
|
// @Summary Obtener resumen
|
||||||
// @Description Obtiene estadísticas del progreso del usuario
|
// @Description Obtiene estadísticas del progreso del usuario (puntos totales, etc.)
|
||||||
// @Tags progreso
|
// @Tags progreso
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Success 200 {object} models.ProgresoResumen
|
// @Success 200 {object} models.ResumenProgreso
|
||||||
// @Router /api/progreso/resumen [get]
|
// @Router /api/progreso/resumen [get]
|
||||||
func (h *ProgresoHandler) GetResumen(c *gin.Context) {
|
func (h *ProgresoHandler) GetResumen(c *gin.Context) {
|
||||||
userID, exists := c.Get("user_id")
|
userID, exists := c.Get("user_id")
|
||||||
@@ -136,7 +95,7 @@ func (h *ProgresoHandler) GetResumen(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resumen, err := h.progresoRepo.GetResumen(c.Request.Context(), userID.(uuid.UUID))
|
resumen, err := h.progresoRepo.GetResumen(userID.(uuid.UUID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al obtener resumen"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al obtener resumen"})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ func (h *UsersHandler) GetUserProgreso(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
progresos, err := h.progresoRepo.GetByUsuarioID(c.Request.Context(), id)
|
progresos, err := h.progresoRepo.GetProgresoByUsuarioID(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al obtener progreso"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error al obtener progreso"})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -7,15 +7,31 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Progreso struct {
|
type Progreso struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
UsuarioID uuid.UUID `json:"usuario_id"`
|
UsuarioID uuid.UUID `json:"usuario_id"`
|
||||||
ModuloNumero int `json:"modulo_numero"`
|
ModuloNumero int `json:"modulo_numero"`
|
||||||
EjercicioID int `json:"ejercicio_id"`
|
EjercicioID string `json:"ejercicio_id"`
|
||||||
Completado bool `json:"completado"`
|
Completado bool `json:"completado"`
|
||||||
Puntuacion int `json:"puntuacion"`
|
Puntuacion int `json:"puntuacion"`
|
||||||
Intentos int `json:"intentos"`
|
Intentos int `json:"intentos"`
|
||||||
UltimaVez time.Time `json:"ultima_vez"`
|
UltimaVez time.Time `json:"ultima_vez"`
|
||||||
RespuestaJSON string `json:"respuesta_json,omitempty"`
|
}
|
||||||
|
|
||||||
|
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 {
|
type ProgresoUpdate struct {
|
||||||
@@ -25,8 +41,8 @@ type ProgresoUpdate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ProgresoResumen struct {
|
type ProgresoResumen struct {
|
||||||
TotalEjercicios int `json:"total_ejercicios"`
|
TotalEjercicios int `json:"total_ejercicios"`
|
||||||
EjerciciosCompletados int `json:"ejercicios_completados"`
|
EjerciciosCompletados int `json:"ejercicios_completados"`
|
||||||
PromedioPuntuacion int `json:"promedio_puntuacion"`
|
PromedioPuntuacion int `json:"promedio_puntuacion"`
|
||||||
ModulosCompletados int `json:"modulos_completados"`
|
ModulosCompletados int `json:"modulos_completados"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ func NewProgresoRepository(db *pgxpool.Pool) *ProgresoRepository {
|
|||||||
return &ProgresoRepository{db: db}
|
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 := `
|
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
|
FROM progreso_usuario WHERE usuario_id = $1
|
||||||
ORDER BY ultima_vez DESC
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ func (r *ProgresoRepository) GetByUsuario(ctx context.Context, usuarioID uuid.UU
|
|||||||
var p models.Progreso
|
var p models.Progreso
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&p.ID, &p.UsuarioID, &p.ModuloNumero, &p.EjercicioID,
|
&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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -43,99 +43,80 @@ func (r *ProgresoRepository) GetByUsuario(ctx context.Context, usuarioID uuid.UU
|
|||||||
return progresos, nil
|
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 := `
|
query := `
|
||||||
SELECT id, usuario_id, modulo_numero, ejercicio_id, completado, puntuacion, intentos, ultima_vez, respuesta_json
|
INSERT INTO progreso_usuario (id, usuario_id, modulo_numero, ejercicio_id, completado, puntuacion, intentos, ultima_vez)
|
||||||
FROM progreso_usuario WHERE usuario_id = $1 AND modulo_numero = $2
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
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)
|
|
||||||
ON CONFLICT (usuario_id, modulo_numero, ejercicio_id)
|
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 progreso.ID == uuid.Nil {
|
||||||
if err != nil {
|
progreso.ID = uuid.New()
|
||||||
return err
|
}
|
||||||
|
if progreso.UltimaVez.IsZero() {
|
||||||
|
progreso.UltimaVez = time.Now()
|
||||||
|
}
|
||||||
|
if progreso.Intentos == 0 {
|
||||||
|
progreso.Intentos = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
existing, _ := r.GetByEjercicio(ctx, usuarioID, ejercicioID)
|
_, err := r.db.Exec(context.Background(), query,
|
||||||
var intentos int
|
progreso.ID, progreso.UsuarioID, progreso.ModuloNumero, progreso.EjercicioID,
|
||||||
if existing != nil {
|
progreso.Completado, progreso.Puntuacion, progreso.Intentos, progreso.UltimaVez)
|
||||||
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)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ProgresoRepository) getModuloByEjercicio(ctx context.Context, ejercicioID int) (int, error) {
|
func (r *ProgresoRepository) GetResumen(usuarioID uuid.UUID) (*models.ResumenProgreso, 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) {
|
|
||||||
query := `
|
query := `
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(DISTINCT ejercicio_id) as total,
|
COALESCE(SUM(puntuacion), 0) as puntos_totales,
|
||||||
COUNT(CASE WHEN completado THEN 1 END) as completados,
|
COUNT(CASE WHEN completado THEN 1 END) as ejercicios_completados,
|
||||||
COALESCE(AVG(CASE WHEN completado THEN puntuacion END), 0)::int as promedio,
|
COUNT(*) as total_ejercicios
|
||||||
COUNT(DISTINCT CASE WHEN completado THEN modulo_numero END) as modulos
|
|
||||||
FROM progreso_usuario WHERE usuario_id = $1
|
FROM progreso_usuario WHERE usuario_id = $1
|
||||||
`
|
`
|
||||||
var resumen models.ProgresoResumen
|
var resumen models.ResumenProgreso
|
||||||
err := r.db.QueryRow(ctx, query, usuarioID).Scan(
|
err := r.db.QueryRow(context.Background(), query, usuarioID).Scan(
|
||||||
&resumen.TotalEjercicios, &resumen.EjerciciosCompletados,
|
&resumen.PuntosTotales, &resumen.EjerciciosCompletados, &resumen.TotalEjercicios)
|
||||||
&resumen.PromedioPuntuacion, &resumen.ModulosCompletados)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
return &resumen, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ProgresoRepository) GetByUsuarioID(ctx context.Context, usuarioID uuid.UUID) ([]models.Progreso, error) {
|
func calcularNivel(puntuacion int) string {
|
||||||
return r.GetByUsuario(ctx, usuarioID)
|
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",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
|
"framer-motion": "^12.34.0",
|
||||||
"lucide-react": "^0.294.0",
|
"lucide-react": "^0.294.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@@ -1809,6 +1810,33 @@
|
|||||||
"url": "https://github.com/sponsors/rawify"
|
"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": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@@ -2153,6 +2181,21 @@
|
|||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -2850,6 +2893,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"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": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
|
"framer-motion": "^12.34.0",
|
||||||
"lucide-react": "^0.294.0",
|
"lucide-react": "^0.294.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^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 { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import { useAuthStore } from './stores/authStore';
|
import { useAuthStore } from './stores/authStore';
|
||||||
import { Login } from './pages/Login';
|
import { Login } from './pages/Login';
|
||||||
@@ -5,6 +6,7 @@ import { Dashboard } from './pages/Dashboard';
|
|||||||
import { Modulos } from './pages/Modulos';
|
import { Modulos } from './pages/Modulos';
|
||||||
import { Modulo } from './pages/Modulo';
|
import { Modulo } from './pages/Modulo';
|
||||||
import { AdminPanel } from './pages/admin/AdminPanel';
|
import { AdminPanel } from './pages/admin/AdminPanel';
|
||||||
|
import { RecursosPage } from './pages/Recursos';
|
||||||
|
|
||||||
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||||
const { isAuthenticated, isLoading } = useAuthStore();
|
const { isAuthenticated, isLoading } = useAuthStore();
|
||||||
@@ -25,6 +27,12 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const { checkAuth } = useAuthStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkAuth();
|
||||||
|
}, [checkAuth]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
@@ -53,6 +61,38 @@ function App() {
|
|||||||
</ProtectedRoute>
|
</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
|
<Route
|
||||||
path="/admin"
|
path="/admin"
|
||||||
element={
|
element={
|
||||||
@@ -61,6 +101,14 @@ function App() {
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/recursos"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<RecursosPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</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 {
|
interface CardProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Card({ children, className = '' }: CardProps) {
|
export function Card({ children, className = '', onClick }: CardProps) {
|
||||||
return (
|
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}
|
{children}
|
||||||
</div>
|
</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 { Link } from 'react-router-dom';
|
||||||
import { useAuthStore } from '../stores/authStore';
|
import { useAuthStore } from '../stores/authStore';
|
||||||
|
import { useProgressStore } from '../stores/progressStore';
|
||||||
import { Card } from '../components/ui/Card';
|
import { Card } from '../components/ui/Card';
|
||||||
import { Button } from '../components/ui/Button';
|
import { Button } from '../components/ui/Button';
|
||||||
import { progresoService } from '../services/api';
|
import { ProgressBar } from '../components/progress/ProgressBar';
|
||||||
import type { ModuloProgreso } from '../types';
|
import { ScoreDisplay } from '../components/progress/ScoreDisplay';
|
||||||
import { BookOpen, TrendingUp, User, LogOut, LayoutGrid } from 'lucide-react';
|
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 = [
|
const MODULOS_CONFIG = [
|
||||||
{ numero: 1, titulo: 'Fundamentos de Economía', descripcion: 'Introducción a los conceptos básicos' },
|
{ id: 'modulo1', numero: 1, titulo: 'Fundamentos de Economía', descripcion: 'Introducción a los conceptos básicos', totalEjercicios: 3 },
|
||||||
{ numero: 2, titulo: 'Oferta, Demanda y Equilibrio', descripcion: 'Curvas de mercado' },
|
{ id: 'modulo2', numero: 2, titulo: 'Oferta, Demanda y Equilibrio', descripcion: 'Curvas de mercado', totalEjercicios: 3 },
|
||||||
{ numero: 3, titulo: 'Utilidad y Elasticidad', descripcion: 'Teoría del consumidor' },
|
{ id: 'modulo3', numero: 3, titulo: 'Utilidad y Elasticidad', descripcion: 'Teoría del consumidor', totalEjercicios: 3 },
|
||||||
{ numero: 4, titulo: 'Teoría del Productor', descripcion: 'Costos y producción' },
|
{ id: 'modulo4', numero: 4, titulo: 'Teoría del Productor', descripcion: 'Costos y producción', totalEjercicios: 3 },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Dashboard() {
|
export function Dashboard() {
|
||||||
const { usuario, logout } = useAuthStore();
|
const { usuario, logout } = useAuthStore();
|
||||||
const [modulosProgreso, setModulosProgreso] = useState<ModuloProgreso[]>([]);
|
const {
|
||||||
|
puntuacionTotal,
|
||||||
|
nivel,
|
||||||
|
calcularPorcentajeModulo,
|
||||||
|
getBadgesDesbloqueados,
|
||||||
|
getBadgesBloqueados,
|
||||||
|
modulos,
|
||||||
|
loadProgreso,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
} = useProgressStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadProgreso();
|
loadProgreso();
|
||||||
}, []);
|
}, [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,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
await logout();
|
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(
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<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="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="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" />
|
<BookOpen className="w-5 h-5 text-white" />
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex items-center gap-2 text-gray-600">
|
<div className="flex items-center gap-2 text-gray-600">
|
||||||
<User className="w-5 h-5" />
|
<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' && (
|
{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
|
Admin
|
||||||
</span>
|
</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">
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h2 className="text-2xl font-bold text-gray-900">Tu progreso</h2>
|
<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>
|
</div>
|
||||||
|
|
||||||
<Card className="mb-8">
|
{/* Stats Cards */}
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||||
<div>
|
<Card className="bg-gradient-to-br from-blue-500 to-blue-600 text-white border-none">
|
||||||
<h3 className="font-semibold text-gray-900">Progreso total</h3>
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-sm text-gray-500">{totalProgreso}% completado</p>
|
<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>
|
||||||
<div className="text-3xl font-bold text-primary">{totalProgreso}%</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-gray-200 rounded-full h-3">
|
|
||||||
<div
|
{/* Columna derecha - Logros */}
|
||||||
className="bg-primary h-3 rounded-full transition-all duration-500"
|
<div>
|
||||||
style={{ width: `${totalProgreso}%` }}
|
<BadgesSection
|
||||||
|
badgesDesbloqueados={badgesDesbloqueados}
|
||||||
|
badgesBloqueados={badgesBloqueados}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
|
|||||||
@@ -1,63 +1,213 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useParams, Link } from 'react-router-dom';
|
import { useParams, Link } from 'react-router-dom';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
import { Card } from '../components/ui/Card';
|
import { Card } from '../components/ui/Card';
|
||||||
import { Button } from '../components/ui/Button';
|
import { Button } from '../components/ui/Button';
|
||||||
import { progresoService } from '../services/api';
|
import { Loader } from '../components/ui/Loader';
|
||||||
import type { Progreso } from '../types';
|
import { useProgressStore } from '../stores/progressStore';
|
||||||
import { ArrowLeft, CheckCircle, Play } from 'lucide-react';
|
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 }> = {
|
// Importar ejercicios reales
|
||||||
1: { titulo: 'Fundamentos de Economía', descripcion: 'Introducción a los conceptos básicos de economía' },
|
import { FlujoCircular } from '../components/exercises/modulo1/FlujoCircular';
|
||||||
2: { titulo: 'Oferta, Demanda y Equilibrio', descripcion: 'Curvas de oferta y demanda en el mercado' },
|
import { QuizBienes } from '../components/exercises/modulo1/QuizBienes';
|
||||||
3: { titulo: 'Utilidad y Elasticidad', descripcion: 'Teoría del consumidor y elasticidades' },
|
import { SimuladorDisyuntivas } from '../components/exercises/modulo1/SimuladorDisyuntivas';
|
||||||
4: { titulo: 'Teoría del Productor', descripcion: 'Costos de producción y competencia perfecta' },
|
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 = [
|
const EJERCICIOS_POR_MODULO: Record<number, Array<{
|
||||||
{ id: 'e1', titulo: 'Conceptos básicos', descripcion: 'Repasa los fundamentos de la economía' },
|
id: string;
|
||||||
{ id: 'e2', titulo: 'Agentes económicos', descripcion: 'Identifica los diferentes agentes en la economía' },
|
titulo: string;
|
||||||
{ id: 'e3', titulo: 'Factores de producción', descripcion: 'Aprende sobre tierra, trabajo y capital' },
|
descripcion: string;
|
||||||
{ id: 'e4', titulo: 'Flujo circular', descripcion: 'Comprende el flujo de bienes y dinero' },
|
componente: React.ComponentType<{ ejercicioId: string; onComplete?: (puntuacion: number) => void }>;
|
||||||
{ id: 'e5', titulo: 'Evaluación final', descripcion: 'Pon a prueba todo lo aprendido' },
|
}>> = {
|
||||||
];
|
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() {
|
export function Modulo() {
|
||||||
const { numero } = useParams<{ numero: string }>();
|
const { numero } = useParams<{ numero: string }>();
|
||||||
const num = parseInt(numero || '1', 10);
|
const num = parseInt(numero || '1', 10);
|
||||||
const [progresos, setProgresos] = useState<Progreso[]>([]);
|
|
||||||
|
|
||||||
const moduloInfo = MODULOS_INFO[num] || MODULOS_INFO[1];
|
const {
|
||||||
const ejercicios = EJERCICIOS_MOCK;
|
puntuacionTotal,
|
||||||
|
getProgresoEjercicio,
|
||||||
|
saveProgreso,
|
||||||
|
calcularPorcentajeModulo,
|
||||||
|
loadProgreso,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
} = useProgressStore();
|
||||||
|
|
||||||
|
const [ejercicioActivo, setEjercicioActivo] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadProgreso();
|
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 {
|
try {
|
||||||
const data = await progresoService.getProgreso();
|
await saveProgreso(moduloInfo.id, ejercicioId, puntuacion);
|
||||||
setProgresos(data);
|
setEjercicioActivo(null);
|
||||||
} catch {
|
} catch (err) {
|
||||||
// Silencio
|
console.error('Error al guardar progreso:', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProgresoForEjercicio = (ejercicioId: string) => {
|
const completados = ejercicios.filter(
|
||||||
return progresos.find(
|
(e) => getProgresoEjercicioLocal(e.id)?.completado
|
||||||
(p) => p.modulo_numero === num && p.ejercicio_id === ejercicioId
|
).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(
|
if (ejercicioActivo) {
|
||||||
(e) => getProgresoForEjercicio(e.id)?.completado
|
const ejercicio = ejercicios.find(e => e.id === ejercicioActivo);
|
||||||
).length;
|
if (!ejercicio) return null;
|
||||||
const porcentaje = Math.round((completados / ejercicios.length) * 100);
|
|
||||||
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<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">
|
<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" />
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||||
Volver al Dashboard
|
Volver al Dashboard
|
||||||
</Link>
|
</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">
|
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center gap-4 mb-4">
|
<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}
|
{num}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -76,76 +226,153 @@ export function Modulo() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="bg-gradient-to-r from-primary to-blue-600 text-white">
|
<Card className={`bg-gradient-to-r ${moduloInfo.color} text-white border-none`}>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div>
|
<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>
|
<p className="text-3xl font-bold mt-1">{porcentaje}%</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<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>
|
</div>
|
||||||
<div className="mt-4 w-full bg-white/20 rounded-full h-2">
|
|
||||||
<div
|
<div className="w-full bg-white/20 rounded-full h-3 overflow-hidden">
|
||||||
className="bg-white h-2 rounded-full transition-all"
|
<motion.div
|
||||||
style={{ width: `${porcentaje}%` }}
|
className="bg-white h-full rounded-full"
|
||||||
|
initial={{ width: 0 }}
|
||||||
|
animate={{ width: `${porcentaje}%` }}
|
||||||
|
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</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) => {
|
{ejercicios.map((ejercicio, index) => {
|
||||||
const progreso = getProgresoForEjercicio(ejercicio.id);
|
const progreso = getProgresoEjercicioLocal(ejercicio.id);
|
||||||
const completado = progreso?.completado || false;
|
const completado = progreso?.completado || false;
|
||||||
|
const bloqueado = isEjercicioBloqueado(index);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card key={ejercicio.id} className="hover:shadow-md transition-shadow">
|
<motion.div
|
||||||
<div className="flex items-center gap-4">
|
key={ejercicio.id}
|
||||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
initial={{ opacity: 0, x: -20 }}
|
||||||
completado ? 'bg-success text-white' : 'bg-gray-100 text-gray-500'
|
animate={{ opacity: 1, x: 0 }}
|
||||||
}`}>
|
transition={{ delay: index * 0.1 }}
|
||||||
{completado ? (
|
>
|
||||||
<CheckCircle className="w-5 h-5" />
|
<Card className={`transition-all ${
|
||||||
) : (
|
bloqueado
|
||||||
<span className="font-medium">{index + 1}</span>
|
? 'opacity-60 bg-gray-50'
|
||||||
)}
|
: 'hover:shadow-md cursor-pointer'
|
||||||
</div>
|
}`}
|
||||||
|
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">
|
<div className="flex-1">
|
||||||
<h3 className="font-semibold text-gray-900">{ejercicio.titulo}</h3>
|
<h3 className={`font-semibold ${
|
||||||
<p className="text-sm text-gray-500">{ejercicio.descripcion}</p>
|
completado ? 'text-gray-900' : bloqueado ? 'text-gray-500' : 'text-gray-900'
|
||||||
</div>
|
}`}>
|
||||||
|
{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">
|
<Button
|
||||||
<Play className="w-4 h-4 mr-2" />
|
size="sm"
|
||||||
{completado ? 'Repetir' : 'Comenzar'}
|
disabled={bloqueado}
|
||||||
</Button>
|
variant={completado ? 'outline' : 'primary'}
|
||||||
</div>
|
onClick={(e) => {
|
||||||
</Card>
|
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>
|
</div>
|
||||||
|
|
||||||
{porcentaje === 100 && (
|
{porcentaje === 100 && (
|
||||||
<Card className="mt-6 bg-success/10 border border-success">
|
<motion.div
|
||||||
<div className="flex items-center gap-4">
|
initial={{ opacity: 0, y: 20 }}
|
||||||
<div className="w-12 h-12 bg-success rounded-full flex items-center justify-center">
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<CheckCircle className="w-6 h-6 text-white" />
|
>
|
||||||
|
<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>
|
||||||
<div>
|
</Card>
|
||||||
<h3 className="font-semibold text-success">¡Felicitaciones!</h3>
|
</motion.div>
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
Has completado todos los ejercicios de este módulo.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Modulo;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Card } from '../components/ui/Card';
|
import { Card } from '../components/ui/Card';
|
||||||
import { Button } from '../components/ui/Button';
|
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 = [
|
const MODULOS = [
|
||||||
{
|
{
|
||||||
@@ -9,28 +10,45 @@ const MODULOS = [
|
|||||||
titulo: 'Fundamentos de Economía',
|
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.',
|
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'],
|
temas: ['Definición de economía', 'Agentes económicos', 'Factores de producción', 'Flujo circular'],
|
||||||
|
totalEjercicios: 5,
|
||||||
|
bloqueado: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
numero: 2,
|
numero: 2,
|
||||||
titulo: 'Oferta, Demanda y Equilibrio',
|
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.',
|
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'],
|
temas: ['Curva de demanda', 'Curva de oferta', 'Equilibrio de mercado', 'Controles de precios'],
|
||||||
|
totalEjercicios: 5,
|
||||||
|
bloqueado: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
numero: 3,
|
numero: 3,
|
||||||
titulo: 'Utilidad y Elasticidad',
|
titulo: 'Utilidad y Elasticidad',
|
||||||
descripcion: 'Explora la teoría del consumidor, aprende a calcular elasticidades y clasifica diferentes tipos de bienes.',
|
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'],
|
temas: ['Utilidad marginal', 'Elasticidad precio', 'Elasticidad ingreso', 'Clasificación de bienes'],
|
||||||
|
totalEjercicios: 5,
|
||||||
|
bloqueado: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
numero: 4,
|
numero: 4,
|
||||||
titulo: 'Teoría del Productor',
|
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.',
|
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'],
|
temas: ['Costos de producción', 'Producción y costos', 'Competencia perfecta', 'Maximización de beneficios'],
|
||||||
|
totalEjercicios: 5,
|
||||||
|
bloqueado: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Modulos() {
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
<header className="bg-white shadow-sm">
|
<header className="bg-white shadow-sm">
|
||||||
@@ -52,42 +70,90 @@ export function Modulos() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{MODULOS.map((modulo) => (
|
{MODULOS.map((modulo) => {
|
||||||
<Card key={modulo.numero} className="hover:shadow-lg transition-shadow">
|
const { completados, porcentaje } = getModuloProgress(modulo.numero, modulo.totalEjercicios);
|
||||||
<div className="flex flex-col md:flex-row md:items-center gap-6">
|
const estaCompletado = porcentaje === 100;
|
||||||
<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">
|
return (
|
||||||
{modulo.numero}
|
<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>
|
</div>
|
||||||
|
</Card>
|
||||||
<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>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</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 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';
|
const API_BASE_URL = '/api';
|
||||||
|
|
||||||
@@ -96,8 +96,8 @@ export const progresoService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async saveProgreso(progreso: Progreso): Promise<Progreso> {
|
async saveProgreso(data: { modulo_numero: number; ejercicio_id: string; puntuacion: number; completado?: boolean }): Promise<Progreso> {
|
||||||
const response = await api.post<Progreso>('/progreso', progreso);
|
const response = await api.post<Progreso>('/progreso', data);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -105,16 +105,36 @@ export const progresoService = {
|
|||||||
const response = await api.get<Progreso[]>(`/admin/usuarios/${userId}/progreso`);
|
const response = await api.get<Progreso[]>(`/admin/usuarios/${userId}/progreso`);
|
||||||
return response.data;
|
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 = {
|
export const moduloService = {
|
||||||
async getModulos(): Promise<Modulo[]> {
|
async getModulos(): Promise<Modulo[]> {
|
||||||
const response = await api.get<Modulo[]>('/modulos');
|
const response = await api.get<Modulo[]>('/contenido/modulos');
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getModulo(numero: number): Promise<Modulo> {
|
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;
|
return response.data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -66,7 +66,10 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'auth-storage',
|
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;
|
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 {
|
export interface Ejercicio {
|
||||||
id: string;
|
id: string;
|
||||||
titulo: string;
|
titulo: string;
|
||||||
descripcion: string;
|
descripcion: string;
|
||||||
tipo: 'quiz' | 'simulador' | 'ejercicio';
|
tipo: 'quiz' | 'simulador' | 'ejercicio';
|
||||||
|
configuracion?: ConfiguracionEjercicio;
|
||||||
|
moduloNumero: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Modulo {
|
export interface Modulo {
|
||||||
@@ -36,6 +77,16 @@ export interface ModuloProgreso {
|
|||||||
totalEjercicios: number;
|
totalEjercicios: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ResumenProgreso {
|
||||||
|
totalPuntuacion: number;
|
||||||
|
ejerciciosCompletados: number;
|
||||||
|
totalEjercicios: number;
|
||||||
|
porcentajeGlobal: number;
|
||||||
|
badges: Badge[];
|
||||||
|
nivel: NivelUsuario;
|
||||||
|
moduloProgreso: ProgresoModulo[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface LoginRequest {
|
export interface LoginRequest {
|
||||||
email?: string;
|
email?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user