diff --git a/.gitignore b/.gitignore
index 915f2a2..8b83b04 100644
--- a/.gitignore
+++ b/.gitignore
@@ -47,3 +47,5 @@ build/
.venv
+
+.fake
\ No newline at end of file
diff --git a/src/main/resources/static/css/planning.css b/src/main/resources/static/css/planning.css
index e4b6568..2a96542 100644
--- a/src/main/resources/static/css/planning.css
+++ b/src/main/resources/static/css/planning.css
@@ -201,3 +201,239 @@ h1 { font-size: 18pt; text-align: center; margin-bottom: 0.2em; color: var(--acc
.hoy-label, .btn-reset-planning { display: none !important; }
.tema-btn.completado { text-decoration: none; opacity: 1; color: inherit !important; }
}
+
+
+/* ── Planning interactivo 20260615 ───────────────────── */
+
+.planning-container {
+ max-width: 1200px;
+ margin: 20px auto;
+ padding: 0 20px;
+ }
+
+ .planning-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 30px;
+ flex-wrap: wrap;
+ gap: 15px;
+ }
+
+ .month-nav {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ }
+
+ .month-title {
+ font-size: 24px;
+ font-weight: bold;
+ min-width: 200px;
+ text-align: center;
+ }
+
+ .nav-btn {
+ background: #007bff;
+ color: white;
+ border: none;
+ padding: 8px 15px;
+ border-radius: 5px;
+ cursor: pointer;
+ font-size: 16px;
+ transition: background 0.3s;
+ }
+
+ .nav-btn:hover {
+ background: #0056b3;
+ }
+
+ .add-task-form {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 20px;
+ flex-wrap: wrap;
+ }
+
+ .add-task-form input {
+ flex: 1;
+ min-width: 200px;
+ padding: 10px;
+ border: 1px solid #ddd;
+ border-radius: 5px;
+ font-size: 14px;
+ }
+
+ .add-task-form button {
+ background: #28a745;
+ color: white;
+ border: none;
+ padding: 10px 20px;
+ border-radius: 5px;
+ cursor: pointer;
+ font-size: 14px;
+ transition: background 0.3s;
+ }
+
+ .add-task-form button:hover {
+ background: #218838;
+ }
+
+ .reset-btn {
+ background: #dc3545;
+ color: white;
+ border: none;
+ padding: 8px 15px;
+ border-radius: 5px;
+ cursor: pointer;
+ font-size: 14px;
+ transition: background 0.3s;
+ }
+
+ .reset-btn:hover {
+ background: #c82333;
+ }
+
+ .calendar-grid {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ gap: 10px;
+ margin-top: 20px;
+ }
+
+ .day-header {
+ text-align: center;
+ font-weight: bold;
+ padding: 10px;
+ background: #f0f0f0;
+ border-radius: 5px;
+ }
+
+ .day-cell {
+ border: 1px solid #ddd;
+ border-radius: 5px;
+ padding: 10px;
+ min-height: 150px;
+ background: white;
+ cursor: move;
+ transition: all 0.3s;
+ position: relative;
+ }
+
+ .day-cell.empty {
+ background: #f9f9f9;
+ cursor: default;
+ }
+
+ .day-cell.other-month {
+ background: #f0f0f0;
+ opacity: 0.5;
+ cursor: default;
+ }
+
+ .day-cell.today {
+ background: #e7f3ff;
+ border: 2px solid #007bff;
+ }
+
+ .day-cell.weekend {
+ background: #fff8f0;
+ }
+
+ .day-cell:hover:not(.empty):not(.other-month) {
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
+ transform: translateY(-2px);
+ }
+
+ .day-number {
+ font-weight: bold;
+ margin-bottom: 8px;
+ color: #333;
+ }
+
+ .day-number.weekend {
+ color: #d9534f;
+ }
+
+ .tasks-list {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ }
+
+ .task-item {
+ background: #007bff;
+ color: white;
+ padding: 6px 8px;
+ border-radius: 3px;
+ font-size: 12px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ word-break: break-word;
+ cursor: grab;
+ transition: all 0.2s;
+ }
+
+ .task-item:active {
+ cursor: grabbing;
+ opacity: 0.8;
+ }
+
+ .task-item.dragging {
+ opacity: 0.5;
+ }
+
+ .task-item:hover {
+ background: #0056b3;
+ }
+
+ .task-delete-btn {
+ background: none;
+ border: none;
+ color: white;
+ cursor: pointer;
+ font-size: 14px;
+ padding: 0 4px;
+ margin-left: 4px;
+ transition: all 0.2s;
+ }
+
+ .task-delete-btn:hover {
+ color: #ffcccc;
+ transform: scale(1.2);
+ }
+
+ .drop-zone {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ border-radius: 5px;
+ display: none;
+ border: 2px dashed #007bff;
+ background: rgba(0, 123, 255, 0.1);
+ }
+
+ .day-cell.drag-over .drop-zone {
+ display: block;
+ }
+
+ @media (max-width: 768px) {
+ .calendar-grid {
+ grid-template-columns: repeat(7, 1fr);
+ gap: 5px;
+ }
+
+ .day-cell {
+ min-height: 120px;
+ padding: 8px;
+ font-size: 12px;
+ }
+
+ .task-item {
+ font-size: 11px;
+ padding: 4px 6px;
+ }
+ }
\ No newline at end of file
diff --git a/src/main/resources/templates/planning.html b/src/main/resources/templates/planning.html
index 47593fd..86a481f 100644
--- a/src/main/resources/templates/planning.html
+++ b/src/main/resources/templates/planning.html
@@ -3,9 +3,8 @@
+
Mi Planning 2026
+
+
+
-
-
-
- LunMarMiéJueVieSábDom
-
-
-
-
-
-
-
- 8
- Preparar el planning
Preparar flashcards
-
-
-
-
-
+
+
-
-
-
-
LunMarMiéJueVieSábDom
+
+
-
-
-
-
-
- LunMarMiéJueVieSábDom
-
-
-
- 18
- Repaso
B1. Organización del Estado y Administración electrónica
B2. Tecnología básica
-
-
- 19
- Repaso
B3. Desarrollo de sistemas
B4. Sistemas y comunicaciones
-
-
- 20
- 🧪 Simulacro
Examen completo
con tiempo real
-
-
- 21
- Repaso de fallos
del simulacro
-
-
- 22
- Solo flashcards
Nada nuevo
Descansar pronto
-
-
- 23
- 🎯 EXAMEN
-
-
-
-
+
+ init() {
+ this.setupEventListeners();
+ this.render();
+ }
+
+ setupEventListeners() {
+ document.getElementById('prevMonth').addEventListener('click', () => this.previousMonth());
+ document.getElementById('nextMonth').addEventListener('click', () => this.nextMonth());
+ document.getElementById('addTaskForm').addEventListener('submit', (e) => this.addTask(e));
+ document.getElementById('resetBtn').addEventListener('click', () => this.resetAll());
+ }
+
+ previousMonth() {
+ this.currentDate.setMonth(this.currentDate.getMonth() - 1);
+ this.render();
+ }
+
+ nextMonth() {
+ this.currentDate.setMonth(this.currentDate.getMonth() + 1);
+ this.render();
+ }
+
+ render() {
+ this.renderMonthTitle();
+ this.renderCalendar();
+ }
+
+ renderMonthTitle() {
+ const options = { month: 'long', year: 'numeric' };
+ const title = this.currentDate.toLocaleDateString('es-ES', options);
+ document.getElementById('monthTitle').textContent = title.charAt(0).toUpperCase() + title.slice(1);
+ }
+
+ renderCalendar() {
+ const grid = document.getElementById('calendarGrid');
+ grid.innerHTML = '';
+
+ // Encabezados de días
+ const dayNames = ['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'];
+ dayNames.forEach(day => {
+ const header = document.createElement('div');
+ header.className = 'day-header';
+ header.textContent = day;
+ grid.appendChild(header);
+ });
+
+ // Obtener primer y último día del mes
+ const year = this.currentDate.getFullYear();
+ const month = this.currentDate.getMonth();
+ const firstDay = new Date(year, month, 1);
+ const lastDay = new Date(year, month + 1, 0);
+
+ // Ajustar para que lunes sea 0
+ let dayOfWeek = firstDay.getDay() - 1;
+ if (dayOfWeek === -1) dayOfWeek = 6;
+
+ // Días del mes anterior
+ const prevMonthLastDay = new Date(year, month, 0).getDate();
+ for (let i = dayOfWeek - 1; i >= 0; i--) {
+ const cell = this.createDayCell(prevMonthLastDay - i, 'other-month');
+ grid.appendChild(cell);
+ }
+
+ // Días del mes actual
+ for (let day = 1; day <= lastDay.getDate(); day++) {
+ const date = new Date(year, month, day);
+ const cell = this.createDayCell(day, '', date);
+ grid.appendChild(cell);
+ }
+
+ // Días del próximo mes
+ const remainingCells = grid.children.length - 7; // 7 headers
+ const totalCells = Math.ceil(remainingCells / 7) * 7;
+ for (let day = 1; day <= totalCells - remainingCells; day++) {
+ const cell = this.createDayCell(day, 'other-month');
+ grid.appendChild(cell);
+ }
+ }
+
+ createDayCell(day, extraClass, date = null) {
+ const cell = document.createElement('div');
+ cell.className = `day-cell ${extraClass}`;
+
+ if (extraClass === 'other-month') {
+ cell.classList.add('empty');
+ } else {
+ // Es día del mes actual
+ const isToday = date && this.isToday(date);
+ const isWeekend = date && (date.getDay() === 0 || date.getDay() === 6);
+
+ if (isToday) cell.classList.add('today');
+ if (isWeekend) cell.classList.add('weekend');
+
+ const dayNumber = document.createElement('div');
+ dayNumber.className = 'day-number' + (isWeekend ? ' weekend' : '');
+ dayNumber.textContent = day;
+ cell.appendChild(dayNumber);
+
+ const dateKey = this.getDateKey(date);
+ const dayTasks = this.tasks[dateKey] || [];
+
+ const tasksList = document.createElement('div');
+ tasksList.className = 'tasks-list';
+
+ dayTasks.forEach(task => {
+ const taskEl = this.createTaskElement(task, dateKey);
+ tasksList.appendChild(taskEl);
+ });
+
+ cell.appendChild(tasksList);
+
+ // Agregar zona de drop
+ const dropZone = document.createElement('div');
+ dropZone.className = 'drop-zone';
+ cell.appendChild(dropZone);
+
+ // Event listeners para drag & drop
+ cell.addEventListener('dragover', (e) => this.handleDragOver(e));
+ cell.addEventListener('drop', (e) => this.handleDrop(e, dateKey));
+ cell.addEventListener('dragleave', (e) => this.handleDragLeave(e));
+ }
+
+ return cell;
+ }
+
+ createTaskElement(task, dateKey) {
+ const taskEl = document.createElement('div');
+ taskEl.className = 'task-item';
+ taskEl.draggable = true;
+ taskEl.textContent = task.text;
+
+ const deleteBtn = document.createElement('button');
+ deleteBtn.type = 'button';
+ deleteBtn.className = 'task-delete-btn';
+ deleteBtn.textContent = '✕';
+ deleteBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ this.deleteTask(task.id, dateKey);
+ });
+
+ taskEl.appendChild(deleteBtn);
+
+ taskEl.addEventListener('dragstart', (e) => this.handleDragStart(e, task.id, dateKey));
+ taskEl.addEventListener('dragend', (e) => this.handleDragEnd(e));
+
+ return taskEl;
+ }
+
+ handleDragStart(e, taskId, fromDateKey) {
+ this.draggedTask = { taskId, fromDateKey };
+ e.target.classList.add('dragging');
+ e.dataTransfer.effectAllowed = 'move';
+ }
+
+ handleDragEnd(e) {
+ e.target.classList.remove('dragging');
+ }
+
+ handleDragOver(e) {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'move';
+ e.target.closest('.day-cell').classList.add('drag-over');
+ }
+
+ handleDragLeave(e) {
+ e.target.closest('.day-cell').classList.remove('drag-over');
+ }
+
+ handleDrop(e, toDateKey) {
+ e.preventDefault();
+ e.target.closest('.day-cell').classList.remove('drag-over');
+
+ if (!this.draggedTask) return;
+
+ const { taskId, fromDateKey } = this.draggedTask;
+ this.moveTask(taskId, fromDateKey, toDateKey);
+ this.draggedTask = null;
+ }
+
+ addTask(e) {
+ e.preventDefault();
+ const input = document.getElementById('taskInput');
+ const text = input.value.trim();
+
+ if (!text) return;
+
+ const today = this.getDateKey(new Date());
+ const task = {
+ id: Date.now(),
+ text: text,
+ date: today
+ };
+
+ if (!this.tasks[today]) {
+ this.tasks[today] = [];
+ }
+
+ this.tasks[today].push(task);
+ this.saveTasks();
+ input.value = '';
+ this.render();
+ }
+
+ deleteTask(taskId, dateKey) {
+ if (this.tasks[dateKey]) {
+ this.tasks[dateKey] = this.tasks[dateKey].filter(t => t.id !== taskId);
+ if (this.tasks[dateKey].length === 0) {
+ delete this.tasks[dateKey];
+ }
+ this.saveTasks();
+ this.render();
+ }
+ }
+
+ moveTask(taskId, fromDateKey, toDateKey) {
+ if (!this.tasks[fromDateKey]) return;
+
+ const taskIndex = this.tasks[fromDateKey].findIndex(t => t.id === taskId);
+ if (taskIndex === -1) return;
+
+ const task = this.tasks[fromDateKey][taskIndex];
+ this.tasks[fromDateKey].splice(taskIndex, 1);
+
+ if (this.tasks[fromDateKey].length === 0) {
+ delete this.tasks[fromDateKey];
+ }
+
+ if (!this.tasks[toDateKey]) {
+ this.tasks[toDateKey] = [];
+ }
+
+ this.tasks[toDateKey].push(task);
+ this.saveTasks();
+ this.render();
+ }
+
+ getDateKey(date) {
+ return date.toISOString().split('T')[0];
+ }
+
+ isToday(date) {
+ const today = new Date();
+ return date.toDateString() === today.toDateString();
+ }
+
+ saveTasks() {
+ localStorage.setItem('planningTasks', JSON.stringify(this.tasks));
+ }
+
+ loadTasks() {
+ const stored = localStorage.getItem('planningTasks');
+ return stored ? JSON.parse(stored) : {};
+ }
+
+ resetAll() {
+ if (confirm('¿Estás seguro de que deseas limpiar todas las tareas?')) {
+ this.tasks = {};
+ this.saveTasks();
+ this.render();
+ }
+ }
+ }
+
+ // Inicializar cuando el DOM esté listo
+ document.addEventListener('DOMContentLoaded', () => {
+ new PlanningCalendar();
+ });
+