diff --git a/backend/app.py b/backend/app.py index 67c2f88..53450d6 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,6 +1,6 @@ import os import sqlite3 -from flask import Flask, send_from_directory +from flask import Flask, send_from_directory, request, jsonify app = Flask(__name__) DB_PATH = os.environ.get('DB_PATH', '/app/db/pendientes.db') @@ -22,11 +22,79 @@ def init_db(): conn.close() +def get_db(): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + return conn + + @app.route('/') def index(): return send_from_directory('/app/frontend', 'index.html') +@app.route('/') +def static_files(path): + return send_from_directory('/app/frontend', path) + + +@app.route('/api/pendientes', methods=['GET']) +def listar_pendientes(): + conn = get_db() + pendientes = conn.execute('SELECT * FROM pendientes ORDER BY fecha').fetchall() + conn.close() + return jsonify([dict(row) for row in pendientes]) + + +@app.route('/api/pendientes', methods=['POST']) +def crear_pendiente(): + data = request.get_json() or {} + descripcion = data.get('descripcion', '') + fecha = data.get('fecha', '') + estado = data.get('estado', 'pendiente') + urgencia = data.get('urgencia', 'media') + + conn = get_db() + cursor = conn.execute( + 'INSERT INTO pendientes (descripcion, fecha, estado, urgencia) VALUES (?, ?, ?, ?)', + (descripcion, fecha, estado, urgencia) + ) + conn.commit() + nuevo_id = cursor.lastrowid + conn.close() + + return jsonify({'id': nuevo_id}), 201 + + +@app.route('/api/pendientes/', methods=['PUT']) +def actualizar_pendiente(id): + data = request.get_json() or {} + descripcion = data.get('descripcion', '') + fecha = data.get('fecha', '') + estado = data.get('estado', '') + urgencia = data.get('urgencia', '') + + conn = get_db() + conn.execute( + 'UPDATE pendientes SET descripcion = ?, fecha = ?, estado = ?, urgencia = ? WHERE id = ?', + (descripcion, fecha, estado, urgencia, id) + ) + conn.commit() + conn.close() + + return jsonify({'ok': True}) + + +@app.route('/api/pendientes/', methods=['DELETE']) +def eliminar_pendiente(id): + conn = get_db() + conn.execute('DELETE FROM pendientes WHERE id = ?', (id,)) + conn.commit() + conn.close() + + return jsonify({'ok': True}) + + if __name__ == '__main__': init_db() app.run(host='0.0.0.0', port=5000) diff --git a/db/pendientes.db b/db/pendientes.db index 39093e7..31b5f05 100644 Binary files a/db/pendientes.db and b/db/pendientes.db differ diff --git a/frontend/index.html b/frontend/index.html index 3e414ed..2292954 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,13 +1,58 @@ - - + + - - + + Calendario y Pendientes - + -

Calendario y Pendientes

- +
+
+

Calendario y Pendientes

+
+ + +
+
+
+ +

+ +
+
+
Dom
Lun
Mar
Mie
Jue
Vie
Sab
+
+
+
+
+
+ + + + + + + +
+
+
+
+ + diff --git a/frontend/script.js b/frontend/script.js index 1234046..3533024 100644 --- a/frontend/script.js +++ b/frontend/script.js @@ -1 +1,207 @@ -// Lógica del calendario y pendientes +const API = '/api/pendientes'; +let currentDate = new Date(); +let todosPendientes = []; + +const monthNames = ['Enero','Febrero','Marzo','Abril','Mayo','Junio','Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre']; + +async function cargarPendientes() { + const res = await fetch(API); + todosPendientes = await res.json(); + renderCalendar(); + renderLista(); +} + +function formatearFecha(fechaStr) { + const partes = fechaStr.split('-'); + return partes[2] + '-' + partes[1] + '-' + partes[0]; +} + +function hoyStr() { + const hoy = new Date(); + return hoy.getFullYear() + '-' + String(hoy.getMonth() + 1).padStart(2, '0') + '-' + String(hoy.getDate()).padStart(2, '0'); +} + +function esVencido(p) { + if (p.estado === 'completado') return false; + return p.fecha < hoyStr(); +} + +function renderCalendar() { + const year = currentDate.getFullYear(); + const month = currentDate.getMonth(); + document.getElementById('monthYear').textContent = monthNames[month] + ' ' + year; + + const firstDay = new Date(year, month, 1).getDay(); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + const grid = document.getElementById('calendarGrid'); + grid.innerHTML = ''; + + for (let i = 0; i < firstDay; i++) { + grid.appendChild(document.createElement('div')); + } + + for (let d = 1; d <= daysInMonth; d++) { + const dateStr = year + '-' + String(month + 1).padStart(2, '0') + '-' + String(d).padStart(2, '0'); + const delDia = todosPendientes.filter(function(p) { return p.fecha === dateStr; }); + const dayEl = document.createElement('div'); + dayEl.className = 'day'; + dayEl.innerHTML = '' + d + ''; + if (delDia.length) { + const dots = document.createElement('div'); + dots.className = 'day-dots'; + delDia.slice(0, 4).forEach(function(p) { + const dot = document.createElement('span'); + dot.className = 'dot ' + (esVencido(p) ? 'alta' : p.urgencia); + dots.appendChild(dot); + }); + dayEl.appendChild(dots); + } + dayEl.onclick = function() { abrirModalDia(dateStr, delDia); }; + grid.appendChild(dayEl); + } +} + +function abrirModalDia(fecha, pendientes) { + document.getElementById('modalTitulo').textContent = 'Pendientes del ' + formatearFecha(fecha); + const cont = document.getElementById('modalPendientes'); + cont.innerHTML = ''; + if (!pendientes.length) { + cont.innerHTML = '

