Browse Source

implement daily log

Matthew Trejo 2 months ago
parent
commit
0f697ae354
39 changed files with 3811 additions and 16 deletions
  1. 192 0
      docs/DAILY_LOG_COMPLETE.md
  2. 215 0
      docs/DAILY_LOG_IMPLEMENTATION.md
  3. 472 9
      package-lock.json
  4. 2 0
      package.json
  5. 27 0
      prisma/migrations/20251016100748_add_daily_log/migration.sql
  6. 25 0
      prisma/schema.prisma
  7. 198 0
      src/app/api/daily-log/[date]/route.ts
  8. 156 0
      src/app/api/daily-log/route.ts
  9. 157 0
      src/app/api/daily-log/stats/route.ts
  10. 289 0
      src/app/daily-log/page.tsx
  11. 71 0
      src/components/daily-log/CalendarDay.tsx
  12. 53 0
      src/components/daily-log/CalendarHeader.tsx
  13. 170 0
      src/components/daily-log/DailyLogCalendar.tsx
  14. 110 0
      src/components/daily-log/DailyLogCard.tsx
  15. 28 0
      src/components/daily-log/DailyLogEmptyState.tsx
  16. 121 0
      src/components/daily-log/DailyLogEntryForm.tsx
  17. 85 0
      src/components/daily-log/DailyLogFilters.tsx
  18. 60 0
      src/components/daily-log/DailyLogHeader.tsx
  19. 43 0
      src/components/daily-log/DailyLogList.tsx
  20. 62 0
      src/components/daily-log/DailyLogSkeleton.tsx
  21. 124 0
      src/components/daily-log/DailyLogStats.tsx
  22. 39 0
      src/components/daily-log/DateRangeSelector.tsx
  23. 68 0
      src/components/daily-log/DeleteConfirmDialog.tsx
  24. 46 0
      src/components/daily-log/EnergySelector.tsx
  25. 40 0
      src/components/daily-log/ExportButton.tsx
  26. 92 0
      src/components/daily-log/ExportDialog.tsx
  27. 65 0
      src/components/daily-log/MoodFilter.tsx
  28. 46 0
      src/components/daily-log/MoodSelector.tsx
  29. 60 0
      src/components/daily-log/QuickAddButton.tsx
  30. 83 0
      src/components/daily-log/SleepInput.tsx
  31. 49 0
      src/components/daily-log/StatsCard.tsx
  32. 97 0
      src/components/daily-log/TrendChart.tsx
  33. 1 1
      src/components/dashboard/DashboardStats.tsx
  34. 3 4
      src/components/dashboard/QuickActions.tsx
  35. 15 1
      src/components/sidebar/SidebarNavigation.tsx
  36. 157 0
      src/components/ui/alert-dialog.tsx
  37. 251 0
      src/hooks/useDailyLog.ts
  38. 1 1
      src/lib/auth.ts
  39. 38 0
      src/types/daily-log.ts

+ 192 - 0
docs/DAILY_LOG_COMPLETE.md

@@ -0,0 +1,192 @@
+# Daily Log - Implementación Completa ✅
+
+## Resumen de la Implementación
+
+Se ha implementado exitosamente el sistema de **Daily Log** (Diario Personal) para pacientes en Ani Assistant.
+
+---
+
+## 📊 Fases Completadas
+
+### ✅ Fase 1: Base y CRUD
+- [x] Modelo de base de datos (`DailyLog`)
+- [x] Migración de Prisma
+- [x] API Routes (GET, POST, PUT, DELETE)
+- [x] Hook personalizado (`useDailyLog`)
+- [x] Componentes de formulario (Mood, Energy, Sleep)
+- [x] Componentes de lista y cards
+- [x] Página principal con tabs
+- [x] Integración en sidebar
+
+### ✅ Fase 2: Visualizaciones
+- [x] API de estadísticas
+- [x] Componentes de stats (promedios, racha)
+- [x] Calendario con colores por mood
+- [x] Gráfico de tendencias (recharts)
+- [x] Navegación por mes en calendario
+
+### ✅ Fase 3: Polish y UX
+- [x] QuickAddButton (botón flotante)
+- [x] DeleteConfirmDialog (confirmación)
+- [x] DailyLogSkeleton (loading states)
+- [x] DailyLogFilters (filtros avanzados)
+- [x] DateRangeSelector (períodos)
+- [x] MoodFilter (filtro por ánimo)
+- [x] ExportButton + ExportDialog
+- [x] Función exportToCSV en hook
+
+---
+
+## 🗂️ Estructura de Archivos
+
+```
+prisma/
+└── schema.prisma                    # Modelo DailyLog
+
+src/
+├── app/
+│   ├── api/
+│   │   └── daily-log/
+│   │       ├── route.ts             # GET (rango), POST (crear/actualizar)
+│   │       ├── [date]/route.ts      # GET, PUT, DELETE por fecha
+│   │       └── stats/route.ts       # Estadísticas calculadas
+│   └── daily-log/
+│       └── page.tsx                 # Página principal con tabs
+│
+├── components/
+│   ├── daily-log/
+│   │   ├── CalendarDay.tsx          # Día individual del calendario
+│   │   ├── CalendarHeader.tsx       # Navegación de mes
+│   │   ├── DailyLogCalendar.tsx     # Calendario completo
+│   │   ├── DailyLogCard.tsx         # Card individual de log
+│   │   ├── DailyLogEmptyState.tsx   # Estado vacío
+│   │   ├── DailyLogEntryForm.tsx    # Formulario principal
+│   │   ├── DailyLogFilters.tsx      # Panel de filtros ⭐ NUEVO
+│   │   ├── DailyLogList.tsx         # Lista de logs
+│   │   ├── DailyLogSkeleton.tsx     # Loading skeleton ⭐ NUEVO
+│   │   ├── DailyLogStats.tsx        # Cards de estadísticas
+│   │   ├── DateRangeSelector.tsx    # Selector de período ⭐ NUEVO
+│   │   ├── DeleteConfirmDialog.tsx  # Modal de confirmación ⭐ NUEVO
+│   │   ├── EnergySelector.tsx       # Selector de energía
+│   │   ├── ExportButton.tsx         # Botón de export ⭐ NUEVO
+│   │   ├── ExportDialog.tsx         # Modal de export ⭐ NUEVO
+│   │   ├── MoodFilter.tsx           # Filtro por ánimo ⭐ NUEVO
+│   │   ├── MoodSelector.tsx         # Selector de ánimo
+│   │   ├── QuickAddButton.tsx       # Botón flotante ⭐ NUEVO
+│   │   ├── SleepInput.tsx           # Input de sueño
+│   │   ├── StatsCard.tsx            # Card de métrica
+│   │   └── TrendChart.tsx           # Gráfico de líneas
+│   │
+│   ├── sidebar/
+│   │   └── SidebarNavigation.tsx    # Sección "Personal" agregada
+│   │
+│   └── ui/
+│       └── alert-dialog.tsx         # Shadcn AlertDialog ⭐ NUEVO
+│
+├── hooks/
+│   └── useDailyLog.ts               # Hook con CRUD + exportToCSV
+│
+└── types/
+    └── daily-log.ts                 # Tipos TypeScript
+```
+
+---
+
+## 🎨 Características Principales
+
+### 1. **Registro Diario**
+- Mood (ánimo): 1-5 con emojis 😢😕😐🙂😄
+- Energy (energía): 1-5 con iconos de batería 🪫🔋⚡
+- Sleep Hours (horas de sueño): número decimal
+- Sleep Quality (calidad de sueño): 1-5
+- Notes (notas): texto libre
+
+### 2. **Visualizaciones**
+- **Calendario**: Vista mensual con días coloreados por mood
+  - Rojo (mood 1-2), Naranja (3), Amarillo-Verde (4), Verde (5)
+- **Gráfico de Tendencias**: 3 líneas (mood, energy, sleep)
+- **Estadísticas**: Promedios, racha de días consecutivos
+
+### 3. **Filtros Avanzados** ⭐
+- Por período: Última semana, mes, 3 meses, todo
+- Por ánimo: Multi-select con emojis
+- Aplicables a calendario, tendencias y lista
+
+### 4. **Export CSV** ⭐
+- 4 opciones de período
+- Formato UTF-8 con BOM
+- Columnas: Fecha, Ánimo, Energía, Horas de Sueño, Calidad, Notas
+
+### 5. **UX Mejorado** ⭐
+- Botón flotante para quick-add del día actual
+- Modal de confirmación antes de eliminar
+- Skeleton loaders durante carga
+- Navegación por tabs (Calendario/Tendencias/Lista)
+
+---
+
+## 🔐 Seguridad
+
+- ✅ Solo usuarios con rol `PATIENT` pueden acceder
+- ✅ Cada usuario solo ve sus propios registros
+- ✅ Validación de fechas (no futuras)
+- ✅ Validación de rangos (mood/energy 1-5)
+- ✅ Unique constraint: 1 registro por usuario por día
+
+---
+
+## 🎯 API Endpoints
+
+| Método | Endpoint | Descripción |
+|--------|----------|-------------|
+| GET | `/api/daily-log?startDate=&endDate=` | Obtener logs de rango |
+| POST | `/api/daily-log` | Crear/actualizar log (upsert) |
+| GET | `/api/daily-log/[date]` | Obtener log específico |
+| PUT | `/api/daily-log/[date]` | Actualizar log existente |
+| DELETE | `/api/daily-log/[date]` | Eliminar log |
+| GET | `/api/daily-log/stats?startDate=&endDate=` | Estadísticas calculadas |
+
+---
+
+## 🚀 Próximos Pasos (Opcional)
+
+Si deseas continuar mejorando el sistema:
+
+1. **Notificaciones Push**: Recordatorios diarios para registrar
+2. **Insights con IA**: Análisis de patrones con OpenRouter
+3. **Comparación de períodos**: "Este mes vs mes anterior"
+4. **Compartir con doctor**: Opción para que el médico vea tendencias
+5. **Metas personales**: "Mantener mood >3 por 7 días"
+6. **Import CSV**: Importar datos históricos
+7. **Notas de voz**: Grabar en lugar de escribir
+8. **Tags/categorías**: Etiquetar registros (trabajo, familia, salud)
+
+---
+
+## 📝 Notas Técnicas
+
+- **Prisma unique constraint**: `@@unique([userId, date])` evita duplicados
+- **Next.js 15**: Params son `Promise` en rutas dinámicas
+- **Recharts**: Instalado para gráficos (`npm install recharts`)
+- **Shadcn AlertDialog**: Agregado para confirmaciones
+- **Toast system**: Usando `sonner` (no `useToast`)
+- **CSV Export**: BOM UTF-8 para compatibilidad con Excel
+
+---
+
+## ✅ Build Status
+
+```bash
+✓ Compiled successfully
+✓ Linting and checking validity of types
+✓ Collecting page data
+✓ Generating static pages (29/29)
+✓ Finalizing page optimization
+
+Route: /daily-log (101 kB, 279 kB First Load)
+```
+
+---
+
+**Estado**: ✅ Completamente funcional y listo para producción
+**Fecha de implementación**: Octubre 16, 2025

+ 215 - 0
docs/DAILY_LOG_IMPLEMENTATION.md

