| --- |
| license: bigscience-openrail-m |
| datasets: |
| - google/mobile-actions |
| language: |
| - es |
| metrics: |
| - accuracy |
| base_model: |
| - zai-org/GLM-4.7 |
| pipeline_tag: text-classification |
| --- |
| <!DOCTYPE html> |
| <html lang="es" data-color-scheme="light"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| <title>Planificador Mensual de Hábitos</title> |
| <style> |
| /* ===== PERPLEXITY DESIGN SYSTEM (recortado a lo necesario) ===== */ |
| :root { |
| --color-white: rgba(255, 255, 255, 1); |
| --color-cream-50: rgba(252, 252, 249, 1); |
| --color-cream-100: rgba(255, 255, 253, 1); |
| --color-gray-200: rgba(245, 245, 245, 1); |
| --color-gray-300: rgba(167, 169, 169, 1); |
| --color-gray-400: rgba(119, 124, 124, 1); |
| --color-slate-500: rgba(98, 108, 113, 1); |
| --color-brown-600: rgba(94, 82, 64, 1); |
| --color-charcoal-700: rgba(31, 33, 33, 1); |
| --color-charcoal-800: rgba(38, 40, 40, 1); |
| --color-slate-900: rgba(19, 52, 59, 1); |
| --color-teal-300: rgba(50, 184, 198, 1); |
| --color-teal-400: rgba(45, 166, 178, 1); |
| --color-teal-500: rgba(33, 128, 141, 1); |
| --color-teal-600: rgba(29, 116, 128, 1); |
| --color-teal-700: rgba(26, 104, 115, 1); |
| --color-red-400: rgba(255, 84, 89, 1); |
| --color-orange-400: rgba(230, 129, 97, 1); |
| |
| --color-brown-600-rgb: 94, 82, 64; |
| --color-teal-500-rgb: 33, 128, 141; |
| --color-slate-900-rgb: 19, 52, 59; |
| --color-slate-500-rgb: 98, 108, 113; |
| |
| --color-background: var(--color-cream-50); |
| --color-surface: var(--color-cream-100); |
| --color-text: var(--color-slate-900); |
| --color-text-secondary: var(--color-slate-500); |
| --color-primary: var(--color-teal-500); |
| --color-primary-hover: var(--color-teal-600); |
| --color-primary-active: var(--color-teal-700); |
| --color-secondary: rgba(var(--color-brown-600-rgb), 0.12); |
| --color-secondary-hover: rgba(var(--color-brown-600-rgb), 0.2); |
| --color-secondary-active: rgba(var(--color-brown-600-rgb), 0.25); |
| --color-border: rgba(var(--color-brown-600-rgb), 0.2); |
| --color-btn-primary-text: var(--color-cream-50); |
| --color-card-border: rgba(var(--color-brown-600-rgb), 0.12); |
| --color-card-border-inner: rgba(var(--color-brown-600-rgb), 0.12); |
| --color-focus-ring: rgba(var(--color-teal-500-rgb), 0.4); |
| |
| --font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; |
| --font-family-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; |
| --font-size-sm: 12px; |
| --font-size-base: 14px; |
| --font-size-lg: 16px; |
| --font-size-2xl: 20px; |
| --font-weight-medium: 500; |
| --font-weight-semibold: 550; |
| |
| --space-4: 4px; |
| --space-8: 8px; |
| --space-12: 12px; |
| --space-16: 16px; |
| --space-20: 20px; |
| --space-24: 24px; |
| --space-32: 32px; |
| |
| --radius-sm: 6px; |
| --radius-base: 8px; |
| --radius-lg: 12px; |
| |
| --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.02); |
| --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.04), |
| 0 2px 4px -1px rgba(0, 0, 0, 0.02); |
| |
| --duration-fast: 150ms; |
| --duration-normal: 250ms; |
| --ease-standard: cubic-bezier(0.16, 1, 0.3, 1); |
| } |
| |
| html { |
| font-size: var(--font-size-base); |
| font-family: var(--font-family-base); |
| color: var(--color-text); |
| background-color: var(--color-background); |
| -webkit-font-smoothing: antialiased; |
| box-sizing: border-box; |
| } |
| *, *::before, *::after { box-sizing: inherit; } |
| body { margin: 0; } |
| |
| h1, h2, h3 { |
| margin: 0; |
| font-weight: var(--font-weight-semibold); |
| } |
| h1 { font-size: 24px; } |
| h2 { font-size: 20px; } |
| h3 { font-size: 16px; } |
| |
| .container { |
| max-width: 1200px; |
| margin: 0 auto; |
| padding: var(--space-16); |
| } |
| |
| .card { |
| background-color: var(--color-surface); |
| border-radius: var(--radius-lg); |
| border: 1px solid var(--color-card-border); |
| box-shadow: var(--shadow-sm); |
| padding: var(--space-16); |
| margin-bottom: var(--space-16); |
| } |
| |
| .btn { |
| display: inline-flex; |
| align-items: center; |
| justify-content: center; |
| padding: var(--space-8) var(--space-16); |
| border-radius: var(--radius-base); |
| font-size: var(--font-size-base); |
| font-weight: var(--font-weight-medium); |
| cursor: pointer; |
| border: none; |
| transition: all var(--duration-normal) var(--ease-standard); |
| } |
| .btn-primary { |
| background: var(--color-primary); |
| color: var(--color-btn-primary-text); |
| } |
| .btn-primary:hover { background: var(--color-primary-hover); } |
| .btn-secondary { |
| background: var(--color-secondary); |
| color: var(--color-text); |
| } |
| .btn-secondary:hover { background: var(--color-secondary-hover); } |
| |
| .form-group { margin-bottom: var(--space-12); } |
| .form-label { |
| display: block; |
| margin-bottom: var(--space-4); |
| font-size: var(--font-size-sm); |
| color: var(--color-text-secondary); |
| } |
| .form-control { |
| width: 100%; |
| padding: var(--space-8) var(--space-12); |
| border-radius: var(--radius-base); |
| border: 1px solid var(--color-border); |
| background-color: var(--color-surface); |
| font-size: var(--font-size-base); |
| } |
| .form-control:focus { |
| outline: 2px solid var(--color-primary); |
| outline-offset: 1px; |
| } |
| textarea.form-control { |
| min-height: 120px; |
| resize: vertical; |
| } |
| |
| .layout-main { |
| display: grid; |
| grid-template-columns: 260px minmax(260px, 1fr) 300px; |
| gap: var(--space-16); |
| } |
| @media (max-width: 1024px) { |
| .layout-main { |
| grid-template-columns: 1fr; |
| } |
| } |
| |
| .habit-list-item { |
| display: grid; |
| grid-template-columns: auto 1fr auto; |
| gap: var(--space-8); |
| align-items: center; |
| padding: var(--space-8); |
| border-radius: var(--radius-base); |
| border: 1px solid rgba(0,0,0,0.03); |
| margin-bottom: var(--space-8); |
| background-color: rgba(0,0,0,0.01); |
| } |
| .habit-number { |
| font-weight: var(--font-weight-semibold); |
| font-size: var(--font-size-sm); |
| color: var(--color-text-secondary); |
| width: 24px; |
| text-align: right; |
| } |
| .habit-name-input { |
| border-radius: var(--radius-sm); |
| border: 1px solid var(--color-border); |
| padding: var(--space-4) var(--space-8); |
| font-size: var(--font-size-sm); |
| width: 100%; |
| } |
| .habit-meta { |
| display: flex; |
| flex-direction: column; |
| gap: var(--space-4); |
| align-items: flex-end; |
| } |
| .habit-toggle-row { |
| display: flex; |
| align-items: center; |
| gap: var(--space-4); |
| font-size: var(--font-size-sm); |
| } |
| .habit-toggle-row input[type="checkbox"] { |
| width: 16px; |
| height: 16px; |
| } |
| .habit-color-input { |
| width: 24px; |
| height: 24px; |
| padding: 0; |
| border-radius: var(--radius-sm); |
| border: 1px solid var(--color-border); |
| background: transparent; |
| cursor: pointer; |
| } |
| |
| .month-header { |
| display: flex; |
| flex-wrap: wrap; |
| align-items: center; |
| gap: var(--space-12); |
| margin-bottom: var(--space-16); |
| } |
| .month-header > * { |
| flex: 0 0 auto; |
| } |
| .month-header-title { |
| font-size: var(--font-size-2xl); |
| font-weight: var(--font-weight-semibold); |
| } |
| |
| .month-select-row { |
| display: flex; |
| gap: var(--space-8); |
| flex-wrap: wrap; |
| margin-bottom: var(--space-12); |
| } |
| |
| .month-list { |
| display: flex; |
| flex-wrap: wrap; |
| gap: var(--space-8); |
| } |
| .month-pill { |
| padding: var(--space-4) var(--space-12); |
| border-radius: 999px; |
| border: 1px solid var(--color-border); |
| font-size: var(--font-size-sm); |
| cursor: pointer; |
| background-color: var(--color-surface); |
| } |
| .month-pill.active { |
| background-color: var(--color-primary); |
| color: var(--color-btn-primary-text); |
| border-color: var(--color-primary); |
| } |
| |
| /* ===== CÍRCULO MENSUAL ===== */ |
| .circle-container-card { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| } |
| .circle-wrapper { |
| position: relative; |
| width: min(420px, 90vw); |
| height: min(420px, 90vw); |
| } |
| .circle-svg { |
| width: 100%; |
| height: 100%; |
| display: block; |
| } |
| .circle-legend { |
| margin-top: var(--space-12); |
| display: flex; |
| flex-wrap: wrap; |
| gap: var(--space-8); |
| justify-content: center; |
| font-size: var(--font-size-sm); |
| } |
| .circle-legend-item { |
| display: inline-flex; |
| align-items: center; |
| gap: var(--space-4); |
| padding: 2px 8px; |
| border-radius: 999px; |
| background-color: rgba(0,0,0,0.02); |
| } |
| .circle-legend-color { |
| width: 10px; |
| height: 10px; |
| border-radius: 999px; |
| border: 1px solid rgba(0,0,0,0.1); |
| } |
| |
| .circle-center-label { |
| position: absolute; |
| top: 50%; |
| left: 50%; |
| transform: translate(-50%, -50%); |
| text-align: center; |
| font-size: var(--font-size-sm); |
| pointer-events: none; |
| } |
| |
| /* Tooltip simple para día/hábito */ |
| .tooltip { |
| position: fixed; |
| background: var(--color-charcoal-800); |
| color: white; |
| padding: 4px 8px; |
| border-radius: 4px; |
| font-size: 11px; |
| pointer-events: none; |
| z-index: 1000; |
| display: none; |
| white-space: nowrap; |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <h1>Planificador mensual de hábitos</h1> |
| |
| <!-- Selección de mes --> |
| <div class="card"> |
| <div class="month-select-row"> |
| <div class="form-group" style="min-width: 160px;"> |
| <label class="form-label" for="select-mes-num">Mes</label> |
| <select id="select-mes-num" class="form-control"> |
| <option value="1">01 - Enero</option> |
| <option value="2">02 - Febrero</option> |
| <option value="3">03 - Marzo</option> |
| <option value="4">04 - Abril</option> |
| <option value="5">05 - Mayo</option> |
| <option value="6">06 - Junio</option> |
| <option value="7">07 - Julio</option> |
| <option value="8">08 - Agosto</option> |
| <option value="9">09 - Septiembre</option> |
| <option value="10">10 - Octubre</option> |
| <option value="11">11 - Noviembre</option> |
| <option value="12">12 - Diciembre</option> |
| </select> |
| </div> |
| <div class="form-group" style="min-width: 120px;"> |
| <label class="form-label" for="input-anio">Año</label> |
| <input type="number" id="input-anio" class="form-control" value="2026" /> |
| </div> |
| <div class="form-group" style="align-self: flex-end;"> |
| <button id="btn-nuevo-mes" class="btn btn-primary">+ Nuevo mes</button> |
| </div> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">Meses creados</label> |
| <div id="lista-meses" class="month-list"></div> |
| </div> |
| </div> |
| |
| <!-- Pantalla principal Mes --> |
| <div class="card"> |
| <div id="mes-activo-header" class="month-header" style="margin-bottom: 0;"> |
| <div class="month-header-title" id="titulo-mes-visual">Sin mes seleccionado</div> |
| </div> |
| <div class="layout-main" style="margin-top: var(--space-16);"> |
| <!-- Zona izquierda: hábitos --> |
| <div> |
| <h3 style="margin-bottom: var(--space-8);">Hábitos (1–8)</h3> |
| <div id="lista-habitos"></div> |
| </div> |
| |
| <!-- Zona central: círculo mensual --> |
| <div class="circle-container-card"> |
| <div class="circle-wrapper"> |
| <svg id="habit-circle" class="circle-svg" viewBox="0 0 400 400"></svg> |
| <div class="circle-center-label" id="circle-center-label"></div> |
| </div> |
| <div class="circle-legend" id="circle-legend"></div> |
| </div> |
| |
| <!-- Zona derecha: observaciones --> |
| <div> |
| <h3 style="margin-bottom: var(--space-8);">Observaciones</h3> |
| <textarea id="textarea-observaciones" class="form-control" placeholder="Notas, reflexiones, objetivos del mes..."></textarea> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <div id="tooltip" class="tooltip"></div> |
|
|
| <script> |
| // ===================================== |
| // MODELO EN MEMORIA (Mes, HabitoMes, RegistroDia) |
| // ===================================== |
| |
| function uuid() { |
| return crypto.randomUUID |
| ? crypto.randomUUID() |
| : "xxxx-4xxx-yxxx-xxxx".replace(/[xy]/g, function (c) { |
| var r = (Math.random() * 16) | 0, |
| v = c === "x" ? r : (r & 0x3) | 0x8; |
| return v.toString(16); |
| }); |
| } |
| |
| /** |
| * Mes: { |
| * id_mes, |
| * mes (1-12), |
| * anio, |
| * titulo_mes, |
| * observaciones |
| * } |
| * |
| * HabitoMes: { |
| * id_habito_mes, |
| * id_mes, |
| * numero_habito (1-8), |
| * nombre_habito, |
| * activo (bool), |
| * color (string) |
| * } |
| * |
| * RegistroDia: { |
| * id_registro, |
| * id_mes, |
| * numero_habito, |
| * dia (1-31), |
| * cumplido (bool) |
| * } |
| */ |
| |
| const DB = { |
| meses: [], |
| habitosMes: [], |
| registrosDia: [] |
| }; |
| |
| let mesActivoId = null; |
| |
| const COLOR_POR_DEFECTO = [ |
| "#3B82F6", // azul |
| "#10B981", // verde |
| "#F59E0B", // amarillo |
| "#EF4444", // rojo |
| "#8B5CF6", // violeta |
| "#EC4899", // rosa |
| "#14B8A6", // teal |
| "#6366F1" // índigo |
| ]; |
| |
| function crearMes(mesNumero, anio) { |
| const id_mes = uuid(); |
| const nombreMeses = [ |
| "Enero","Febrero","Marzo","Abril","Mayo","Junio", |
| "Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre" |
| ]; |
| const titulo_mes = `${nombreMeses[mesNumero - 1]} ${anio}`; |
| |
| const nuevoMes = { |
| id_mes, |
| mes: mesNumero, |
| anio, |
| titulo_mes, |
| observaciones: "" |
| }; |
| DB.meses.push(nuevoMes); |
| |
| // Crear 8 filas en HabitoMes |
| for (let i = 1; i <= 8; i++) { |
| DB.habitosMes.push({ |
| id_habito_mes: uuid(), |
| id_mes, |
| numero_habito: i, |
| nombre_habito: `Hábito ${i}`, |
| activo: false, |
| color: COLOR_POR_DEFECTO[i - 1] || "#999999" |
| }); |
| } |
| |
| return nuevoMes; |
| } |
| |
| function obtenerMesesOrdenados() { |
| return [...DB.meses].sort((a, b) => { |
| if (a.anio !== b.anio) return a.anio - b.anio; |
| return a.mes - b.mes; |
| }); |
| } |
| |
| function getMesActivo() { |
| if (!mesActivoId) return null; |
| return DB.meses.find(m => m.id_mes === mesActivoId) || null; |
| } |
| |
| function getHabitosDeMes(id_mes) { |
| return DB.habitosMes |
| .filter(h => h.id_mes === id_mes) |
| .sort((a, b) => a.numero_habito - b.numero_habito); |
| } |
| |
| function getRegistroDia(id_mes, numero_habito, dia) { |
| return DB.registrosDia.find( |
| r => r.id_mes === id_mes && r.numero_habito === numero_habito && r.dia === dia |
| ) || null; |
| } |
| |
| function toggleRegistroDia(id_mes, numero_habito, dia) { |
| const existente = getRegistroDia(id_mes, numero_habito, dia); |
| if (!existente) { |
| const nuevo = { |
| id_registro: uuid(), |
| id_mes, |
| numero_habito, |
| dia, |
| cumplido: true |
| }; |
| DB.registrosDia.push(nuevo); |
| } else { |
| existente.cumplido = !existente.cumplido; |
| } |
| } |
| |
| function getDiasDelMes(mes, anio) { |
| return new Date(anio, mes, 0).getDate(); |
| } |
| |
| // ===================================== |
| // RENDER UI |
| // ===================================== |
| |
| const listaMesesEl = document.getElementById("lista-meses"); |
| const selectMesNumEl = document.getElementById("select-mes-num"); |
| const inputAnioEl = document.getElementById("input-anio"); |
| const btnNuevoMesEl = document.getElementById("btn-nuevo-mes"); |
| |
| const tituloMesVisualEl = document.getElementById("titulo-mes-visual"); |
| const listaHabitosEl = document.getElementById("lista-habitos"); |
| const textareaObsEl = document.getElementById("textarea-observaciones"); |
| |
| const svgCircleEl = document.getElementById("habit-circle"); |
| const circleCenterLabelEl = document.getElementById("circle-center-label"); |
| const circleLegendEl = document.getElementById("circle-legend"); |
| const tooltipEl = document.getElementById("tooltip"); |
| |
| function renderMeses() { |
| listaMesesEl.innerHTML = ""; |
| const meses = obtenerMesesOrdenados(); |
| if (meses.length === 0) { |
| const span = document.createElement("span"); |
| span.style.fontSize = "12px"; |
| span.style.color = "var(--color-text-secondary)"; |
| span.textContent = "Aún no hay meses creados. Usa \"+ Nuevo mes\" para empezar."; |
| listaMesesEl.appendChild(span); |
| return; |
| } |
| |
| meses.forEach(m => { |
| const btn = document.createElement("button"); |
| btn.className = "month-pill" + (m.id_mes === mesActivoId ? " active" : ""); |
| btn.textContent = m.titulo_mes; |
| btn.addEventListener("click", () => { |
| mesActivoId = m.id_mes; |
| renderMeses(); |
| renderPantallaMes(); |
| }); |
| listaMesesEl.appendChild(btn); |
| }); |
| } |
| |
| function renderPantallaMes() { |
| const mes = getMesActivo(); |
| if (!mes) { |
| tituloMesVisualEl.textContent = "Sin mes seleccionado"; |
| listaHabitosEl.innerHTML = ""; |
| textareaObsEl.value = ""; |
| svgCircleEl.innerHTML = ""; |
| circleCenterLabelEl.textContent = ""; |
| circleLegendEl.innerHTML = ""; |
| return; |
| } |
| |
| tituloMesVisualEl.textContent = mes.titulo_mes; |
| textareaObsEl.value = mes.observaciones || ""; |
| renderHabitos(mes); |
| renderCircle(mes); |
| } |
| |
| function renderHabitos(mes) { |
| const habitos = getHabitosDeMes(mes.id_mes); |
| listaHabitosEl.innerHTML = ""; |
| |
| habitos.forEach(h => { |
| const row = document.createElement("div"); |
| row.className = "habit-list-item"; |
| |
| const numEl = document.createElement("div"); |
| numEl.className = "habit-number"; |
| numEl.textContent = h.numero_habito; |
| |
| const inputNombre = document.createElement("input"); |
| inputNombre.className = "habit-name-input"; |
| inputNombre.value = h.nombre_habito || ""; |
| inputNombre.placeholder = `Hábito ${h.numero_habito}`; |
| inputNombre.addEventListener("input", () => { |
| h.nombre_habito = inputNombre.value; |
| renderCircle(mes); |
| }); |
| |
| const meta = document.createElement("div"); |
| meta.className = "habit-meta"; |
| |
| const toggleRow = document.createElement("div"); |
| toggleRow.className = "habit-toggle-row"; |
| const chk = document.createElement("input"); |
| chk.type = "checkbox"; |
| chk.checked = !!h.activo; |
| chk.addEventListener("change", () => { |
| h.activo = chk.checked; |
| renderCircle(mes); |
| }); |
| const lbl = document.createElement("span"); |
| lbl.textContent = "Activo"; |
| toggleRow.appendChild(chk); |
| toggleRow.appendChild(lbl); |
| |
| const colorInput = document.createElement("input"); |
| colorInput.type = "color"; |
| colorInput.className = "habit-color-input"; |
| colorInput.value = h.color || "#999999"; |
| colorInput.addEventListener("input", () => { |
| h.color = colorInput.value; |
| renderCircle(mes); |
| }); |
| |
| meta.appendChild(toggleRow); |
| meta.appendChild(colorInput); |
| |
| row.appendChild(numEl); |
| row.appendChild(inputNombre); |
| row.appendChild(meta); |
| |
| listaHabitosEl.appendChild(row); |
| }); |
| } |
| |
| function renderCircle(mes) { |
| const habitos = getHabitosDeMes(mes.id_mes).filter(h => h.activo); |
| const diasMes = getDiasDelMes(mes.mes, mes.anio); |
| |
| svgCircleEl.innerHTML = ""; |
| circleLegendEl.innerHTML = ""; |
| if (habitos.length === 0) { |
| circleCenterLabelEl.textContent = "Activa algún hábito\npara ver el círculo"; |
| return; |
| } else { |
| circleCenterLabelEl.textContent = `${diasMes} días`; |
| } |
| |
| const centerX = 200; |
| const centerY = 200; |
| const innerRadius = 60; |
| const bandWidth = 20; |
| const gapBetweenBands = 4; |
| const totalDays = 31; // estructura de 31 segmentos, pero se "grisearán" los días > diasMes |
| const angleStep = (2 * Math.PI) / totalDays; |
| |
| habitos.forEach((habito, index) => { |
| const r0 = innerRadius + index * (bandWidth + gapBetweenBands); |
| const r1 = r0 + bandWidth; |
| |
| // Leyenda |
| const legItem = document.createElement("div"); |
| legItem.className = "circle-legend-item"; |
| const col = document.createElement("div"); |
| col.className = "circle-legend-color"; |
| col.style.backgroundColor = habito.color || "#999999"; |
| const txt = document.createElement("span"); |
| txt.textContent = habito.nombre_habito || `Hábito ${habito.numero_habito}`; |
| legItem.appendChild(col); |
| legItem.appendChild(txt); |
| circleLegendEl.appendChild(legItem); |
| |
| for (let dia = 1; dia <= totalDays; dia++) { |
| const startAngle = (dia - 1) * angleStep - Math.PI / 2; |
| const endAngle = dia * angleStep - Math.PI / 2; |
| |
| const x0 = centerX + r0 * Math.cos(startAngle); |
| const y0 = centerY + r0 * Math.sin(startAngle); |
| const x1 = centerX + r1 * Math.cos(startAngle); |
| const y1 = centerY + r1 * Math.sin(startAngle); |
| const x2 = centerX + r1 * Math.cos(endAngle); |
| const y2 = centerY + r1 * Math.sin(endAngle); |
| const x3 = centerX + r0 * Math.cos(endAngle); |
| const y3 = centerY + r0 * Math.sin(endAngle); |
| |
| const largeArcFlag = 0; |
| |
| const pathData = [ |
| "M", x0, y0, |
| "L", x1, y1, |
| "A", r1, r1, 0, largeArcFlag, 1, x2, y2, |
| "L", x3, y3, |
| "A", r0, r0, 0, largeArcFlag, 0, x0, y0, |
| "Z" |
| ].join(" "); |
| |
| const registro = getRegistroDia(mes.id_mes, habito.numero_habito, dia); |
| const isCumplido = registro && registro.cumplido; |
| |
| const pathEl = document.createElementNS("http://www.w3.org/2000/svg", "path"); |
| pathEl.setAttribute("d", pathData); |
| |
| if (dia > diasMes) { |
| pathEl.setAttribute("fill", "#f1f5f9"); |
| pathEl.setAttribute("stroke", "#e5e7eb"); |
| pathEl.setAttribute("stroke-width", "0.5"); |
| pathEl.setAttribute("fill-opacity", "0.6"); |
| } else if (isCumplido) { |
| pathEl.setAttribute("fill", habito.color || "#22c55e"); |
| pathEl.setAttribute("fill-opacity", "0.9"); |
| } else { |
| pathEl.setAttribute("fill", "#ffffff"); |
| pathEl.setAttribute("fill-opacity", "0.9"); |
| pathEl.setAttribute("stroke", "#e2e8f0"); |
| pathEl.setAttribute("stroke-width", "0.5"); |
| } |
| |
| pathEl.style.cursor = dia <= diasMes ? "pointer" : "default"; |
| |
| pathEl.addEventListener("click", () => { |
| if (dia > diasMes) return; |
| toggleRegistroDia(mes.id_mes, habito.numero_habito, dia); |
| renderCircle(mes); |
| }); |
| |
| const habitName = habito.nombre_habito || `Hábito ${habito.numero_habito}`; |
| pathEl.addEventListener("mousemove", (ev) => { |
| tooltipEl.style.display = "block"; |
| tooltipEl.textContent = `${habitName} · Día ${dia}`; |
| tooltipEl.style.left = ev.clientX + 12 + "px"; |
| tooltipEl.style.top = ev.clientY + 12 + "px"; |
| }); |
| pathEl.addEventListener("mouseleave", () => { |
| tooltipEl.style.display = "none"; |
| }); |
| |
| svgCircleEl.appendChild(pathEl); |
| } |
| }); |
| } |
| |
| // Observaciones |
| textareaObsEl.addEventListener("input", () => { |
| const mes = getMesActivo(); |
| if (!mes) return; |
| mes.observaciones = textareaObsEl.value; |
| }); |
| |
| // Botón "Nuevo mes" |
| btnNuevoMesEl.addEventListener("click", () => { |
| const mesNum = parseInt(selectMesNumEl.value, 10); |
| const anio = parseInt(inputAnioEl.value, 10) || new Date().getFullYear(); |
| const nuevoMes = crearMes(mesNum, anio); |
| mesActivoId = nuevoMes.id_mes; |
| renderMeses(); |
| renderPantallaMes(); |
| }); |
| |
| // Estado inicial: crear un mes por defecto (mes actual) |
| (function init() { |
| const hoy = new Date(); |
| const mesNum = hoy.getMonth() + 1; |
| const anio = hoy.getFullYear(); |
| const mesInicial = crearMes(mesNum, anio); |
| mesActivoId = mesInicial.id_mes; |
| renderMeses(); |
| renderPantallaMes(); |
| })(); |
| </script> |
| </body> |
| </html> |