No hay pendientes para este dia.

'; + } else { + pendientes.forEach(function(p) { + const div = crearCardPendiente(p); + cont.appendChild(div); + }); + } + document.getElementById('modalDia').classList.remove('hidden'); +} + +function crearCardPendiente(p) { + const vencido = esVencido(p); + const div = document.createElement('div'); + div.className = 'pendiente-card ' + (vencido ? 'vencido' : p.urgencia); + let html = '
' + + '
' + escaparHtml(p.descripcion) + ''; + if (vencido) { + html += 'Vencido'; + } + html += '
' + + '
' + formatearFecha(p.fecha) + ' - ' + p.estado + ' - Urgencia: ' + p.urgencia + '
' + + '
' + + '
' + + '' + + '' + + '
'; + div.innerHTML = html; + return div; +} + +function escaparHtml(texto) { + const div = document.createElement('div'); + div.textContent = texto; + return div.innerHTML; +} + +document.getElementById('cerrarModal').onclick = function() { + document.getElementById('modalDia').classList.add('hidden'); +}; + +document.getElementById('prevMonth').onclick = function() { + currentDate.setMonth(currentDate.getMonth() - 1); + renderCalendar(); +}; + +document.getElementById('nextMonth').onclick = function() { + currentDate.setMonth(currentDate.getMonth() + 1); + renderCalendar(); +}; + +document.querySelectorAll('.tab').forEach(function(tab) { + tab.onclick = function() { + document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); }); + document.querySelectorAll('.section').forEach(function(s) { s.classList.remove('active'); }); + tab.classList.add('active'); + document.getElementById(tab.dataset.tab).classList.add('active'); + }; +}); + +document.getElementById('formPendiente').onsubmit = async function(e) { + e.preventDefault(); + const id = document.getElementById('pendienteId').value; + const body = { + descripcion: document.getElementById('descripcion').value, + fecha: document.getElementById('fecha').value, + estado: document.getElementById('estado').value, + urgencia: document.getElementById('urgencia').value + }; + if (id) { + await fetch(API + '/' + id, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); + } else { + await fetch(API, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); + } + resetForm(); + cargarPendientes(); +}; + +document.getElementById('btnCancelar').onclick = resetForm; + +function resetForm() { + document.getElementById('formPendiente').reset(); + document.getElementById('pendienteId').value = ''; + document.getElementById('btnGuardar').textContent = 'Guardar'; + document.getElementById('btnCancelar').classList.add('hidden'); +} + +function renderLista() { + const cont = document.getElementById('listaPendientes'); + cont.innerHTML = ''; + + const vencidos = todosPendientes.filter(function(p) { return esVencido(p); }); + const normales = todosPendientes.filter(function(p) { return !esVencido(p); }); + + if (!todosPendientes.length) { + cont.innerHTML = '

No hay pendientes aun.