@@ -0,0 +1,215 @@
+# Implementación del Daily Log (Diario Personal)
+
+**Fecha de inicio:** 16 de octubre, 2025  
+**Objetivo:** Sistema de seguimiento diario personal para pacientes (PATIENT)
+
+---
+
+## 📋 Progreso General
+
+- [x] **Fase 1:** Base de Datos y APIs ✅
+- [x] **Fase 2:** Visualizaciones ✅
+- [ ] **Fase 3:** Polish y UX
+
+---
+
+## Fase 1: Base de Datos y APIs ✅
+
+### 1.1 Base de Datos ✅
+- [x] Agregar modelo `DailyLog` a `prisma/schema.prisma`
+- [x] Agregar relación en modelo `User`
+- [x] Ejecutar migración: `npx prisma migrate dev`
+
+### 1.2 TypeScript Types ✅
+- [x] Crear `src/types/daily-log.ts`
+
+### 1.3 API Routes ✅
+- [x] `src/app/api/daily-log/route.ts` (GET rango, POST crear)
+- [x] `src/app/api/daily-log/[date]/route.ts` (GET, PUT, DELETE)
+- [x] Validación: Solo PATIENT, solo sus propios datos
+
+### 1.4 Hooks ✅
+- [x] `src/hooks/useDailyLog.ts` (fetchLogs, createLog, updateLog, deleteLog)
+
+### 1.5 Componentes Base (Formulario) ✅
+- [x] `src/components/daily-log/DailyLogEntryForm.tsx` (contenedor del form)
+- [x] `src/components/daily-log/MoodSelector.tsx` (5 emojis)
+- [x] `src/components/daily-log/EnergySelector.tsx` (5 niveles)
+- [x] `src/components/daily-log/SleepInput.tsx` (horas + calidad)
+
+### 1.6 Componentes de Lista ✅
+- [x] `src/components/daily-log/DailyLogList.tsx` (lista de registros)
+- [x] `src/components/daily-log/DailyLogCard.tsx` (card individual)
+- [x] `src/components/daily-log/DailyLogEmptyState.tsx` (sin registros)
+
+### 1.7 Página Principal (Composición) ✅
+- [x] `src/app/daily-log/page.tsx` (protegida para PATIENT)
+  - Usa DailyLogList
+  - Botón "Registrar hoy" (abre modal/drawer)
+  - Modal con DailyLogEntryForm
+
+### 1.8 Navegación ✅
+- [x] Actualizar `src/components/sidebar/SidebarNavigation.tsx`
+- [x] Agregar nueva sección "Personal" solo para PATIENT
+- [x] Agregar "Mi Diario" dentro de "Personal"
+
+---
+
+## Fase 2: Visualizaciones ✅
+
+### 2.1 Componentes de Calendario ✅
+- [x] `src/components/daily-log/DailyLogCalendar.tsx` (contenedor)
+- [x] `src/components/daily-log/CalendarDay.tsx` (día individual)
+- [x] `src/components/daily-log/CalendarHeader.tsx` (mes/año + navegación)
+- [x] Vista mensual con colores según mood
+- [x] Click para ver/editar
+
+### 2.2 Componentes de Estadísticas ✅
+- [x] `src/app/api/daily-log/stats/route.ts`
+- [x] `src/components/daily-log/DailyLogStats.tsx` (contenedor)
+- [x] `src/components/daily-log/StatsCard.tsx` (card individual reutilizable)
+- [x] Cards: días registrados, promedios mood/energy/sleep, racha
+
+### 2.3 Componentes de Gráficas ✅
+- [x] `src/components/daily-log/TrendChart.tsx` (gráfica con recharts)
+- [x] Instalar recharts: `npm install recharts`
+- [x] Líneas de tendencia (últimos 14 días)
+
+### 2.4 Layout Modular ✅
+- [x] Reorganizar página con tabs (Calendario, Tendencias, Lista)
+- [x] Integrar DailyLogStats en header
+- [x] Grid responsive para todas las vistas
+
+---
+
+## Fase 3: Polish y UX
+
+### 3.1 Componentes de Interacción
+- [ ] `src/components/daily-log/QuickAddButton.tsx` (botón flotante)
+- [ ] `src/components/daily-log/DeleteConfirmDialog.tsx` (confirmación)
+- [ ] `src/components/daily-log/DailyLogSkeleton.tsx` (loading state)
+- [ ] Toast notifications (usar sistema existente)
+
+### 3.2 Componentes de Filtros
+- [ ] `src/components/daily-log/DailyLogFilters.tsx` (contenedor)
+- [ ] `src/components/daily-log/DateRangeSelector.tsx`
+- [ ] `src/components/daily-log/MoodFilter.tsx`
+
+### 3.3 Componentes de Exportación
+- [ ] `src/components/daily-log/ExportButton.tsx`
+- [ ] `src/components/daily-log/ExportDialog.tsx`
+- [ ] Formato CSV
+
+---
+
+## 📐 Schema de Base de Datos
+
+```prisma
+model DailyLog {
+  id           String   @id @default(cuid())
+  userId       String
+  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
+  date         DateTime @db.Date
+  
+  // Métricas (1-5)
+  mood         Int?
+  energy       Int?
+  sleepHours   Float?
+  sleepQuality Int?
+  
+  // Notas
+  notes        String?  @db.Text
+  
+  createdAt    DateTime @default(now())
+  updatedAt    DateTime @updatedAt
+  
+  @@unique([userId, date])
+  @@index([userId])
+  @@index([date])
+}
+```
+
+---
+
+## 🎯 Alcance
+
+### ✅ Incluido
+- Registro diario (mood, energy, sleep, notes)
+- Calendario visual
+- Estadísticas y gráficas
+- 100% privado para el usuario
+
+### ❌ Fuera del Alcance (v1)
+- Integración con chatbot
+- Compartir con doctor
+- Alertas automáticas
+- Síntomas/medicamentos avanzados
+
+---
+
+## 📊 Estimación
+
+| Fase | Tiempo |
+|------|--------|
+| Fase 1 | 4-6h |
+| Fase 2 | 3-4h |
+| Fase 3 | 2-3h |
+| **Total** | **9-13h** |
+
+---
+
+## 🚀 Estado Actual
+
+**Última actualización:** 16 de octubre, 2025  
+**Status:** 🟢 Fase 2 Completada  
+**Siguiente paso:** Implementar polish y UX (Fase 3)
+
+---
+
+## 📝 Resumen de Fases 1 + 2
+
+### Archivos creados (20):
+**Fase 1:**
+1. `prisma/migrations/20251016100748_add_daily_log/migration.sql`
+2. `src/types/daily-log.ts`
+3. `src/app/api/daily-log/route.ts`
+4. `src/app/api/daily-log/[date]/route.ts`
+5. `src/hooks/useDailyLog.ts`
+6. `src/components/daily-log/MoodSelector.tsx`
+7. `src/components/daily-log/EnergySelector.tsx`
+8. `src/components/daily-log/SleepInput.tsx`
+9. `src/components/daily-log/DailyLogEntryForm.tsx`
+10. `src/components/daily-log/DailyLogList.tsx`
+11. `src/components/daily-log/DailyLogCard.tsx`
+12. `src/components/daily-log/DailyLogEmptyState.tsx`
+13. `src/app/daily-log/page.tsx`
+
+**Fase 2:**
+14. `src/app/api/daily-log/stats/route.ts`
+15. `src/components/daily-log/StatsCard.tsx`
+16. `src/components/daily-log/DailyLogStats.tsx`
+17. `src/components/daily-log/CalendarDay.tsx`
+18. `src/components/daily-log/CalendarHeader.tsx`
+19. `src/components/daily-log/DailyLogCalendar.tsx`
+20. `src/components/daily-log/TrendChart.tsx`
+
+### Archivos modificados (3):
+1. `prisma/schema.prisma` - Modelo DailyLog
+2. `src/components/sidebar/SidebarNavigation.tsx` - Sección "Personal"
+3. `src/app/daily-log/page.tsx` - Layout con tabs + AuthenticatedLayout
+
+### 📦 Dependencias:
+- `recharts` - Gráficas interactivas
+
+### ✅ Funcionalidades:
+**Fase 1:** CRUD, protección, validaciones, UI modular, notificaciones  
+**Fase 2:** Estadísticas, calendario interactivo, gráficas, tabs, rachas
+
+---
+
+## 📝 Notas
+
+- Solo para usuarios PATIENT
+- No permite fechas futuras
+- Todas las métricas son opcionales
+- Un solo registro por día por usuario

+ 472 - 9
package-lock.json

@@ -13,6 +13,7 @@
         "@auth/prisma-adapter": "^2.10.0",
         "@headlessui/react": "^2.2.7",
         "@prisma/client": "^6.12.0",
+        "@radix-ui/react-alert-dialog": "^1.1.15",
         "@radix-ui/react-avatar": "^1.1.10",
         "@radix-ui/react-dialog": "^1.1.14",
         "@radix-ui/react-dropdown-menu": "^2.1.15",
@@ -47,6 +48,7 @@
         "react-dom": "19.1.0",
         "react-markdown": "^10.1.0",
         "react-paginate": "^8.3.0",
+        "recharts": "^3.2.1",
         "rehype-highlight": "^7.0.2",
         "rehype-raw": "^7.0.0",
         "rehype-stringify": "^10.0.1",
@@ -2581,6 +2583,40 @@
       "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
       "license": "MIT"
     },
+    "node_modules/@radix-ui/react-alert-dialog": {
+      "version": "1.1.15",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz",
+      "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/primitive": "1.1.3",
+        "@radix-ui/react-compose-refs": "1.1.2",
+        "@radix-ui/react-context": "1.1.2",
+        "@radix-ui/react-dialog": "1.1.15",
+        "@radix-ui/react-primitive": "2.1.3",
+        "@radix-ui/react-slot": "1.2.3"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/primitive": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+      "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
+      "license": "MIT"
+    },
     "node_modules/@radix-ui/react-arrow": {
       "version": "1.1.7",
       "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
@@ -2688,20 +2724,20 @@
       }
     },
     "node_modules/@radix-ui/react-dialog": {
-      "version": "1.1.14",
-      "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz",
-      "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==",
+      "version": "1.1.15",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
+      "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
       "license": "MIT",
       "dependencies": {
-        "@radix-ui/primitive": "1.1.2",
+        "@radix-ui/primitive": "1.1.3",
         "@radix-ui/react-compose-refs": "1.1.2",
         "@radix-ui/react-context": "1.1.2",
-        "@radix-ui/react-dismissable-layer": "1.1.10",
-        "@radix-ui/react-focus-guards": "1.1.2",
+        "@radix-ui/react-dismissable-layer": "1.1.11",
+        "@radix-ui/react-focus-guards": "1.1.3",
         "@radix-ui/react-focus-scope": "1.1.7",
         "@radix-ui/react-id": "1.1.1",
         "@radix-ui/react-portal": "1.1.9",
-        "@radix-ui/react-presence": "1.1.4",
+        "@radix-ui/react-presence": "1.1.5",
         "@radix-ui/react-primitive": "2.1.3",
         "@radix-ui/react-slot": "1.2.3",
         "@radix-ui/react-use-controllable-state": "1.2.2",
@@ -2723,6 +2759,78 @@
         }
       }
     },
+    "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+      "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
+      "license": "MIT"
+    },
+    "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
+      "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/primitive": "1.1.3",
+        "@radix-ui/react-compose-refs": "1.1.2",
+        "@radix-ui/react-primitive": "2.1.3",
+        "@radix-ui/react-use-callback-ref": "1.1.1",
+        "@radix-ui/react-use-escape-keydown": "1.1.1"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
+      "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
+      "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/react-compose-refs": "1.1.2",
+        "@radix-ui/react-use-layout-effect": "1.1.1"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@radix-ui/react-direction": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
@@ -3920,6 +4028,32 @@
         "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
       }
     },
+    "node_modules/@reduxjs/toolkit": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz",
+      "integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==",
+      "license": "MIT",
+      "dependencies": {
+        "@standard-schema/spec": "^1.0.0",
+        "@standard-schema/utils": "^0.3.0",
+        "immer": "^10.0.3",
+        "redux": "^5.0.1",
+        "redux-thunk": "^3.1.0",
+        "reselect": "^5.1.0"
+      },
+      "peerDependencies": {
+        "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+        "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+      },
+      "peerDependenciesMeta": {
+        "react": {
+          "optional": true
+        },
+        "react-redux": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@rtsao/scc": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -3938,8 +4072,13 @@
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
       "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
-      "license": "MIT",
-      "peer": true
+      "license": "MIT"
+    },
+    "node_modules/@standard-schema/utils": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
+      "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
+      "license": "MIT"
     },
     "node_modules/@swc/helpers": {
       "version": "0.5.15",
@@ -4002,6 +4141,69 @@
       "optional": true,
       "peer": true
     },
+    "node_modules/@types/d3-array": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+      "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-color": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+      "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-ease": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+      "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-interpolate": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+      "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-color": "*"
+      }
+    },
+    "node_modules/@types/d3-path": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+      "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-scale": {
+      "version": "4.0.9",
+      "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+      "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-time": "*"
+      }
+    },
+    "node_modules/@types/d3-shape": {
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
+      "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-path": "*"
+      }
+    },
+    "node_modules/@types/d3-time": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+      "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-timer": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+      "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+      "license": "MIT"
+    },
     "node_modules/@types/debug": {
       "version": "4.1.12",
       "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -4118,6 +4320,12 @@
       "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
       "license": "MIT"
     },
+    "node_modules/@types/use-sync-external-store": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+      "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+      "license": "MIT"
+    },
     "node_modules/@typescript-eslint/eslint-plugin": {
       "version": "8.37.0",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz",
@@ -5922,6 +6130,127 @@
       "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
       "license": "MIT"
     },
+    "node_modules/d3-array": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+      "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+      "license": "ISC",
+      "dependencies": {
+        "internmap": "1 - 2"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-color": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+      "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-ease": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+      "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-format": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
+      "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-interpolate": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+      "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-color": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-path": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+      "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-scale": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+      "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-array": "2.10.0 - 3",
+        "d3-format": "1 - 3",
+        "d3-interpolate": "1.2.0 - 3",
+        "d3-time": "2.1.1 - 3",
+        "d3-time-format": "2 - 4"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-shape": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+      "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-path": "^3.1.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-time": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+      "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-array": "2 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-time-format": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+      "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-time": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-timer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+      "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/damerau-levenshtein": {
       "version": "1.0.8",
       "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -6025,6 +6354,12 @@
         }
       }
     },
+    "node_modules/decimal.js-light": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+      "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
+      "license": "MIT"
+    },
     "node_modules/decode-named-character-reference": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz",
@@ -6414,6 +6749,16 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/es-toolkit": {
+      "version": "1.40.0",
+      "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.40.0.tgz",
+      "integrity": "sha512-8o6w0KFmU0CiIl0/Q/BCEOabF2IJaELM1T2PWj6e8KqzHv1gdx+7JtFnDwOx1kJH/isJ5NwlDG1nCr1HrRF94Q==",
+      "license": "MIT",
+      "workspaces": [
+        "docs",
+        "benchmarks"
+      ]
+    },
     "node_modules/esbuild": {
       "version": "0.25.6",
       "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",
@@ -6926,6 +7271,12 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/eventemitter3": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+      "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+      "license": "MIT"
+    },
     "node_modules/events": {
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -7862,6 +8213,16 @@
         "node": ">= 4"
       }
     },
+    "node_modules/immer": {
+      "version": "10.1.3",
+      "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz",
+      "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/immer"
+      }
+    },
     "node_modules/import-fresh": {
       "version": "3.3.1",
       "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -7915,6 +8276,15 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/internmap": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+      "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/is-alphabetical": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
@@ -10970,6 +11340,29 @@
         "react": "^16 || ^17 || ^18 || ^19"
       }
     },
+    "node_modules/react-redux": {
+      "version": "9.2.0",
+      "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
+      "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/use-sync-external-store": "^0.0.6",
+        "use-sync-external-store": "^1.4.0"
+      },
+      "peerDependencies": {
+        "@types/react": "^18.2.25 || ^19",
+        "react": "^18.0 || ^19",
+        "redux": "^5.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "redux": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/react-remove-scroll": {
       "version": "2.7.1",
       "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
@@ -11096,6 +11489,48 @@
         "node": ">= 4"
       }
     },
+    "node_modules/recharts": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.2.1.tgz",
+      "integrity": "sha512-0JKwHRiFZdmLq/6nmilxEZl3pqb4T+aKkOkOi/ZISRZwfBhVMgInxzlYU9D4KnCH3KINScLy68m/OvMXoYGZUw==",
+      "license": "MIT",
+      "dependencies": {
+        "@reduxjs/toolkit": "1.x.x || 2.x.x",
+        "clsx": "^2.1.1",
+        "decimal.js-light": "^2.5.1",
+        "es-toolkit": "^1.39.3",
+        "eventemitter3": "^5.0.1",
+        "immer": "^10.1.1",
+        "react-redux": "8.x.x || 9.x.x",
+        "reselect": "5.1.1",
+        "tiny-invariant": "^1.3.3",
+        "use-sync-external-store": "^1.2.2",
+        "victory-vendor": "^37.0.2"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
+    "node_modules/redux": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+      "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+      "license": "MIT"
+    },
+    "node_modules/redux-thunk": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+      "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+      "license": "MIT",
+      "peerDependencies": {
+        "redux": "^5.0.0"
+      }
+    },
     "node_modules/reflect.getprototypeof": {
       "version": "1.0.10",
       "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -11269,6 +11704,12 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/reselect": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+      "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
+      "license": "MIT"
+    },
     "node_modules/resolve": {
       "version": "1.22.10",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -12975,6 +13416,28 @@
         "url": "https://opencollective.com/unified"
       }
     },
+    "node_modules/victory-vendor": {
+      "version": "37.3.6",
+      "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
+      "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
+      "license": "MIT AND ISC",
+      "dependencies": {
+        "@types/d3-array": "^3.0.3",
+        "@types/d3-ease": "^3.0.0",
+        "@types/d3-interpolate": "^3.0.1",
+        "@types/d3-scale": "^4.0.2",
+        "@types/d3-shape": "^3.1.0",
+        "@types/d3-time": "^3.0.0",
+        "@types/d3-timer": "^3.0.0",
+        "d3-array": "^3.1.6",
+        "d3-ease": "^3.0.1",
+        "d3-interpolate": "^3.0.1",
+        "d3-scale": "^4.0.2",
+        "d3-shape": "^3.1.0",
+        "d3-time": "^3.0.0",
+        "d3-timer": "^3.0.1"
+      }
+    },
     "node_modules/vite-compatible-readable-stream": {
       "version": "3.6.1",
       "resolved": "https://registry.npmjs.org/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz",

+ 2 - 0
package.json

@@ -28,6 +28,7 @@
     "@auth/prisma-adapter": "^2.10.0",
     "@headlessui/react": "^2.2.7",
     "@prisma/client": "^6.12.0",
+    "@radix-ui/react-alert-dialog": "^1.1.15",
     "@radix-ui/react-avatar": "^1.1.10",
     "@radix-ui/react-dialog": "^1.1.14",
     "@radix-ui/react-dropdown-menu": "^2.1.15",
@@ -62,6 +63,7 @@
     "react-dom": "19.1.0",
     "react-markdown": "^10.1.0",
     "react-paginate": "^8.3.0",
+    "recharts": "^3.2.1",
     "rehype-highlight": "^7.0.2",
     "rehype-raw": "^7.0.0",
     "rehype-stringify": "^10.0.1",

+ 27 - 0
prisma/migrations/20251016100748_add_daily_log/migration.sql

@@ -0,0 +1,27 @@
+-- CreateTable
+CREATE TABLE "DailyLog" (
+    "id" TEXT NOT NULL,
+    "userId" TEXT NOT NULL,
+    "date" DATE NOT NULL,
+    "mood" INTEGER,
+    "energy" INTEGER,
+    "sleepHours" DOUBLE PRECISION,
+    "sleepQuality" INTEGER,
+    "notes" TEXT,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+
+    CONSTRAINT "DailyLog_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE INDEX "DailyLog_userId_idx" ON "DailyLog"("userId");
+
+-- CreateIndex
+CREATE INDEX "DailyLog_date_idx" ON "DailyLog"("date");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "DailyLog_userId_date_key" ON "DailyLog"("userId", "date");
+
+-- AddForeignKey
+ALTER TABLE "DailyLog" ADD CONSTRAINT "DailyLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

+ 25 - 0
prisma/schema.prisma

@@ -35,6 +35,7 @@ model User {
   assignedDoctor PatientAssignment[] @relation("PatientDoctor")
   patientAppointments Appointment[] @relation("PatientAppointments")
   doctorAppointments Appointment[] @relation("DoctorAppointments")
+  dailyLogs    DailyLog[]
 }
 
 model PatientAssignment {
@@ -96,6 +97,30 @@ model Appointment {
   @@index([fechaSolicitada])
 }
 
+model DailyLog {
+  id           String   @id @default(cuid())
+  userId       String
+  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
+  date         DateTime @db.Date
+  
+  // Métricas (1-5)
+  mood         Int?
+  energy       Int?
+  sleepHours   Float?
+  sleepQuality Int?
+  
+  // Notas personales
+  notes        String?  @db.Text
+  
+  // Metadata
+  createdAt    DateTime @default(now())
+  updatedAt    DateTime @updatedAt
+  
+  @@unique([userId, date])
+  @@index([userId])
+  @@index([date])
+}
+
 enum Role {
   ADMIN
   DOCTOR

+ 198 - 0
src/app/api/daily-log/[date]/route.ts

@@ -0,0 +1,198 @@
+import { NextRequest, NextResponse } from "next/server"
+import { getServerSession } from "next-auth"
+import { authOptions } from "@/lib/auth"
+import { prisma } from "@/lib/prisma"
+
+// GET - Obtener log de una fecha específica
+export async function GET(
+  req: NextRequest,
+  { params }: { params: Promise<{ date: string }> }
+) {
+  try {
+    const session = await getServerSession(authOptions)
+
+    if (!session?.user) {
+      return NextResponse.json({ error: "No autorizado" }, { status: 401 })
+    }
+
+    if (session.user.role !== "PATIENT") {
+      return NextResponse.json(
+        { error: "Solo pacientes pueden acceder al diario" },
+        { status: 403 }
+      )
+    }
+
+    const { date } = await params
+
+    const log = await prisma.dailyLog.findUnique({
+      where: {
+        userId_date: {
+          userId: session.user.id,
+          date: new Date(date),
+        },
+      },
+    })
+
+    if (!log) {
+      return NextResponse.json(
+        { error: "Registro no encontrado" },
+        { status: 404 }
+      )
+    }
+
+    return NextResponse.json(log)
+  } catch (error) {
+    console.error("Error al obtener log:", error)
+    return NextResponse.json(
+      { error: "Error al obtener registro" },
+      { status: 500 }
+    )
+  }
+}
+
+// PUT - Actualizar log existente
+export async function PUT(
+  req: NextRequest,
+  { params }: { params: Promise<{ date: string }> }
+) {
+  try {
+    const session = await getServerSession(authOptions)
+
+    if (!session?.user) {
+      return NextResponse.json({ error: "No autorizado" }, { status: 401 })
+    }
+
+    if (session.user.role !== "PATIENT") {
+      return NextResponse.json(
+        { error: "Solo pacientes pueden actualizar registros" },
+        { status: 403 }
+      )
+    }
+
+    const { date } = await params
+    const body = await req.json()
+    const { mood, energy, sleepHours, sleepQuality, notes } = body
+
+    // Validar rangos
+    if (mood !== undefined && (mood < 1 || mood > 5)) {
+      return NextResponse.json(
+        { error: "El ánimo debe estar entre 1 y 5" },
+        { status: 400 }
+      )
+    }
+
+    if (energy !== undefined && (energy < 1 || energy > 5)) {
+      return NextResponse.json(
+        { error: "La energía debe estar entre 1 y 5" },
+        { status: 400 }
+      )
+    }
+
+    if (sleepQuality !== undefined && (sleepQuality < 1 || sleepQuality > 5)) {
+      return NextResponse.json(
+        { error: "La calidad del sueño debe estar entre 1 y 5" },
+        { status: 400 }
+      )
+    }
+
+    // Verificar que el log existe y pertenece al usuario
+    const existingLog = await prisma.dailyLog.findUnique({
+      where: {
+        userId_date: {
+          userId: session.user.id,
+          date: new Date(date),
+        },
+      },
+    })
+
+    if (!existingLog) {
+      return NextResponse.json(
+        { error: "Registro no encontrado" },
+        { status: 404 }
+      )
+    }
+
+    // Actualizar
+    const updatedLog = await prisma.dailyLog.update({
+      where: {
+        userId_date: {
+          userId: session.user.id,
+          date: new Date(date),
+        },
+      },
+      data: {
+        mood,
+        energy,
+        sleepHours,
+        sleepQuality,
+        notes,
+      },
+    })
+
+    return NextResponse.json(updatedLog)
+  } catch (error) {
+    console.error("Error al actualizar log:", error)
+    return NextResponse.json(
+      { error: "Error al actualizar registro" },
+      { status: 500 }
+    )
+  }
+}
+
+// DELETE - Eliminar log
+export async function DELETE(
+  req: NextRequest,
+  { params }: { params: Promise<{ date: string }> }
+) {
+  try {
+    const session = await getServerSession(authOptions)
+
+    if (!session?.user) {
+      return NextResponse.json({ error: "No autorizado" }, { status: 401 })
+    }
+
+    if (session.user.role !== "PATIENT") {
+      return NextResponse.json(
+        { error: "Solo pacientes pueden eliminar registros" },
+        { status: 403 }
+      )
+    }
+
+    const { date } = await params
+
+    // Verificar que existe
+    const existingLog = await prisma.dailyLog.findUnique({
+      where: {
+        userId_date: {
+          userId: session.user.id,
+          date: new Date(date),
+        },
+      },
+    })
+
+    if (!existingLog) {
+      return NextResponse.json(
+        { error: "Registro no encontrado" },
+        { status: 404 }
+      )
+    }
+
+    // Eliminar
+    await prisma.dailyLog.delete({
+      where: {
+        userId_date: {
+          userId: session.user.id,
+          date: new Date(date),
+        },
+      },
+    })
+
+    return NextResponse.json({ message: "Registro eliminado" })
+  } catch (error) {
+    console.error("Error al eliminar log:", error)
+    return NextResponse.json(
+      { error: "Error al eliminar registro" },
+      { status: 500 }
+    )
+  }
+}

+ 156 - 0
src/app/api/daily-log/route.ts

@@ -0,0 +1,156 @@
+import { NextRequest, NextResponse } from "next/server"
+import { getServerSession } from "next-auth"
+import { authOptions } from "@/lib/auth"
+import { prisma } from "@/lib/prisma"
+
+// GET - Obtener logs de un rango de fechas
+export async function GET(req: NextRequest) {
+  try {
+    const session = await getServerSession(authOptions)
+
+    if (!session?.user) {
+      return NextResponse.json({ error: "No autorizado" }, { status: 401 })
+    }
+
+    // Solo PATIENT puede acceder a sus logs
+    if (session.user.role !== "PATIENT") {
+      return NextResponse.json(
+        { error: "Solo pacientes pueden acceder al diario" },
+        { status: 403 }
+      )
+    }
+
+    // Obtener parámetros de query
+    const searchParams = req.nextUrl.searchParams
+    const startDate = searchParams.get("startDate")
+    const endDate = searchParams.get("endDate")
+
+    // Validar fechas
+    if (!startDate || !endDate) {
+      return NextResponse.json(
+        { error: "Se requieren startDate y endDate" },
+        { status: 400 }
+      )
+    }
+
+    // Obtener logs del usuario en el rango de fechas
+    const logs = await prisma.dailyLog.findMany({
+      where: {
+        userId: session.user.id,
+        date: {
+          gte: new Date(startDate),
+          lte: new Date(endDate),
+        },
+      },
+      orderBy: {
+        date: "desc",
+      },
+    })
+
+    return NextResponse.json(logs)
+  } catch (error) {
+    console.error("Error al obtener logs:", error)
+    return NextResponse.json(
+      { error: "Error al obtener registros" },
+      { status: 500 }
+    )
+  }
+}
+
+// POST - Crear nuevo log
+export async function POST(req: NextRequest) {
+  try {
+    const session = await getServerSession(authOptions)
+
+    if (!session?.user) {
+      return NextResponse.json({ error: "No autorizado" }, { status: 401 })
+    }
+
+    // Solo PATIENT puede crear logs
+    if (session.user.role !== "PATIENT") {
+      return NextResponse.json(
+        { error: "Solo pacientes pueden crear registros" },
+        { status: 403 }
+      )
+    }
+
+    const body = await req.json()
+    const { date, mood, energy, sleepHours, sleepQuality, notes } = body
+
+    // Validaciones
+    if (!date) {
+      return NextResponse.json(
+        { error: "La fecha es requerida" },
+        { status: 400 }
+      )
+    }
+
+    // Validar que la fecha no sea futura
+    const logDate = new Date(date)
+    const today = new Date()
+    today.setHours(0, 0, 0, 0)
+    
+    if (logDate > today) {
+      return NextResponse.json(
+        { error: "No se pueden crear registros de fechas futuras" },
+        { status: 400 }
+      )
+    }
+
+    // Validar rangos de valores
+    if (mood !== undefined && (mood < 1 || mood > 5)) {
+      return NextResponse.json(
+        { error: "El ánimo debe estar entre 1 y 5" },
+        { status: 400 }
+      )
+    }
+
+    if (energy !== undefined && (energy < 1 || energy > 5)) {
+      return NextResponse.json(
+        { error: "La energía debe estar entre 1 y 5" },
+        { status: 400 }
+      )
+    }
+
+    if (sleepQuality !== undefined && (sleepQuality < 1 || sleepQuality > 5)) {
+      return NextResponse.json(
+        { error: "La calidad del sueño debe estar entre 1 y 5" },
+        { status: 400 }
+      )
+    }
+
+    // Crear log (upsert para evitar duplicados)
+    const log = await prisma.dailyLog.upsert({
+      where: {
+        userId_date: {
+          userId: session.user.id,
+          date: new Date(date),
+        },
+      },
+      update: {
+        mood,
+        energy,
+        sleepHours,
+        sleepQuality,
+        notes,
+      },
+      create: {
+        userId: session.user.id,
+        date: new Date(date),
+        mood,
+        energy,
+        sleepHours,
+        sleepQuality,
+        notes,
+      },
+    })
+
+    return NextResponse.json(log, { status: 201 })
+  } catch (error) {
+    console.error("Error al crear log:", error)
+    return NextResponse.json(
+      { error: "Error al crear registro" },
+      { status: 500 }
+    )
+  }
+}

+ 157 - 0
src/app/api/daily-log/stats/route.ts

@@ -0,0 +1,157 @@
+import { NextRequest, NextResponse } from "next/server"
+import { getServerSession } from "next-auth"
+import { authOptions } from "@/lib/auth"
+import { prisma } from "@/lib/prisma"
+import type { DailyLog } from "@prisma/client"
+
+// GET - Obtener estadísticas de los logs
+export async function GET(req: NextRequest) {
+  try {
+    const session = await getServerSession(authOptions)
+
+    if (!session?.user) {
+      return NextResponse.json({ error: "No autorizado" }, { status: 401 })
+    }
+
+    if (session.user.role !== "PATIENT") {
+      return NextResponse.json(
+        { error: "Solo pacientes pueden acceder a estadísticas" },
+        { status: 403 }
+      )
+    }
+
+    // Obtener parámetros
+    const searchParams = req.nextUrl.searchParams
+    const startDate = searchParams.get("startDate")
+    const endDate = searchParams.get("endDate")
+
+    // Si no hay fechas, usar últimos 30 días
+    const end = endDate ? new Date(endDate) : new Date()
+    const start = startDate
+      ? new Date(startDate)
+      : new Date(end.getTime() - 30 * 24 * 60 * 60 * 1000)
+
+    // Obtener logs en el rango
+    const logs = await prisma.dailyLog.findMany({
+      where: {
+        userId: session.user.id,
+        date: {
+          gte: start,
+          lte: end,
+        },
+      },
+      orderBy: {
+        date: "asc",
+      },
+    })
+
+    // Calcular estadísticas
+    const totalEntries = logs.length
+
+    if (totalEntries === 0) {
+      return NextResponse.json({
+        totalEntries: 0,
+        avgMood: null,
+        avgEnergy: null,
+        avgSleep: null,
+        avgSleepQuality: null,
+        streak: 0,
+        moodDistribution: {},
+        energyDistribution: {},
+      })
+    }
+
+    // Promedios
+    const moodLogs = logs.filter((log): log is DailyLog & { mood: number } => log.mood !== null)
+    const energyLogs = logs.filter((log): log is DailyLog & { energy: number } => log.energy !== null)
+    const sleepLogs = logs.filter((log): log is DailyLog & { sleepHours: number } => log.sleepHours !== null)
+    const sleepQualityLogs = logs.filter((log): log is DailyLog & { sleepQuality: number } => log.sleepQuality !== null)
+
+    const avgMood = moodLogs.length
+      ? moodLogs.reduce((sum: number, log) => sum + log.mood, 0) / moodLogs.length
+      : null
+
+    const avgEnergy = energyLogs.length
+      ? energyLogs.reduce((sum: number, log) => sum + log.energy, 0) /
+        energyLogs.length
+      : null
+
+    const avgSleep = sleepLogs.length
+      ? sleepLogs.reduce((sum: number, log) => sum + log.sleepHours, 0) /
+        sleepLogs.length
+      : null
+
+    const avgSleepQuality = sleepQualityLogs.length
+      ? sleepQualityLogs.reduce((sum: number, log) => sum + log.sleepQuality, 0) /
+        sleepQualityLogs.length
+      : null
+
+    // Calcular racha (días consecutivos con registro)
+    let streak = 0
+    const today = new Date()
+    today.setHours(0, 0, 0, 0)
+
+    // Ordenar logs por fecha descendente
+    const sortedLogs = [...logs].sort(
+      (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
+    )
+
+    // Verificar si hay registro hoy o ayer
+    const latestLog = sortedLogs[0]
+    if (latestLog) {
+      const latestDate = new Date(latestLog.date)
+      latestDate.setHours(0, 0, 0, 0)
+      const daysDiff = Math.floor(
+        (today.getTime() - latestDate.getTime()) / (1000 * 60 * 60 * 24)
+      )
+
+      if (daysDiff <= 1) {
+        // Hay registro hoy o ayer, calcular racha
+        streak = 1
+        const checkDate = new Date(latestDate)
+
+        for (let i = 1; i < sortedLogs.length; i++) {
+          checkDate.setDate(checkDate.getDate() - 1)
+          const currentLogDate = new Date(sortedLogs[i].date)
+          currentLogDate.setHours(0, 0, 0, 0)
+
+          if (currentLogDate.getTime() === checkDate.getTime()) {
+            streak++
+          } else {
+            break
+          }
+        }
+      }
+    }
+
+    // Distribución de mood y energy
+    const moodDistribution: Record<number, number> = {}
+    const energyDistribution: Record<number, number> = {}
+
+    logs.forEach((log) => {
+      if (log.mood) {
+        moodDistribution[log.mood] = (moodDistribution[log.mood] || 0) + 1
+      }
+      if (log.energy) {
+        energyDistribution[log.energy] = (energyDistribution[log.energy] || 0) + 1
+      }
+    })
+
+    return NextResponse.json({
+      totalEntries,
+      avgMood: avgMood ? Number(avgMood.toFixed(1)) : null,
+      avgEnergy: avgEnergy ? Number(avgEnergy.toFixed(1)) : null,
+      avgSleep: avgSleep ? Number(avgSleep.toFixed(1)) : null,
+      avgSleepQuality: avgSleepQuality ? Number(avgSleepQuality.toFixed(1)) : null,
+      streak,
+      moodDistribution,
+      energyDistribution,
+    })
+  } catch (error) {
+    console.error("Error al calcular estadísticas:", error)
+    return NextResponse.json(
+      { error: "Error al calcular estadísticas" },
+      { status: 500 }
+    )
+  }
+}

+ 289 - 0
src/app/daily-log/page.tsx

@@ -0,0 +1,289 @@
+"use client"
+
+import { useEffect, useState } from "react"
+import { useSession } from "next-auth/react"
+import { useRouter } from "next/navigation"
+import AuthenticatedLayout from "@/components/AuthenticatedLayout"
+import { useDailyLog } from "@/hooks/useDailyLog"
+import { DailyLog, DailyLogInput, DailyLogFilters } from "@/types/daily-log"
+import { DailyLogList } from "@/components/daily-log/DailyLogList"
+import { DailyLogEntryForm } from "@/components/daily-log/DailyLogEntryForm"
+import { DailyLogCalendar } from "@/components/daily-log/DailyLogCalendar"
+import { DailyLogStats } from "@/components/daily-log/DailyLogStats"
+import { TrendChart } from "@/components/daily-log/TrendChart"
+import { QuickAddButton } from "@/components/daily-log/QuickAddButton"
+import { DeleteConfirmDialog } from "@/components/daily-log/DeleteConfirmDialog"
+import { DailyLogFilters as FiltersComponent } from "@/components/daily-log/DailyLogFilters"
+import { ExportButton } from "@/components/daily-log/ExportButton"
+import { DailyLogHeader } from "@/components/daily-log/DailyLogHeader"
+import { DateRangePreset } from "@/components/daily-log/DateRangeSelector"
+import { Button } from "@/components/ui/button"
+import { Calendar, TrendingUp, Filter } from "lucide-react"
+import { toast } from "sonner"
+import {
+  Dialog,
+  DialogContent,
+  DialogHeader,
+  DialogTitle,
+} from "@/components/ui/dialog"
+import {
+  Tabs,
+  TabsContent,
+  TabsList,
+  TabsTrigger,
+} from "@/components/ui/tabs"
+
+export default function DailyLogPage() {
+  const { data: session, status } = useSession()
+  const router = useRouter()
+  const { logs, loading, fetchLogs, saveLog, deleteLog, exportToCSV } = useDailyLog()
+  
+  const [isFormOpen, setIsFormOpen] = useState(false)
+  const [selectedLog, setSelectedLog] = useState<DailyLog | null>(null)
+  const [selectedDate, setSelectedDate] = useState<string>("")
+  const [dateRange, setDateRange] = useState({ start: "", end: "" })
+  const [showFilters, setShowFilters] = useState(false)
+  const [filters, setFilters] = useState<DailyLogFilters>({})
+  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
+  const [logToDelete, setLogToDelete] = useState<string | null>(null)
+
+  // Protección de ruta
+  useEffect(() => {
+    if (status === "loading") return
+
+    if (!session) {
+      router.push("/auth/login")
+      return
+    }
+
+    if (session.user.role !== "PATIENT") {
+      router.push("/dashboard")
+      return
+    }
+  }, [session, status, router])
+
+  // Cargar logs del mes actual + algo de contexto
+  useEffect(() => {
+    if (session?.user.role === "PATIENT") {
+      const now = new Date()
+      const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
+      // Cargar desde 2 semanas antes para mostrar colores en calendario
+      const startDate = new Date(startOfMonth)
+      startDate.setDate(startDate.getDate() - 14)
+
+      const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0)
+      // Cargar hasta 2 semanas después
+      const endDate = new Date(endOfMonth)
+      endDate.setDate(endDate.getDate() + 14)
+
+      const start = startDate.toISOString().split("T")[0]
+      const end = endDate.toISOString().split("T")[0]
+
+      setDateRange({ start, end })
+      fetchLogs(start, end)
+    }
+  }, [session, fetchLogs])
+
+  if (status === "loading" || !session || session.user.role !== "PATIENT") {
+    return (
+      <div className="flex items-center justify-center min-h-screen">
+        <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary" />
+      </div>
+    )
+  }
+
+  const handleAddNew = () => {
+    setSelectedLog(null)
+    setSelectedDate(new Date().toISOString().split("T")[0])
+    setIsFormOpen(true)
+  }
+
+  const handleEdit = (log: DailyLog) => {
+    setSelectedLog(log)
+    setSelectedDate(log.date)
+    setIsFormOpen(true)
+  }
+
+  const handleDateClick = (date: string, log?: DailyLog) => {
+    // Si ya existe un registro para esta fecha, editarlo
+    if (log) {
+      handleEdit(log)
+      return
+    }
+
+    // Si no existe registro, verificar que no sea fecha futura
+    const selectedDate = new Date(date)
+    const today = new Date()
+    today.setHours(23, 59, 59, 999) // Fin del día actual
+
+    if (selectedDate > today) {
+      toast.error("No puedes registrar fechas futuras", {
+        description: "Solo puedes registrar el día actual o días pasados",
+      })
+      return
+    }
+
+    // Crear nuevo registro
+    setSelectedLog(null)
+    setSelectedDate(date)
+    setIsFormOpen(true)
+  }
+
+  const handleSubmit = async (data: DailyLogInput) => {
+    const result = await saveLog(data)
+    if (result) {
+      setIsFormOpen(false)
+      setSelectedLog(null)
+    }
+  }
+
+  const handleDelete = async (date: string) => {
+    setLogToDelete(date)
+    setDeleteDialogOpen(true)
+  }
+
+  const confirmDelete = async () => {
+    if (logToDelete) {
+      await deleteLog(logToDelete)
+      setDeleteDialogOpen(false)
+      setLogToDelete(null)
+    }
+  }
+
+  const handleExport = async (preset: DateRangePreset) => {
+    const now = new Date()
+    let startDate: Date
+
+    switch (preset) {
+      case "week":
+        startDate = new Date(now)
+        startDate.setDate(now.getDate() - 7)
+        break
+      case "month":
+        startDate = new Date(now)
+        startDate.setMonth(now.getMonth() - 1)
+        break
+      case "3months":
+        startDate = new Date(now)
+        startDate.setMonth(now.getMonth() - 3)
+        break
+      case "all":
+        startDate = new Date(0)
+        break
+    }
+
+    const start = startDate.toISOString().split("T")[0]
+    const end = now.toISOString().split("T")[0]
+
+    const logsToExport = await fetchLogs(start, end)
+    if (logsToExport && logsToExport.length > 0) {
+      exportToCSV(logsToExport)
+    }
+  }
+
+  // Filtrar logs según los filtros aplicados
+  const filteredLogs = logs.filter((log) => {
+    if (filters.moods && filters.moods.length > 0) {
+      if (!log.mood || !filters.moods.includes(log.mood)) {
+        return false
+      }
+    }
+    return true
+  })
+
+  return (
+    <AuthenticatedLayout>
+      <div className="container mx-auto px-4 py-8 max-w-7xl">
+        <DailyLogHeader
+          logsCount={filteredLogs.length}
+          onToggleFilters={() => setShowFilters(!showFilters)}
+          onExport={handleExport}
+          showFilters={showFilters}
+        />
+
+      {/* Filtros */}
+      {showFilters && (
+        <div className="mb-6">
+          <FiltersComponent filters={filters} onFiltersChange={setFilters} />
+        </div>
+      )}
+
+      {/* Estadísticas */}
+      <div className="mb-8">
+        <DailyLogStats startDate={dateRange.start} endDate={dateRange.end} />
+      </div>
+
+      {/* Tabs: Calendario, Tendencias, Lista */}
+      <Tabs defaultValue="calendar" className="w-full">
+        <TabsList className="grid w-full grid-cols-3 mb-6">
+          <TabsTrigger value="calendar">
+            <Calendar className="w-4 h-4 mr-2" />
+            Calendario
+          </TabsTrigger>
+          <TabsTrigger value="trends">
+            <TrendingUp className="w-4 h-4 mr-2" />
+            Tendencias
+          </TabsTrigger>
+          <TabsTrigger value="list">
+            Lista
+          </TabsTrigger>
+        </TabsList>
+
+        <TabsContent value="calendar">
+          <DailyLogCalendar logs={filteredLogs} onDateClick={handleDateClick} />
+        </TabsContent>
+
+        <TabsContent value="trends">
+          <div className="bg-white rounded-lg border p-6">
+            <h3 className="text-lg font-semibold text-gray-900 mb-4">
+              Tendencias de los últimos 14 días
+            </h3>
+            <TrendChart logs={filteredLogs} />
+          </div>
+        </TabsContent>
+
+        <TabsContent value="list">
+          <DailyLogList
+            logs={filteredLogs}
+            loading={loading}
+            onEdit={handleEdit}
+            onDelete={handleDelete}
+            onAddFirst={handleAddNew}
+          />
+        </TabsContent>
+      </Tabs>
+
+      {/* Modal de formulario */}
+      <Dialog open={isFormOpen} onOpenChange={setIsFormOpen}>
+        <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
+          <DialogHeader>
+            <DialogTitle>
+              {selectedLog ? "Editar registro" : "Nuevo registro"}
+            </DialogTitle>
+          </DialogHeader>
+          <DailyLogEntryForm
+            initialData={selectedLog || undefined}
+            date={selectedDate}
+            onSubmit={handleSubmit}
+            onCancel={() => setIsFormOpen(false)}
+            loading={loading}
+          />
+        </DialogContent>
+      </Dialog>
+
+      {/* Delete Confirmation Dialog */}
+      {logToDelete && (
+        <DeleteConfirmDialog
+          isOpen={deleteDialogOpen}
+          onOpenChange={setDeleteDialogOpen}
+          onConfirm={confirmDelete}
+          date={logToDelete}
+        />
+      )}
+
+      {/* Quick Add Button */}
+      <QuickAddButton onSubmit={handleSubmit} />
+      </div>
+    </AuthenticatedLayout>
+  )
+}

+ 71 - 0
src/components/daily-log/CalendarDay.tsx

@@ -0,0 +1,71 @@
+"use client"
+
+import { cn } from "@/lib/utils"
+import { DailyLog } from "@/types/daily-log"
+
+interface CalendarDayProps {
+  date: Date
+  log?: DailyLog
+  isCurrentMonth: boolean
+  isToday: boolean
+  onClick: () => void
+}
+
+const getMoodColor = (mood?: number) => {
+  if (!mood) return "bg-gray-50"
+  const colors = [
+    "bg-red-100 border-red-200",
+    "bg-orange-100 border-orange-200",
+    "bg-yellow-100 border-yellow-200",
+    "bg-green-100 border-green-200",
+    "bg-emerald-100 border-emerald-200",
+  ]
+  return colors[mood - 1]
+}
+
+const getMoodEmoji = (mood?: number) => {
+  if (!mood) return null
+  const emojis = ["😢", "😕", "😐", "🙂", "😄"]
+  return emojis[mood - 1]
+}
+
+export function CalendarDay({
+  date,
+  log,
+  isCurrentMonth,
+  isToday,
+  onClick,
+}: CalendarDayProps) {
+  const dayNumber = date.getDate()
+  const hasLog = !!log
+
+  return (
+    <button
+      onClick={onClick}
+      disabled={!isCurrentMonth}
+      className={cn(
+        "aspect-square p-2 rounded-lg border-2 transition-all",
+        "hover:shadow-md hover:scale-105",
+        isToday && "ring-2 ring-primary ring-offset-2",
+        !isCurrentMonth && "opacity-30 cursor-not-allowed",
+        hasLog ? getMoodColor(log.mood) : "bg-white border-gray-200",
+        !hasLog && isCurrentMonth && "hover:bg-gray-50"
+      )}
+    >
+      <div className="flex flex-col items-center justify-center h-full">
+        <span
+          className={cn(
+            "text-sm font-medium",
+            isToday && "font-bold",
+            hasLog ? "text-gray-900" : "text-gray-600"
+          )}
+        >
+          {dayNumber}
+        </span>
+        {hasLog && (
+          <span className="text-xl mt-1">{getMoodEmoji(log.mood)}</span>
+        )}
+      </div>
+    </button>
+  )
+}

+ 53 - 0
src/components/daily-log/CalendarHeader.tsx

@@ -0,0 +1,53 @@
+"use client"
+
+import { ChevronLeft, ChevronRight } from "lucide-react"
+import { Button } from "@/components/ui/button"
+
+interface CalendarHeaderProps {
+  currentDate: Date
+  onPreviousMonth: () => void
+  onNextMonth: () => void
+}
+
+export function CalendarHeader({
+  currentDate,
+  onPreviousMonth,
+  onNextMonth,
+}: CalendarHeaderProps) {
+  const monthYear = currentDate.toLocaleDateString("es-ES", {
+    month: "long",
+    year: "numeric",
+  })
+
+  const canGoNext = () => {
+    const today = new Date()
+    const nextMonth = new Date(currentDate)
+    nextMonth.setMonth(nextMonth.getMonth() + 1)
+    return nextMonth <= today
+  }
+
+  return (
+    <div className="flex items-center justify-between mb-4">
+      <h3 className="text-lg font-semibold text-gray-900 capitalize">
+        {monthYear}
+      </h3>
+      <div className="flex gap-2">
+        <Button
+          variant="outline"
+          size="sm"
+          onClick={onPreviousMonth}
+        >
+          <ChevronLeft className="w-4 h-4" />
+        </Button>
+        <Button
+          variant="outline"
+          size="sm"
+          onClick={onNextMonth}
+          disabled={!canGoNext()}
+        >
+          <ChevronRight className="w-4 h-4" />
+        </Button>
+      </div>
+    </div>
+  )
+}

+ 170 - 0
src/components/daily-log/DailyLogCalendar.tsx

@@ -0,0 +1,170 @@
+"use client"
+
+import { useState, useEffect } from "react"
+import { DailyLog } from "@/types/daily-log"
+import { CalendarHeader } from "./CalendarHeader"
+import { CalendarDay } from "./CalendarDay"
+
+interface DailyLogCalendarProps {
+  logs: DailyLog[]
+  onDateClick: (date: string, log?: DailyLog) => void
+}
+
+const WEEKDAYS = ["Dom", "Lun", "Mar", "Mié", "Jue", "Vie", "Sáb"]
+
+export function DailyLogCalendar({ logs, onDateClick }: DailyLogCalendarProps) {
+  const [currentDate, setCurrentDate] = useState(new Date())
+  const [calendarDays, setCalendarDays] = useState<Date[]>([])
+
+  useEffect(() => {
+    generateCalendar()
+  }, [currentDate])
+
+  const generateCalendar = () => {
+    const year = currentDate.getFullYear()
+    const month = currentDate.getMonth()
+
+    // Primer día del mes
+    const firstDay = new Date(year, month, 1)
+    // Último día del mes
+    const lastDay = new Date(year, month + 1, 0)
+
+    // Día de la semana del primer día (0 = domingo)
+    const startingDayOfWeek = firstDay.getDay()
+
+    const days: Date[] = []
+
+    // Días del mes anterior para completar la primera semana
+    for (let i = startingDayOfWeek - 1; i >= 0; i--) {
+      const date = new Date(year, month, -i)
+      days.push(date)
+    }
+
+    // Días del mes actual
+    for (let day = 1; day <= lastDay.getDate(); day++) {
+      days.push(new Date(year, month, day))
+    }
+
+    // Días del mes siguiente para completar la última semana
+    const remainingDays = 7 - (days.length % 7)
+    if (remainingDays < 7) {
+      for (let i = 1; i <= remainingDays; i++) {
+        days.push(new Date(year, month + 1, i))
+      }
+    }
+
+    setCalendarDays(days)
+  }
+
+  const handlePreviousMonth = () => {
+    setCurrentDate(
+      new Date(currentDate.getFullYear(), currentDate.getMonth() - 1)
+    )
+  }
+
+  const handleNextMonth = () => {
+    setCurrentDate(
+      new Date(currentDate.getFullYear(), currentDate.getMonth() + 1)
+    )
+  }
+
+  const getLogForDate = (date: Date): DailyLog | undefined => {
+    const dateString = date.toISOString().split("T")[0]
+    const foundLog = logs.find((log) => {
+      // Convertir la fecha del log a string YYYY-MM-DD
+      const logDate = log.date as any // Prisma puede devolver Date o string
+      let logDateString: string
+      if (logDate instanceof Date) {
+        logDateString = logDate.toISOString().split("T")[0]
+      } else if (typeof logDate === 'string') {
+        // Si ya es string, tomar solo la parte de fecha
+        logDateString = logDate.split('T')[0]
+      } else {
+        logDateString = String(logDate).split('T')[0]
+      }
+      return logDateString === dateString
+    })
+    return foundLog
+  }
+
+  const isCurrentMonth = (date: Date) => {
+    return date.getMonth() === currentDate.getMonth()
+  }
+
+  const isToday = (date: Date) => {
+    const today = new Date()
+    return (
+      date.getDate() === today.getDate() &&
+      date.getMonth() === today.getMonth() &&
+      date.getFullYear() === today.getFullYear()
+    )
+  }
+
+  return (
+    <div className="bg-white rounded-lg border p-4">
+      <CalendarHeader
+        currentDate={currentDate}
+        onPreviousMonth={handlePreviousMonth}
+        onNextMonth={handleNextMonth}
+      />
+
+      {/* Días de la semana */}
+      <div className="grid grid-cols-7 gap-2 mb-2">
+        {WEEKDAYS.map((day) => (
+          <div
+            key={day}
+            className="text-center text-xs font-medium text-gray-500 py-2"
+          >
+            {day}
+          </div>
+        ))}
+      </div>
+
+      {/* Días del calendario */}
+      <div className="grid grid-cols-7 gap-2">
+        {calendarDays.map((date, index) => {
+          const log = getLogForDate(date)
+          return (
+            <CalendarDay
+              key={index}
+              date={date}
+              log={log}
+              isCurrentMonth={isCurrentMonth(date)}
+              isToday={isToday(date)}
+              onClick={() => {
+                if (isCurrentMonth(date)) {
+                  const dateString = date.toISOString().split("T")[0]
+                  onDateClick(dateString, log)
+                }
+              }}
+            />
+          )
+        })}
+      </div>
+
+      {/* Leyenda */}
+      <div className="mt-4 pt-4 border-t flex flex-wrap gap-3 text-xs text-gray-600">
+        <div className="flex items-center gap-1">
+          <div className="w-4 h-4 rounded bg-red-100 border-2 border-red-200" />
+          <span>Muy mal</span>
+        </div>
+        <div className="flex items-center gap-1">
+          <div className="w-4 h-4 rounded bg-orange-100 border-2 border-orange-200" />
+          <span>Mal</span>
+        </div>
+        <div className="flex items-center gap-1">
+          <div className="w-4 h-4 rounded bg-yellow-100 border-2 border-yellow-200" />
+          <span>Normal</span>
+        </div>
+        <div className="flex items-center gap-1">
+          <div className="w-4 h-4 rounded bg-green-100 border-2 border-green-200" />
+          <span>Bien</span>
+        </div>
+        <div className="flex items-center gap-1">
+          <div className="w-4 h-4 rounded bg-emerald-100 border-2 border-emerald-200" />
+          <span>Excelente</span>
+        </div>
+      </div>
+    </div>
+  )
+}

+ 110 - 0
src/components/daily-log/DailyLogCard.tsx

@@ -0,0 +1,110 @@
+"use client"
+
+import { DailyLog } from "@/types/daily-log"
+import { Button } from "@/components/ui/button"
+import { Pencil, Trash2 } from "lucide-react"
+
+interface DailyLogCardProps {
+  log: DailyLog
+  onEdit: (log: DailyLog) => void
+  onDelete: (date: string) => void
+}
+
+const getMoodEmoji = (mood?: number) => {
+  if (!mood) return null
+  const emojis = ["😢", "😕", "😐", "🙂", "😄"]
+  return emojis[mood - 1]
+}
+
+const getEnergyIcon = (energy?: number) => {
+  if (!energy) return null
+  const icons = ["🪫", "🔋", "🔋", "🔋", "⚡"]
+  return icons[energy - 1]
+}
+
+const getMoodColor = (mood?: number) => {
+  if (!mood) return "bg-gray-100"
+  const colors = [
+    "bg-red-100",
+    "bg-orange-100",
+    "bg-yellow-100",
+    "bg-green-100",
+    "bg-emerald-100",
+  ]
+  return colors[mood - 1]
+}
+
+export function DailyLogCard({ log, onEdit, onDelete }: DailyLogCardProps) {
+  const formattedDate = new Date(log.date).toLocaleDateString("es-ES", {
+    weekday: "short",
+    month: "short",
+    day: "numeric",
+  })
+
+  return (
+    <div
+      className={`p-4 rounded-lg border-2 ${getMoodColor(
+        log.mood
+      )} border-transparent hover:border-primary/30 transition-all cursor-pointer group`}
+      onClick={() => onEdit(log)}
+    >
+      <div className="flex items-start justify-between gap-4">
+        {/* Fecha y datos */}
+        <div className="flex-1 space-y-2">
+          <div className="flex items-center gap-2">
+            <h4 className="font-semibold text-gray-900 capitalize">
+              {formattedDate}
+            </h4>
+            {log.mood && (
+              <span className="text-2xl">{getMoodEmoji(log.mood)}</span>
+            )}
+            {log.energy && (
+              <span className="text-2xl">{getEnergyIcon(log.energy)}</span>
+            )}
+          </div>
+
+          {/* Información resumida */}
+          <div className="flex flex-wrap gap-3 text-sm text-gray-600">
+            {log.sleepHours && (
+              <span className="flex items-center gap-1">
+                😴 {log.sleepHours}h
+                {log.sleepQuality && ` (${log.sleepQuality}/5)`}
+              </span>
+            )}
+          </div>
+
+          {/* Notas preview */}
+          {log.notes && (
+            <p className="text-sm text-gray-600 line-clamp-2">{log.notes}</p>
+          )}
+        </div>
+
+        {/* Botones de acción */}
+        <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
+          <Button
+            size="sm"
+            variant="ghost"
+            onClick={(e) => {
+              e.stopPropagation()
+              onEdit(log)
+            }}
+          >
+            <Pencil className="h-4 w-4" />
+          </Button>
+          <Button
+            size="sm"
+            variant="ghost"
+            onClick={(e) => {
+              e.stopPropagation()
+              if (confirm("¿Eliminar este registro?")) {
+                onDelete(log.date)
+              }
+            }}
+          >
+            <Trash2 className="h-4 w-4 text-red-600" />
+          </Button>
+        </div>
+      </div>
+    </div>
+  )
+}

+ 28 - 0
src/components/daily-log/DailyLogEmptyState.tsx

@@ -0,0 +1,28 @@
+"use client"
+
+import { BookOpen } from "lucide-react"
+import { Button } from "@/components/ui/button"
+
+interface DailyLogEmptyStateProps {
+  onAddFirst: () => void
+}
+
+export function DailyLogEmptyState({ onAddFirst }: DailyLogEmptyStateProps) {
+  return (
+    <div className="flex flex-col items-center justify-center py-12 px-4 text-center">
+      <div className="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mb-4">
+        <BookOpen className="w-8 h-8 text-primary" />
+      </div>
+      <h3 className="text-lg font-semibold text-gray-900 mb-2">
+        Comienza tu diario
+      </h3>
+      <p className="text-gray-600 mb-6 max-w-md">
+        Registra cómo te sientes cada día y lleva un seguimiento de tu bienestar.
+        Identifica patrones y mejora tu salud mental.
+      </p>
+      <Button onClick={onAddFirst}>
+        Crear mi primer registro
+      </Button>
+    </div>
+  )
+}

+ 121 - 0
src/components/daily-log/DailyLogEntryForm.tsx

@@ -0,0 +1,121 @@
+"use client"
+
+import { useState, useEffect } from "react"
+import { DailyLogInput } from "@/types/daily-log"
+import { MoodSelector } from "./MoodSelector"
+import { EnergySelector } from "./EnergySelector"
+import { SleepInput } from "./SleepInput"
+import { Button } from "@/components/ui/button"
+
+interface DailyLogEntryFormProps {
+  initialData?: DailyLogInput
+  date: string
+  onSubmit: (data: DailyLogInput) => Promise<void>
+  onCancel?: () => void
+  loading?: boolean
+}
+
+export function DailyLogEntryForm({
+  initialData,
+  date,
+  onSubmit,
+  onCancel,
+  loading,
+}: DailyLogEntryFormProps) {
+  const [formData, setFormData] = useState<DailyLogInput>({
+    date,
+    mood: initialData?.mood,
+    energy: initialData?.energy,
+    sleepHours: initialData?.sleepHours,
+    sleepQuality: initialData?.sleepQuality,
+    notes: initialData?.notes || "",
+  })
+
+  useEffect(() => {
+    if (initialData) {
+      setFormData({ ...initialData, date })
+    }
+  }, [initialData, date])
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault()
+    await onSubmit(formData)
+  }
+
+  const isValid = formData.mood || formData.energy || formData.sleepHours || formData.notes
+
+  return (
+    <form onSubmit={handleSubmit} className="space-y-6">
+      {/* Fecha */}
+      <div className="text-center">
+        <h3 className="text-lg font-semibold text-gray-900">
+          {new Date(date).toLocaleDateString("es-ES", {
+            weekday: "long",
+            year: "numeric",
+            month: "long",
+            day: "numeric",
+          })}
+        </h3>
+        <p className="text-sm text-gray-500 mt-1">
+          Registra cómo te sentiste este día
+        </p>
+      </div>
+
+      {/* Ánimo */}
+      <MoodSelector
+        value={formData.mood}
+        onChange={(mood) => setFormData({ ...formData, mood })}
+        disabled={loading}
+      />
+
+      {/* Energía */}
+      <EnergySelector
+        value={formData.energy}
+        onChange={(energy) => setFormData({ ...formData, energy })}
+        disabled={loading}
+      />
+
+      {/* Sueño */}
+      <SleepInput
+        hours={formData.sleepHours}
+        quality={formData.sleepQuality}
+        onHoursChange={(sleepHours) => setFormData({ ...formData, sleepHours })}
+        onQualityChange={(sleepQuality) => setFormData({ ...formData, sleepQuality })}
+        disabled={loading}
+      />
+
+      {/* Notas */}
+      <div className="space-y-2">
+        <label htmlFor="notes" className="text-sm font-medium">
+          Notas adicionales (opcional)
+        </label>
+        <textarea
+          id="notes"
+          value={formData.notes}
+          onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
+          disabled={loading}
+          placeholder="¿Algo más que quieras recordar sobre este día?"
+          rows={4}
+          className="w-full px-4 py-2 rounded-lg border border-gray-300 focus:ring-2 focus:ring-primary focus:border-transparent resize-none disabled:opacity-50 disabled:cursor-not-allowed"
+        />
+      </div>
+
+      {/* Botones */}
+      <div className="flex gap-3 justify-end pt-4 border-t">
+        {onCancel && (
+          <Button
+            type="button"
+            variant="outline"
+            onClick={onCancel}
+            disabled={loading}
+          >
+            Cancelar
+          </Button>
+        )}
+        <Button type="submit" disabled={!isValid || loading}>
+          {loading ? "Guardando..." : "Guardar registro"}
+        </Button>
+      </div>
+    </form>
+  )
+}

+ 85 - 0
src/components/daily-log/DailyLogFilters.tsx

@@ -0,0 +1,85 @@
+"use client"
+
+import { Card, CardContent } from "@/components/ui/card"
+import { DateRangeSelector, DateRangePreset } from "./DateRangeSelector"
+import { MoodFilter } from "./MoodFilter"
+import { DailyLogFilters as FilterTypes } from "@/types/daily-log"
+
+interface DailyLogFiltersProps {
+  filters: FilterTypes
+  onFiltersChange: (filters: FilterTypes) => void
+}
+
+export function DailyLogFilters({
+  filters,
+  onFiltersChange,
+}: DailyLogFiltersProps) {
+  const handleDateRangeChange = (preset: DateRangePreset) => {
+    const now = new Date()
+    let startDate: Date
+
+    switch (preset) {
+      case "week":
+        startDate = new Date(now)
+        startDate.setDate(now.getDate() - 7)
+        break
+      case "month":
+        startDate = new Date(now)
+        startDate.setMonth(now.getMonth() - 1)
+        break
+      case "3months":
+        startDate = new Date(now)
+        startDate.setMonth(now.getMonth() - 3)
+        break
+      case "all":
+        startDate = new Date(0) // Epoch
+        break
+    }
+
+    onFiltersChange({
+      ...filters,
+      startDate: startDate.toISOString().split("T")[0],
+      endDate: now.toISOString().split("T")[0],
+    })
+  }
+
+  const handleMoodChange = (moods: number[]) => {
+    onFiltersChange({
+      ...filters,
+      moods: moods.length > 0 ? moods : undefined,
+    })
+  }
+
+  // Detectar preset actual
+  const currentPreset = (): DateRangePreset => {
+    if (!filters.startDate) return "all"
+
+    const now = new Date()
+    const start = new Date(filters.startDate)
+    const daysDiff = Math.round(
+      (now.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)
+    )
+
+    if (daysDiff <= 7) return "week"
+    if (daysDiff <= 31) return "month"
+    if (daysDiff <= 93) return "3months"
+    return "all"
+  }
+
+  return (
+    <Card>
+      <CardContent className="p-4">
+        <div className="space-y-3">
+          <DateRangeSelector
+            value={currentPreset()}
+            onChange={handleDateRangeChange}
+          />
+          <MoodFilter
+            selectedMoods={filters.moods || []}
+            onChange={handleMoodChange}
+          />
+        </div>
+      </CardContent>
+    </Card>
+  )
+}

+ 60 - 0
src/components/daily-log/DailyLogHeader.tsx

@@ -0,0 +1,60 @@
+"use client"
+
+import { Calendar, Filter } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { ExportButton } from "./ExportButton"
+
+interface DailyLogHeaderProps {
+  logsCount: number
+  onToggleFilters: () => void
+  onExport: (preset: any) => void
+  showFilters: boolean
+}
+
+export function DailyLogHeader({
+  logsCount,
+  onToggleFilters,
+  onExport,
+  showFilters
+}: DailyLogHeaderProps) {
+  return (
+    <div className="mb-6">
+      <div className="bg-card rounded-xl p-6 border shadow-sm">
+        <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
+          <div className="flex items-center space-x-3 flex-1 min-w-0">
+            <div className="w-10 h-10 flex-shrink-0 bg-primary rounded-lg flex items-center justify-center shadow-sm">
+              <Calendar className="w-5 h-5 text-primary-foreground" />
+            </div>
+            <div className="min-w-0">
+              <h1 className="text-xl font-bold text-foreground">
+                Mi Diario Personal
+              </h1>
+              <p className="text-sm text-muted-foreground">
+                Registra cómo te sientes cada día y lleva un seguimiento de tu bienestar emocional
+              </p>
+            </div>
+          </div>
+          <div className="text-right">
+            <div className="bg-muted rounded-lg p-3 shadow-sm border">
+              <div className="text-2xl font-bold text-primary">{logsCount}</div>
+              <div className="text-xs text-muted-foreground font-medium">
+                {logsCount === 1 ? "Registro" : "Registros"}
+              </div>
+            </div>
+          </div>
+        </div>
+        <div className="flex gap-2 mt-4">
+          <Button
+            onClick={onToggleFilters}
+            variant="outline"
+            size="sm"
+          >
+            <Filter className="w-4 h-4 mr-2" />
+            Filtros
+          </Button>
+          <ExportButton onExport={onExport} />
+        </div>
+      </div>
+    </div>
+  )
+}

+ 43 - 0
src/components/daily-log/DailyLogList.tsx

@@ -0,0 +1,43 @@
+"use client"
+
+import { DailyLog } from "@/types/daily-log"
+import { DailyLogCard } from "./DailyLogCard"
+import { DailyLogEmptyState } from "./DailyLogEmptyState"
+import { DailyLogListSkeleton } from "./DailyLogSkeleton"
+
+interface DailyLogListProps {
+  logs: DailyLog[]
+  onEdit: (log: DailyLog) => void
+  onDelete: (date: string) => void
+  onAddFirst: () => void
+  loading?: boolean
+}
+
+export function DailyLogList({
+  logs,
+  onEdit,
+  onDelete,
+  onAddFirst,
+  loading,
+}: DailyLogListProps) {
+  if (loading) {
+    return <DailyLogListSkeleton count={3} />
+  }
+
+  if (logs.length === 0) {
+    return <DailyLogEmptyState onAddFirst={onAddFirst} />
+  }
+
+  return (
+    <div className="space-y-3">
+      {logs.map((log) => (
+        <DailyLogCard
+          key={log.id}
+          log={log}
+          onEdit={onEdit}
+          onDelete={onDelete}
+        />
+      ))}
+    </div>
+  )
+}

+ 62 - 0
src/components/daily-log/DailyLogSkeleton.tsx

@@ -0,0 +1,62 @@
+"use client"
+
+import { Card, CardContent } from "@/components/ui/card"
+
+export function DailyLogSkeleton() {
+  return (
+    <Card>
+      <CardContent className="p-6">
+        <div className="space-y-4 animate-pulse">
+          {/* Fecha */}
+          <div className="flex items-center justify-between">
+            <div className="h-5 w-32 bg-muted rounded" />
+            <div className="h-4 w-24 bg-muted rounded" />
+          </div>
+
+          {/* Mood y Energy */}
+          <div className="grid grid-cols-2 gap-4">
+            <div className="space-y-2">
+              <div className="h-4 w-16 bg-muted rounded" />
+              <div className="h-8 w-full bg-muted rounded" />
+            </div>
+            <div className="space-y-2">
+              <div className="h-4 w-16 bg-muted rounded" />
+              <div className="h-8 w-full bg-muted rounded" />
+            </div>
+          </div>
+
+          {/* Sleep */}
+          <div className="space-y-2">
+            <div className="h-4 w-20 bg-muted rounded" />
+            <div className="h-8 w-full bg-muted rounded" />
+          </div>
+
+          {/* Notes */}
+          <div className="space-y-2">
+            <div className="h-4 w-16 bg-muted rounded" />
+            <div className="space-y-2">
+              <div className="h-4 w-full bg-muted rounded" />
+              <div className="h-4 w-3/4 bg-muted rounded" />
+            </div>
+          </div>
+
+          {/* Actions */}
+          <div className="flex gap-2 justify-end pt-2">
+            <div className="h-9 w-20 bg-muted rounded" />
+            <div className="h-9 w-20 bg-muted rounded" />
+          </div>
+        </div>
+      </CardContent>
+    </Card>
+  )
+}
+
+export function DailyLogListSkeleton({ count = 3 }: { count?: number }) {
+  return (
+    <div className="space-y-4">
+      {Array.from({ length: count }).map((_, i) => (
+        <DailyLogSkeleton key={i} />
+      ))}
+    </div>
+  )
+}

+ 124 - 0
src/components/daily-log/DailyLogStats.tsx

@@ -0,0 +1,124 @@
+"use client"
+
+import { useEffect, useState } from "react"
+import { StatsCard } from "./StatsCard"
+import { Smile, Zap, Moon, Calendar, TrendingUp } from "lucide-react"
+
+interface DailyLogStatsProps {
+  startDate: string
+  endDate: string
+}
+
+interface Stats {
+  totalEntries: number
+  avgMood: number | null
+  avgEnergy: number | null
+  avgSleep: number | null
+  avgSleepQuality: number | null
+  streak: number
+}
+
+const getMoodEmoji = (mood: number) => {
+  const emojis = ["😢", "😕", "😐", "🙂", "😄"]
+  return emojis[Math.round(mood) - 1] || "😐"
+}
+
+const getEnergyEmoji = (energy: number) => {
+  const emojis = ["🪫", "🔋", "🔋", "🔋", "⚡"]
+  return emojis[Math.round(energy) - 1] || "🔋"
+}
+
+export function DailyLogStats({ startDate, endDate }: DailyLogStatsProps) {
+  const [stats, setStats] = useState<Stats | null>(null)
+  const [loading, setLoading] = useState(true)
+
+  useEffect(() => {
+    const fetchStats = async () => {
+      try {
+        const params = new URLSearchParams({ startDate, endDate })
+        const response = await fetch(`/api/daily-log/stats?${params}`)
+
+        if (response.ok) {
+          const data = await response.json()
+          setStats(data)
+        }
+      } catch (error) {
+        console.error("Error al cargar estadísticas:", error)
+      } finally {
+        setLoading(false)
+      }
+    }
+
+    fetchStats()
+  }, [startDate, endDate])
+
+  if (loading) {
+    return (
+      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
+        {[...Array(4)].map((_, i) => (
+          <div key={i} className="h-32 bg-gray-100 rounded-lg animate-pulse" />
+        ))}
+      </div>
+    )
+  }
+
+  if (!stats) {
+    return null
+  }
+
+  return (
+    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
+      <StatsCard
+        title="Registros"
+        value={stats.totalEntries}
+        icon={Calendar}
+        description="Total de días registrados"
+        color="blue"
+      />
+
+      <StatsCard
+        title="Ánimo promedio"
+        value={
+          stats.avgMood
+            ? `${getMoodEmoji(stats.avgMood)} ${stats.avgMood}/5`
+            : "Sin datos"
+        }
+        icon={Smile}
+        description={stats.avgMood ? "Estado de ánimo general" : undefined}
+        color="green"
+      />
+
+      <StatsCard
+        title="Energía promedio"
+        value={
+          stats.avgEnergy
+            ? `${getEnergyEmoji(stats.avgEnergy)} ${stats.avgEnergy}/5`
+            : "Sin datos"
+        }
+        icon={Zap}
+        description={stats.avgEnergy ? "Nivel de vitalidad" : undefined}
+        color="orange"
+      />
+
+      <StatsCard
+        title={stats.streak > 0 ? "¡Racha activa! 🔥" : "Sueño promedio"}
+        value={
+          stats.streak > 0
+            ? `${stats.streak} ${stats.streak === 1 ? "día" : "días"}`
+            : stats.avgSleep
+            ? `${stats.avgSleep}h`
+            : "Sin datos"
+        }
+        icon={stats.streak > 0 ? TrendingUp : Moon}
+        description={
+          stats.streak > 0
+            ? "Días consecutivos registrando"
+            : stats.avgSleep
+            ? `Calidad: ${stats.avgSleepQuality || "N/A"}/5`
+            : undefined
+        }
+        color={stats.streak > 0 ? "pink" : "purple"}
+      />
+    </div>
+  )
+}

+ 39 - 0
src/components/daily-log/DateRangeSelector.tsx

@@ -0,0 +1,39 @@
+"use client"
+
+import { Button } from "@/components/ui/button"
+import { Calendar } from "lucide-react"
+
+export type DateRangePreset = "week" | "month" | "3months" | "all"
+
+interface DateRangeSelectorProps {
+  value: DateRangePreset
+  onChange: (preset: DateRangePreset) => void
+}
+
+export function DateRangeSelector({ value, onChange }: DateRangeSelectorProps) {
+  const presets: { value: DateRangePreset; label: string }[] = [
+    { value: "week", label: "Última semana" },
+    { value: "month", label: "Último mes" },
+    { value: "3months", label: "3 meses" },
+    { value: "all", label: "Todo" },
+  ]
+
+  return (
+    <div className="flex flex-wrap items-center gap-2">
+      <Calendar className="h-4 w-4 text-muted-foreground" />
+      <span className="text-sm font-medium text-muted-foreground">Período:</span>
+      <div className="flex gap-1">
+        {presets.map((preset) => (
+          <Button
+            key={preset.value}
+            variant={value === preset.value ? "default" : "outline"}
+            size="sm"
+            onClick={() => onChange(preset.value)}
+          >
+            {preset.label}
+          </Button>
+        ))}
+      </div>
+    </div>
+  )
+}

+ 68 - 0
src/components/daily-log/DeleteConfirmDialog.tsx

@@ -0,0 +1,68 @@
+"use client"
+
+import {
+  AlertDialog,
+  AlertDialogAction,
+  AlertDialogCancel,
+  AlertDialogContent,
+  AlertDialogDescription,
+  AlertDialogFooter,
+  AlertDialogHeader,
+  AlertDialogTitle,
+} from "@/components/ui/alert-dialog"
+import { AlertTriangle } from "lucide-react"
+
+interface DeleteConfirmDialogProps {
+  isOpen: boolean
+  onOpenChange: (open: boolean) => void
+  onConfirm: () => void
+  date: string
+}
+
+export function DeleteConfirmDialog({
+  isOpen,
+  onOpenChange,
+  onConfirm,
+  date,
+}: DeleteConfirmDialogProps) {
+  const formattedDate = new Date(date + "T00:00:00").toLocaleDateString(
+    "es-ES",
+    {
+      weekday: "long",
+      year: "numeric",
+      month: "long",
+      day: "numeric",
+    }
+  )
+
+  return (
+    <AlertDialog open={isOpen} onOpenChange={onOpenChange}>
+      <AlertDialogContent>
+        <AlertDialogHeader>
+          <div className="flex items-center gap-3">
+            <div className="flex h-12 w-12 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/20">
+              <AlertTriangle className="h-6 w-6 text-red-600 dark:text-red-400" />
+            </div>
+            <AlertDialogTitle>¿Eliminar registro?</AlertDialogTitle>
+          </div>
+          <AlertDialogDescription className="pt-3">
+            Estás a punto de eliminar el registro del{" "}
+            <span className="font-semibold text-foreground">
+              {formattedDate}
+            </span>
+            . Esta acción no se puede deshacer.
+          </AlertDialogDescription>
+        </AlertDialogHeader>
+        <AlertDialogFooter>
+          <AlertDialogCancel>Cancelar</AlertDialogCancel>
+          <AlertDialogAction
+            onClick={onConfirm}
+            className="bg-red-600 hover:bg-red-700 focus:ring-red-600"
+          >
+            Eliminar
+          </AlertDialogAction>
+        </AlertDialogFooter>
+      </AlertDialogContent>
+    </AlertDialog>
+  )
+}

+ 46 - 0
src/components/daily-log/EnergySelector.tsx

@@ -0,0 +1,46 @@
+"use client"
+
+import { cn } from "@/lib/utils"
+
+interface EnergySelectorProps {
+  value?: number
+  onChange: (value: number) => void
+  disabled?: boolean
+}
+
+const energyLevels = [
+  { value: 1, icon: "🪫", label: "Sin energía", color: "hover:bg-red-100" },
+  { value: 2, icon: "🔋", label: "Baja", color: "hover:bg-orange-100" },
+  { value: 3, icon: "🔋", label: "Media", color: "hover:bg-yellow-100" },
+  { value: 4, icon: "🔋", label: "Alta", color: "hover:bg-green-100" },
+  { value: 5, icon: "⚡", label: "Muy alta", color: "hover:bg-emerald-100" },
+]
+
+export function EnergySelector({ value, onChange, disabled }: EnergySelectorProps) {
+  return (
+    <div className="space-y-2">
+      <label className="text-sm font-medium">¿Cuál fue tu nivel de energía?</label>
+      <div className="flex gap-2 justify-between">
+        {energyLevels.map((level) => (
+          <button
+            key={level.value}
+            type="button"
+            onClick={() => onChange(level.value)}
+            disabled={disabled}
+            className={cn(
+              "flex flex-col items-center gap-1 p-3 rounded-lg border-2 transition-all",
+              value === level.value
+                ? "border-primary bg-primary/10 scale-110"
+                : "border-gray-200",
+              !disabled && level.color,
+              disabled && "opacity-50 cursor-not-allowed"
+            )}
+          >
+            <span className="text-3xl">{level.icon}</span>
+            <span className="text-xs text-gray-600">{level.label}</span>
+          </button>
+        ))}
+      </div>
+    </div>
+  )
+}

+ 40 - 0
src/components/daily-log/ExportButton.tsx

@@ -0,0 +1,40 @@
+"use client"
+
+import { useState } from "react"
+import { Download } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { ExportDialog } from "./ExportDialog"
+import { DateRangePreset } from "./DateRangeSelector"
+
+interface ExportButtonProps {
+  onExport: (preset: DateRangePreset) => void
+}
+
+export function ExportButton({ onExport }: ExportButtonProps) {
+  const [isOpen, setIsOpen] = useState(false)
+
+  const handleExport = (preset: DateRangePreset) => {
+    onExport(preset)
+    setIsOpen(false)
+  }
+
+  return (
+    <>
+      <Button
+        onClick={() => setIsOpen(true)}
+        variant="outline"
+        size="sm"
+        className="gap-2"
+      >
+        <Download className="h-4 w-4" />
+        Exportar CSV
+      </Button>
+
+      <ExportDialog
+        isOpen={isOpen}
+        onOpenChange={setIsOpen}
+        onExport={handleExport}
+      />
+    </>
+  )
+}

+ 92 - 0
src/components/daily-log/ExportDialog.tsx

@@ -0,0 +1,92 @@
+"use client"
+
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogHeader,
+  DialogTitle,
+  DialogFooter,
+} from "@/components/ui/dialog"
+import { Button } from "@/components/ui/button"
+import { DateRangePreset } from "./DateRangeSelector"
+import { Download, FileSpreadsheet } from "lucide-react"
+
+interface ExportDialogProps {
+  isOpen: boolean
+  onOpenChange: (open: boolean) => void
+  onExport: (preset: DateRangePreset) => void
+}
+
+export function ExportDialog({
+  isOpen,
+  onOpenChange,
+  onExport,
+}: ExportDialogProps) {
+  const presets: { value: DateRangePreset; label: string; description: string }[] = [
+    {
+      value: "week",
+      label: "Última semana",
+      description: "Los últimos 7 días de registros",
+    },
+    {
+      value: "month",
+      label: "Último mes",
+      description: "Los últimos 30 días de registros",
+    },
+    {
+      value: "3months",
+      label: "3 meses",
+      description: "Los últimos 90 días de registros",
+    },
+    {
+      value: "all",
+      label: "Todos los registros",
+      description: "Exportar todos tus datos históricos",
+    },
+  ]
+
+  return (
+    <Dialog open={isOpen} onOpenChange={onOpenChange}>
+      <DialogContent className="sm:max-w-[500px]">
+        <DialogHeader>
+          <div className="flex items-center gap-3">
+            <div className="flex h-12 w-12 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/20">
+              <FileSpreadsheet className="h-6 w-6 text-green-600 dark:text-green-400" />
+            </div>
+            <div>
+              <DialogTitle>Exportar a CSV</DialogTitle>
+              <DialogDescription>
+                Selecciona el período que deseas exportar
+              </DialogDescription>
+            </div>
+          </div>
+        </DialogHeader>
+
+        <div className="space-y-2">
+          {presets.map((preset) => (
+            <Button
+              key={preset.value}
+              variant="outline"
+              className="w-full justify-start h-auto py-3 px-4"
+              onClick={() => onExport(preset.value)}
+            >
+              <div className="text-left">
+                <div className="font-medium">{preset.label}</div>
+                <div className="text-xs text-muted-foreground">
+                  {preset.description}
+                </div>
+              </div>
+            </Button>
+          ))}
+        </div>
+
+        <DialogFooter>
+          <Button variant="ghost" onClick={() => onOpenChange(false)}>
+            Cancelar
+          </Button>
+        </DialogFooter>
+      </DialogContent>
+    </Dialog>
+  )
+}

+ 65 - 0
src/components/daily-log/MoodFilter.tsx

@@ -0,0 +1,65 @@
+"use client"
+
+import { Button } from "@/components/ui/button"
+import { Badge } from "@/components/ui/badge"
+import { Smile, X } from "lucide-react"
+
+const MOOD_EMOJIS = ["😢", "😕", "😐", "🙂", "😄"]
+const MOOD_LABELS = ["Muy mal", "Mal", "Normal", "Bien", "Excelente"]
+
+interface MoodFilterProps {
+  selectedMoods: number[]
+  onChange: (moods: number[]) => void
+}
+
+export function MoodFilter({ selectedMoods, onChange }: MoodFilterProps) {
+  const toggleMood = (mood: number) => {
+    if (selectedMoods.includes(mood)) {
+      onChange(selectedMoods.filter((m) => m !== mood))
+    } else {
+      onChange([...selectedMoods, mood].sort())
+    }
+  }
+
+  const clearFilters = () => {
+    onChange([])
+  }
+
+  return (
+    <div className="flex flex-wrap items-center gap-2">
+      <Smile className="h-4 w-4 text-muted-foreground" />
+      <span className="text-sm font-medium text-muted-foreground">Ánimo:</span>
+      <div className="flex flex-wrap gap-1">
+        {[1, 2, 3, 4, 5].map((mood) => (
+          <Button
+            key={mood}
+            variant={selectedMoods.includes(mood) ? "default" : "outline"}
+            size="sm"
+            onClick={() => toggleMood(mood)}
+            className="h-8 px-2"
+          >
+            <span className="text-base mr-1">{MOOD_EMOJIS[mood - 1]}</span>
+            <span className="text-xs">{MOOD_LABELS[mood - 1]}</span>
+          </Button>
+        ))}
+      </div>
+      {selectedMoods.length > 0 && (
+        <>
+          <div className="flex items-center gap-1">
+            <Badge variant="secondary" className="text-xs">
+              {selectedMoods.length} {selectedMoods.length === 1 ? "filtro" : "filtros"}
+            </Badge>
+            <Button
+              variant="ghost"
+              size="sm"
+              onClick={clearFilters}
+              className="h-6 w-6 p-0"
+            >
+              <X className="h-3 w-3" />
+            </Button>
+          </div>
+        </>
+      )}
+    </div>
+  )
+}

+ 46 - 0
src/components/daily-log/MoodSelector.tsx

@@ -0,0 +1,46 @@
+"use client"
+
+import { cn } from "@/lib/utils"
+
+interface MoodSelectorProps {
+  value?: number
+  onChange: (value: number) => void
+  disabled?: boolean
+}
+
+const moods = [
+  { value: 1, emoji: "😢", label: "Muy mal", color: "hover:bg-red-100" },
+  { value: 2, emoji: "😕", label: "Mal", color: "hover:bg-orange-100" },
+  { value: 3, emoji: "😐", label: "Normal", color: "hover:bg-yellow-100" },
+  { value: 4, emoji: "🙂", label: "Bien", color: "hover:bg-green-100" },
+  { value: 5, emoji: "😄", label: "Excelente", color: "hover:bg-emerald-100" },
+]
+
+export function MoodSelector({ value, onChange, disabled }: MoodSelectorProps) {
+  return (
+    <div className="space-y-2">
+      <label className="text-sm font-medium">¿Cómo te sentiste hoy?</label>
+      <div className="flex gap-2 justify-between">
+        {moods.map((mood) => (
+          <button
+            key={mood.value}
+            type="button"
+            onClick={() => onChange(mood.value)}
+            disabled={disabled}
+            className={cn(
+              "flex flex-col items-center gap-1 p-3 rounded-lg border-2 transition-all",
+              value === mood.value
+                ? "border-primary bg-primary/10 scale-110"
+                : "border-gray-200",
+              !disabled && mood.color,
+              disabled && "opacity-50 cursor-not-allowed"
+            )}
+          >
+            <span className="text-3xl">{mood.emoji}</span>
+            <span className="text-xs text-gray-600">{mood.label}</span>
+          </button>
+        ))}
+      </div>
+    </div>
+  )
+}

+ 60 - 0
src/components/daily-log/QuickAddButton.tsx

@@ -0,0 +1,60 @@
+"use client"
+
+import { useState } from "react"
+import { Plus } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogHeader,
+  DialogTitle,
+} from "@/components/ui/dialog"
+import { DailyLogEntryForm } from "./DailyLogEntryForm"
+import { DailyLogInput } from "@/types/daily-log"
+
+interface QuickAddButtonProps {
+  onSubmit: (log: DailyLogInput) => Promise<void>
+}
+
+export function QuickAddButton({ onSubmit }: QuickAddButtonProps) {
+  const [isOpen, setIsOpen] = useState(false)
+
+  const handleSubmit = async (log: DailyLogInput) => {
+    await onSubmit(log)
+    setIsOpen(false)
+  }
+
+  const today = new Date().toISOString().split("T")[0]
+
+  return (
+    <>
+      {/* Botón flotante */}
+      <Button
+        onClick={() => setIsOpen(true)}
+        size="lg"
+        className="fixed bottom-6 right-6 h-14 w-14 rounded-full shadow-lg hover:shadow-xl transition-all duration-200 hover:scale-110 z-50"
+        aria-label="Agregar registro de hoy"
+      >
+        <Plus className="h-6 w-6" />
+      </Button>
+
+      {/* Modal */}
+      <Dialog open={isOpen} onOpenChange={setIsOpen}>
+        <DialogContent className="sm:max-w-[500px]">
+          <DialogHeader>
+            <DialogTitle>¿Cómo te sientes hoy?</DialogTitle>
+            <DialogDescription>
+              Registra tu estado de ánimo, energía y sueño de hoy
+            </DialogDescription>
+          </DialogHeader>
+          <DailyLogEntryForm
+            date={today}
+            onSubmit={handleSubmit}
+            onCancel={() => setIsOpen(false)}
+          />
+        </DialogContent>
+      </Dialog>
+    </>
+  )
+}

+ 83 - 0
src/components/daily-log/SleepInput.tsx

@@ -0,0 +1,83 @@
+"use client"
+
+import { cn } from "@/lib/utils"
+
+interface SleepInputProps {
+  hours?: number
+  quality?: number
+  onHoursChange: (hours: number) => void
+  onQualityChange: (quality: number) => void
+  disabled?: boolean
+}
+
+const qualityLevels = [
+  { value: 1, label: "Muy mala" },
+  { value: 2, label: "Mala" },
+  { value: 3, label: "Regular" },
+  { value: 4, label: "Buena" },
+  { value: 5, label: "Excelente" },
+]
+
+export function SleepInput({
+  hours,
+  quality,
+  onHoursChange,
+  onQualityChange,
+  disabled,
+}: SleepInputProps) {
+  return (
+    <div className="space-y-4">
+      {/* Horas de sueño */}
+      <div className="space-y-2">
+        <label htmlFor="sleep-hours" className="text-sm font-medium">
+          ¿Cuántas horas dormiste?
+        </label>
+        <input
+          id="sleep-hours"
+          type="number"
+          min="0"
+          max="24"
+          step="0.5"
+          value={hours || ""}
+          onChange={(e) => {
+            const value = parseFloat(e.target.value)
+            if (!isNaN(value)) {
+              onHoursChange(value)
+            }
+          }}
+          disabled={disabled}
+          placeholder="8.0"
+          className={cn(
+            "w-full px-4 py-2 rounded-lg border border-gray-300 focus:ring-2 focus:ring-primary focus:border-transparent",
+            disabled && "opacity-50 cursor-not-allowed"
+          )}
+        />
+      </div>
+
+      {/* Calidad del sueño */}
+      <div className="space-y-2">
+        <label className="text-sm font-medium">¿Cómo fue la calidad de tu sueño?</label>
+        <div className="flex gap-2">
+          {qualityLevels.map((level) => (
+            <button
+              key={level.value}
+              type="button"
+              onClick={() => onQualityChange(level.value)}
+              disabled={disabled}
+              className={cn(
+                "flex-1 py-2 px-3 rounded-lg border-2 text-sm transition-all",
+                quality === level.value
+                  ? "border-primary bg-primary/10 font-medium"
+                  : "border-gray-200",
+                !disabled && "hover:bg-gray-50",
+                disabled && "opacity-50 cursor-not-allowed"
+              )}
+            >
+              {level.label}
+            </button>
+          ))}
+        </div>
+      </div>
+    </div>
+  )
+}

+ 49 - 0
src/components/daily-log/StatsCard.tsx

@@ -0,0 +1,49 @@
+"use client"
+
+import { LucideIcon } from "lucide-react"
+import { Card, CardContent } from "@/components/ui/card"
+
+interface StatsCardProps {
+  title: string
+  value: string | number
+  icon: LucideIcon
+  description?: string
+  color?: "blue" | "green" | "purple" | "orange" | "pink"
+}
+
+const colorClasses = {
+  blue: "bg-blue-100 text-blue-600",
+  green: "bg-green-100 text-green-600",
+  purple: "bg-purple-100 text-purple-600",
+  orange: "bg-orange-100 text-orange-600",
+  pink: "bg-pink-100 text-pink-600",
+}
+
+export function StatsCard({
+  title,
+  value,
+  icon: Icon,
+  description,
+  color = "blue",
+}: StatsCardProps) {
+  return (
+    <Card>
+      <CardContent className="p-6">
+        <div className="flex items-center justify-between">
+          <div className="flex-1">
+            <p className="text-sm font-medium text-gray-600 mb-1">{title}</p>
+            <p className="text-3xl font-bold text-gray-900">{value}</p>
+            {description && (
+              <p className="text-xs text-gray-500 mt-1">{description}</p>
+            )}
+          </div>
+          <div
+            className={`w-12 h-12 rounded-lg flex items-center justify-center ${colorClasses[color]}`}
+          >
+            <Icon className="w-6 h-6" />
+          </div>
+        </div>
+      </CardContent>
+    </Card>
+  )
+}

+ 97 - 0
src/components/daily-log/TrendChart.tsx

@@ -0,0 +1,97 @@
+"use client"
+
+import { DailyLog } from "@/types/daily-log"
+import {
+  LineChart,
+  Line,
+  XAxis,
+  YAxis,
+  CartesianGrid,
+  Tooltip,
+  Legend,
+  ResponsiveContainer,
+} from "recharts"
+
+interface TrendChartProps {
+  logs: DailyLog[]
+}
+
+export function TrendChart({ logs }: TrendChartProps) {
+  // Preparar datos para la gráfica
+  const chartData = logs
+    .filter((log) => log.mood || log.energy || log.sleepHours)
+    .map((log) => ({
+      date: new Date(log.date).toLocaleDateString("es-ES", {
+        month: "short",
+        day: "numeric",
+      }),
+      Ánimo: log.mood || null,
+      Energía: log.energy || null,
+      "Sueño (h)": log.sleepHours ? Number((log.sleepHours / 2).toFixed(1)) : null, // Dividir por 2 para que esté en la misma escala 1-5
+    }))
+    .slice(-14) // Últimos 14 días
+
+  if (chartData.length === 0) {
+    return (
+      <div className="flex items-center justify-center h-64 text-gray-500">
+        <div className="text-center">
+          <p className="text-lg font-medium">Sin datos suficientes</p>
+          <p className="text-sm">Registra más días para ver tus tendencias</p>
+        </div>
+      </div>
+    )
+  }
+
+  return (
+    <div className="w-full h-64">
+      <ResponsiveContainer width="100%" height="100%">
+        <LineChart data={chartData}>
+          <CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
+          <XAxis
+            dataKey="date"
+            tick={{ fontSize: 12 }}
+            stroke="#6b7280"
+          />
+          <YAxis
+            domain={[0, 5]}
+            ticks={[1, 2, 3, 4, 5]}
+            tick={{ fontSize: 12 }}
+            stroke="#6b7280"
+          />
+          <Tooltip
+            contentStyle={{
+              backgroundColor: "white",
+              border: "1px solid #e5e7eb",
+              borderRadius: "8px",
+            }}
+          />
+          <Legend />
+          <Line
+            type="monotone"
+            dataKey="Ánimo"
+            stroke="#10b981"
+            strokeWidth={2}
+            dot={{ fill: "#10b981", r: 4 }}
+            connectNulls
+          />
+          <Line
+            type="monotone"
+            dataKey="Energía"
+            stroke="#f59e0b"
+            strokeWidth={2}
+            dot={{ fill: "#f59e0b", r: 4 }}
+            connectNulls
+          />
+          <Line
+            type="monotone"
+            dataKey="Sueño (h)"
+            stroke="#8b5cf6"
+            strokeWidth={2}
+            dot={{ fill: "#8b5cf6", r: 4 }}
+            connectNulls
+          />
+        </LineChart>
+      </ResponsiveContainer>
+    </div>
+  )
+}

+ 1 - 1
src/components/dashboard/DashboardStats.tsx

@@ -57,7 +57,7 @@ export default function DashboardStats({ isPatient, isDoctor, isAdmin, stats }:
             {isPatient ? "Disponible" : stats.totalConsults}
           </div>
           <p className="text-xs text-muted-foreground">
-            {isPatient ? "3 consultas por sesión" : stats.consultsTrend + " desde el mes pasado"}
+            {isPatient ? "5 consultas por sesión" : stats.consultsTrend + " desde el mes pasado"}
           </p>
         </CardContent>
       </Card>

+ 3 - 4
src/components/dashboard/QuickActions.tsx

@@ -80,16 +80,15 @@ export default function QuickActions({ isAdmin, isDoctor, isPatient }: QuickActi
               title="Chat Médico"
               description="Consulta con el asistente virtual"
               href="/chat"
-              iconBg="bg-blue-100 dark:bg-blue-900/20"
+              iconBg="bg-blue-100 dark:bg-blue-400/20"
               iconColor="text-blue-600 dark:text-blue-400"
-              badge="3 disponibles"
             />
             <QuickActionButton
               icon={FileText}
               title="Mis Reportes"
               description="Historial de reportes médicos"
               href="/records"
-              iconBg="bg-emerald-100 dark:bg-emerald-900/20"
+              iconBg="bg-emerald-100 dark:bg-emerald-400/20"
               iconColor="text-emerald-600 dark:text-emerald-400"
             />
             <QuickActionButton
@@ -97,7 +96,7 @@ export default function QuickActions({ isAdmin, isDoctor, isPatient }: QuickActi
               title="Mis Citas"
               description="Próximas consultas programadas"
               href="/appointments"
-              iconBg="bg-amber-100 dark:bg-amber-900/20"
+              iconBg="bg-amber-100 dark:bg-amber-400/20"
               iconColor="text-amber-600 dark:text-amber-400"
             />
           </>

+ 15 - 1
src/components/sidebar/SidebarNavigation.tsx

@@ -12,7 +12,9 @@ import {
   ChevronRight,
   Home,
   Calendar,
-  Sparkles
+  Sparkles,
+  BookOpen,
+  User
 } from "lucide-react"
 import { COLOR_PALETTE } from "@/utils/palette"
 import { useAppointmentsBadge } from "@/hooks/useAppointmentsBadge"
@@ -58,6 +60,8 @@ export default function SidebarNavigation({ onItemClick, isCollapsed = false }:
     } else {
       if (currentPath === "/dashboard") {
         sectionsToExpand.push("General")
+      } else if (currentPath.startsWith("/daily-log")) {
+        sectionsToExpand.push("Personal")
       } else if (currentPath.startsWith("/chat") || currentPath.startsWith("/records") || currentPath.startsWith("/appointments")) {
         sectionsToExpand.push("Servicios Médicos")
       }
@@ -150,6 +154,16 @@ export default function SidebarNavigation({ onItemClick, isCollapsed = false }:
             }
           ]
         },
+        {
+          title: "Personal",
+          items: [
+            {
+              title: "Mi Diario",
+              href: "/daily-log",
+              icon: BookOpen
+            }
+          ]
+        },
         {
           title: "Servicios Médicos",
           items: [

+ 157 - 0
src/components/ui/alert-dialog.tsx

@@ -0,0 +1,157 @@
+"use client"
+
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+function AlertDialog({
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
+  return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
+}
+
+function AlertDialogTrigger({
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
+  return (
+    <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
+  )
+}
+
+function AlertDialogPortal({
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
+  return (
+    <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
+  )
+}
+
+function AlertDialogOverlay({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
+  return (
+    <AlertDialogPrimitive.Overlay
+      data-slot="alert-dialog-overlay"
+      className={cn(
+        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function AlertDialogContent({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
+  return (
+    <AlertDialogPortal>
+      <AlertDialogOverlay />
+      <AlertDialogPrimitive.Content
+        data-slot="alert-dialog-content"
+        className={cn(
+          "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
+          className
+        )}
+        {...props}
+      />
+    </AlertDialogPortal>
+  )
+}
+
+function AlertDialogHeader({
+  className,
+  ...props
+}: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="alert-dialog-header"
+      className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
+      {...props}
+    />
+  )
+}
+
+function AlertDialogFooter({
+  className,
+  ...props
+}: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="alert-dialog-footer"
+      className={cn(
+        "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+function AlertDialogTitle({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
+  return (
+    <AlertDialogPrimitive.Title
+      data-slot="alert-dialog-title"
+      className={cn("text-lg font-semibold", className)}
+      {...props}
+    />
+  )
+}
+
+function AlertDialogDescription({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
+  return (
+    <AlertDialogPrimitive.Description
+      data-slot="alert-dialog-description"
+      className={cn("text-muted-foreground text-sm", className)}
+      {...props}
+    />
+  )
+}
+
+function AlertDialogAction({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
+  return (
+    <AlertDialogPrimitive.Action
+      className={cn(buttonVariants(), className)}
+      {...props}
+    />
+  )
+}
+
+function AlertDialogCancel({
+  className,
+  ...props
+}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
+  return (
+    <AlertDialogPrimitive.Cancel
+      className={cn(buttonVariants({ variant: "outline" }), className)}
+      {...props}
+    />
+  )
+}
+
+export {
+  AlertDialog,
+  AlertDialogPortal,
+  AlertDialogOverlay,
+  AlertDialogTrigger,
+  AlertDialogContent,
+  AlertDialogHeader,
+  AlertDialogFooter,
+  AlertDialogTitle,
+  AlertDialogDescription,
+  AlertDialogAction,
+  AlertDialogCancel,
+}

+ 251 - 0
src/hooks/useDailyLog.ts

@@ -0,0 +1,251 @@
+import { useState, useCallback } from "react"
+import { DailyLog, DailyLogInput } from "@/types/daily-log"
+import { toast } from "sonner"
+
+export function useDailyLog() {
+  const [logs, setLogs] = useState<DailyLog[]>([])
+  const [loading, setLoading] = useState(false)
+  const [error, setError] = useState<string | null>(null)
+
+  // Obtener logs de un rango de fechas
+  const fetchLogs = useCallback(async (startDate: string, endDate: string) => {
+    setLoading(true)
+    setError(null)
+
+    try {
+      const params = new URLSearchParams({ startDate, endDate })
+      const response = await fetch(`/api/daily-log?${params}`)
+
+      if (!response.ok) {
+        const data = await response.json()
+        throw new Error(data.error || "Error al cargar registros")
+      }
+
+      const data = await response.json()
+      setLogs(data)
+      return data
+    } catch (err) {
+      const message = err instanceof Error ? err.message : "Error desconocido"
+      setError(message)
+      toast.error("Error al cargar registros", {
+        description: message,
+      })
+      return []
+    } finally {
+      setLoading(false)
+    }
+  }, [toast])
+
+  // Obtener log de una fecha específica
+  const fetchLogByDate = useCallback(async (date: string) => {
+    try {
+      const response = await fetch(`/api/daily-log/${date}`)
+
+      if (response.status === 404) {
+        return null // No existe log para esta fecha
+      }
+
+      if (!response.ok) {
+        const data = await response.json()
+        throw new Error(data.error || "Error al cargar registro")
+      }
+
+      return await response.json()
+    } catch (err) {
+      const message = err instanceof Error ? err.message : "Error desconocido"
+      toast.error("Error al cargar registro", {
+        description: message,
+      })
+      return null
+    }
+  }, [])
+
+  // Crear o actualizar log
+  const saveLog = useCallback(async (input: DailyLogInput) => {
+    setLoading(true)
+    setError(null)
+
+    try {
+      const response = await fetch("/api/daily-log", {
+        method: "POST",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify(input),
+      })
+
+      if (!response.ok) {
+        const data = await response.json()
+        throw new Error(data.error || "Error al guardar registro")
+      }
+
+      const savedLog = await response.json()
+
+      // Actualizar lista local
+      setLogs((prev) => {
+        const filtered = prev.filter((log) => log.date !== savedLog.date)
+        return [...filtered, savedLog].sort(
+          (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
+        )
+      })
+
+      toast.success("¡Guardado!", {
+        description: "Tu registro ha sido guardado correctamente",
+      })
+
+      return savedLog
+    } catch (err) {
+      const message = err instanceof Error ? err.message : "Error desconocido"
+      setError(message)
+      toast.error("Error al guardar", {
+        description: message,
+      })
+      return null
+    } finally {
+      setLoading(false)
+    }
+  }, [toast])
+
+  // Actualizar log existente
+  const updateLog = useCallback(async (date: string, input: Partial<DailyLogInput>) => {
+    setLoading(true)
+    setError(null)
+
+    try {
+      const response = await fetch(`/api/daily-log/${date}`, {
+        method: "PUT",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify(input),
+      })
+
+      if (!response.ok) {
+        const data = await response.json()
+        throw new Error(data.error || "Error al actualizar registro")
+      }
+
+      const updatedLog = await response.json()
+
+      // Actualizar lista local
+      setLogs((prev) =>
+        prev.map((log) => (log.date === updatedLog.date ? updatedLog : log))
+      )
+
+      toast.success("¡Actualizado!", {
+        description: "Tu registro ha sido actualizado",
+      })
+
+      return updatedLog
+    } catch (err) {
+      const message = err instanceof Error ? err.message : "Error desconocido"
+      setError(message)
+      toast.error("Error al actualizar", {
+        description: message,
+      })
+      return null
+    } finally {
+      setLoading(false)
+    }
+  }, [toast])
+
+  // Eliminar log
+  const deleteLog = useCallback(async (date: string) => {
+    setLoading(true)
+    setError(null)
+
+    try {
+      const response = await fetch(`/api/daily-log/${date}`, {
+        method: "DELETE",
+      })
+
+      if (!response.ok) {
+        const data = await response.json()
+        throw new Error(data.error || "Error al eliminar registro")
+      }
+
+      // Eliminar de lista local
+      setLogs((prev) => prev.filter((log) => log.date !== date))
+
+      toast.success("Eliminado", {
+        description: "Tu registro ha sido eliminado",
+      })
+
+      return true
+    } catch (err) {
+      const message = err instanceof Error ? err.message : "Error desconocido"
+      setError(message)
+      toast.error("Error al eliminar", {
+        description: message,
+      })
+      return false
+    } finally {
+      setLoading(false)
+    }
+  }, [toast])
+
+  // Exportar logs a CSV
+  const exportToCSV = useCallback((logs: DailyLog[], filename = "diario-personal.csv") => {
+    try {
+      // Crear headers CSV
+      const headers = [
+        "Fecha",
+        "Ánimo",
+        "Energía",
+        "Horas de Sueño",
+        "Calidad de Sueño",
+        "Notas",
+      ]
+
+      // Convertir logs a filas CSV
+      const rows = logs.map((log) => {
+        const date = new Date(log.date + "T00:00:00").toLocaleDateString("es-ES")
+        return [
+          date,
+          log.mood?.toString() || "",
+          log.energy?.toString() || "",
+          log.sleepHours?.toString() || "",
+          log.sleepQuality?.toString() || "",
+          log.notes ? `"${log.notes.replace(/"/g, '""')}"` : "", // Escapar comillas
+        ]
+      })
+
+      // Crear contenido CSV
+      const csvContent = [
+        headers.join(","),
+        ...rows.map((row) => row.join(",")),
+      ].join("\n")
+
+      // Crear blob y descargar
+      const blob = new Blob(["\uFEFF" + csvContent], {
+        type: "text/csv;charset=utf-8;",
+      }) // \uFEFF es BOM para UTF-8
+      const link = document.createElement("a")
+      const url = URL.createObjectURL(blob)
+
+      link.setAttribute("href", url)
+      link.setAttribute("download", filename)
+      link.style.visibility = "hidden"
+      document.body.appendChild(link)
+      link.click()
+      document.body.removeChild(link)
+
+      toast.success("Archivo exportado", {
+        description: `${logs.length} registros exportados a ${filename}`,
+      })
+    } catch (err) {
+      const message = err instanceof Error ? err.message : "Error desconocido"
+      toast.error("Error al exportar", {
+        description: message,
+      })
+    }
+  }, [])
+
+  return {
+    logs,
+    loading,
+    error,
+    fetchLogs,
+    fetchLogByDate,
+    saveLog,
+    updateLog,
+    deleteLog,
+    exportToCSV,
+  }
+}

+ 1 - 1
src/lib/auth.ts

@@ -124,7 +124,7 @@ export const authOptions: NextAuthOptions = {
         session.user = {
           ...session.user,
           id: token.id as string,
-          role: token.role as any,
+          role: token.role as "PATIENT" | "ADMIN",
           name: token.name as string,
           lastname: token.lastname as string,
           email: token.email as string,

+ 38 - 0
src/types/daily-log.ts

@@ -0,0 +1,38 @@
+export interface DailyLog {
+  id: string
+  userId: string
+  date: string // ISO date string (YYYY-MM-DD)
+  mood?: number // 1-5
+  energy?: number // 1-5
+  sleepHours?: number
+  sleepQuality?: number // 1-5
+  notes?: string
+  createdAt: string
+  updatedAt: string
+}
+
+export interface DailyLogInput {
+  date: string // ISO date string (YYYY-MM-DD)
+  mood?: number
+  energy?: number
+  sleepHours?: number
+  sleepQuality?: number
+  notes?: string
+}
+
+export interface DailyLogStats {
+  totalEntries: number
+  avgMood?: number
+  avgEnergy?: number
+  avgSleep?: number
+  avgSleepQuality?: number
+  streak: number // días consecutivos con registro
+}
+
+export interface DailyLogFilters {
+  startDate?: string
+  endDate?: string
+  minMood?: number
+  maxMood?: number
+  moods?: number[] // Array de moods específicos (1-5)
+}