'; + return; + } + + if (vencidos.length) { + const titVencidos = document.createElement('div'); + titVencidos.className = 'subtitulo'; + titVencidos.textContent = 'Vencidos / Prioritarios (' + vencidos.length + ')'; + cont.appendChild(titVencidos); + vencidos.forEach(function(p) { + cont.appendChild(crearCardPendiente(p)); + }); + } + + if (normales.length) { + const titNormales = document.createElement('div'); + titNormales.className = 'subtitulo'; + titNormales.textContent = 'Pendientes (' + normales.length + ')'; + cont.appendChild(titNormales); + normales.forEach(function(p) { + cont.appendChild(crearCardPendiente(p)); + }); + } +} + +async function editarPendiente(id) { + const p = todosPendientes.find(function(x) { return x.id === id; }); + if (!p) return; + document.getElementById('pendienteId').value = p.id; + document.getElementById('descripcion').value = p.descripcion; + document.getElementById('fecha').value = p.fecha; + document.getElementById('estado').value = p.estado; + document.getElementById('urgencia').value = p.urgencia; + document.getElementById('btnGuardar').textContent = 'Actualizar'; + document.getElementById('btnCancelar').classList.remove('hidden'); + document.querySelector('.tab[data-tab=pendientes]').click(); +} + +async function eliminarPendiente(id) { + if (!confirm('Eliminar este pendiente?')) return; + await fetch(API + '/' + id, { method: 'DELETE' }); + cargarPendientes(); +} + +cargarPendientes(); diff --git a/frontend/style.css b/frontend/style.css index 7a2a2ee..bb24dc5 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -1 +1,373 @@ -/* Estilos del calendario y pendientes */ +* { + box-sizing: border-box; + -webkit-tap-highlight-color: transparent; +} + +body { + margin: 0; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + color: #fff; + overflow-x: hidden; + min-height: 100vh; +} + +.background { + position: fixed; + inset: 0; + background: linear-gradient(135deg, #0f2027, #203a43, #2c5364); + z-index: -2; +} + +.background::before { + content: ''; + position: absolute; + inset: -50%; + background: radial-gradient(circle at 20% 30%, rgba(255,255,255,0.1) 0%, transparent 40%), + radial-gradient(circle at 80% 70%, rgba(255,255,255,0.08) 0%, transparent 40%); + animation: move 20s infinite linear; + z-index: -1; +} + +@keyframes move { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.glass { + background: rgba(255, 255, 255, 0.08); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid rgba(255, 255, 255, 0.18); + border-radius: 20px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); +} + +.container { + max-width: 900px; + margin: 20px auto; + padding: 20px; +} + +h1 { + text-align: center; + margin-bottom: 18px; + font-weight: 300; + letter-spacing: 1px; + font-size: 1.6rem; +} + +.tabs { + display: flex; + justify-content: center; + gap: 12px; + margin-bottom: 20px; +} + +.tab { + padding: 10px 24px; + border: none; + border-radius: 30px; + background: rgba(255,255,255,0.1); + color: #fff; + cursor: pointer; + transition: 0.3s; + font-size: 1rem; + touch-action: manipulation; +} + +.tab:hover, .tab.active { + background: rgba(255,255,255,0.25); +} + +.section { + display: none; +} + +.section.active { + display: block; +} + +.calendar-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + gap: 10px; +} + +.calendar-header h2 { + font-size: 1.2rem; + margin: 0; + text-align: center; + flex: 1; +} + +.nav-btn { + background: rgba(255,255,255,0.15); + border: none; + color: #fff; + font-size: 1.4rem; + width: 42px; + height: 42px; + border-radius: 50%; + cursor: pointer; + transition: 0.3s; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + touch-action: manipulation; +} + +.nav-btn:hover { + background: rgba(255,255,255,0.3); +} + +.weekdays, .calendar-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 6px; +} + +.weekdays div { + text-align: center; + font-weight: 600; + padding: 8px 2px; + color: rgba(255,255,255,0.8); + font-size: 0.85rem; +} + +.day { + aspect-ratio: 1; + background: rgba(255,255,255,0.05); + border-radius: 10px; + padding: 4px; + cursor: pointer; + display: flex; + flex-direction: column; + align-items: flex-start; + transition: 0.2s; + position: relative; + overflow: hidden; + min-height: 42px; +} + +.day:hover, .day:active { + background: rgba(255,255,255,0.15); +} + +.day-number { + font-size: 0.9rem; + font-weight: 500; + line-height: 1; +} + +.day-dots { + display: flex; + gap: 3px; + margin-top: auto; + flex-wrap: wrap; + width: 100%; +} + +.dot { + width: 7px; + height: 7px; + border-radius: 50%; + flex-shrink: 0; +} + +.dot.alta { background: #ff6b6b; } +.dot.media { background: #feca57; } +.dot.baja { background: #48dbfb; } + +.glass-form { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 20px; + padding: 14px; + background: rgba(255,255,255,0.05); + border-radius: 16px; + border: 1px solid rgba(255,255,255,0.1); +} + +.glass-form input, .glass-form select { + flex: 1 1 160px; + padding: 12px 14px; + border-radius: 10px; + border: 1px solid rgba(255,255,255,0.15); + background: rgba(255,255,255,0.08); + color: #fff; + outline: none; + font-size: 1rem; +} + +.glass-form select option { + background: rgba(20, 30, 50, 0.95); + color: #fff; +} + +.glass-form input::placeholder { + color: rgba(255,255,255,0.5); +} + +.glass-form button { + padding: 12px 20px; + border: none; + border-radius: 10px; + background: rgba(255,255,255,0.2); + color: #fff; + cursor: pointer; + transition: 0.3s; + font-size: 1rem; + touch-action: manipulation; +} + +.glass-form button:hover { + background: rgba(255,255,255,0.35); +} + +.subtitulo { + margin: 18px 0 10px; + font-size: 1.05rem; + font-weight: 600; + color: rgba(255,255,255,0.85); + border-bottom: 1px solid rgba(255,255,255,0.15); + padding-bottom: 6px; +} + +.badge-vencido { + display: inline-block; + background: #ff2e2e; + color: #fff; + padding: 2px 8px; + border-radius: 12px; + font-size: 0.7rem; + font-weight: 700; + margin-left: 8px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.lista-pendientes { + display: flex; + flex-direction: column; + gap: 12px; +} + +.pendiente-card { + background: rgba(255,255,255,0.06); + border-radius: 14px; + padding: 14px; + border-left: 5px solid; + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 10px; +} + +.pendiente-card.alta { border-left-color: #ff6b6b; } +.pendiente-card.media { border-left-color: #feca57; } +.pendiente-card.baja { border-left-color: #48dbfb; } + +.pendiente-card.vencido { + border-left-color: #ff2e2e; + box-shadow: 0 0 14px rgba(255, 46, 46, 0.35); + background: rgba(255, 46, 46, 0.08); +} + +.pendiente-info { + flex: 1; + min-width: 0; +} + +.pendiente-info div { + margin-bottom: 4px; + word-break: break-word; +} + +.pendiente-actions { + display: flex; + flex-shrink: 0; + gap: 6px; +} + +.pendiente-actions button { + padding: 8px 12px; + border: none; + border-radius: 8px; + cursor: pointer; + background: rgba(255,255,255,0.15); + color: #fff; + transition: 0.3s; + font-size: 0.9rem; + touch-action: manipulation; + white-space: nowrap; +} + +.pendiente-actions button:hover { + background: rgba(255,255,255,0.3); +} + +.modal { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 100; + padding: 16px; +} + +.modal-content { + width: 100%; + max-width: 500px; + padding: 22px; + border-radius: 20px; + background: rgba(30, 40, 60, 0.9); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + max-height: 80vh; + overflow-y: auto; +} + +.modal-content h3 { + margin-top: 0; + font-size: 1.15rem; +} + +.modal-content button { + margin-top: 14px; + padding: 12px 20px; + border: none; + border-radius: 10px; + background: rgba(255,255,255,0.2); + color: #fff; + cursor: pointer; + font-size: 1rem; + width: 100%; +} + +.hidden { + display: none !important; +} + +@media (max-width: 430px) { + .container { + margin: 10px; + padding: 14px; + border-radius: 16px; + } + h1 { font-size: 1.35rem; } + .tab { padding: 10px 18px; font-size: 0.95rem; } + .weekdays div { font-size: 0.75rem; padding: 6px 1px; } + .day { border-radius: 8px; min-height: 48px; } + .day-number { font-size: 0.8rem; } + .dot { width: 6px; height: 6px; } + .calendar-header h2 { font-size: 1.05rem; } + .nav-btn { width: 38px; height: 38px; } + .pendiente-card { flex-direction: column; } + .pendiente-actions { width: 100%; justify-content: flex-end; } + .badge-vencido { font-size: 0.65rem; padding: 2px 6px; } +}