From c3da4cb6f02a6b72b89f763c489ad3c7f62a42db Mon Sep 17 00:00:00 2001 From: Ricardo Monla Date: Tue, 10 Mar 2026 17:45:46 -0300 Subject: [PATCH] =?UTF-8?q?Feat(Web):=20Mini=20Calendario=20con=20indicado?= =?UTF-8?q?res=20de=20d=C3=ADas=20activos=20en=20bit=C3=A1coras=20-=20DB-F?= =?UTF-8?q?irst?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/routes/bitacoras.js | 11 +++ .../nginx/dtic-bitacoras/frontend/src/App.tsx | 82 ++++++++++++++++- .../dtic-bitacoras/frontend/src/index.css | 91 ++++++++++++++++--- 3 files changed, 168 insertions(+), 16 deletions(-) diff --git a/servicios/nginx/dtic-bitacoras/backend/src/routes/bitacoras.js b/servicios/nginx/dtic-bitacoras/backend/src/routes/bitacoras.js index ed280d97..e1ebccbc 100644 --- a/servicios/nginx/dtic-bitacoras/backend/src/routes/bitacoras.js +++ b/servicios/nginx/dtic-bitacoras/backend/src/routes/bitacoras.js @@ -19,6 +19,17 @@ module.exports = (pool) => { res.json(rows); }); + // GET /api/bitacoras/fechas/activas — Fechas que tienen entradas cargadas (para el calendario) + router.get('/fechas/activas', async (req, res) => { + const { rows } = await pool.query(` + SELECT b.fecha::text + FROM bitacoras.bitacoras b + WHERE EXISTS (SELECT 1 FROM bitacoras.entradas e WHERE e.bitacora_id = b.id) + ORDER BY b.fecha DESC + `); + res.json(rows.map(r => r.fecha)); + }); + // GET /api/bitacoras/:fecha/completa — Bitácora completa con entradas agrupadas por nodo router.get('/:fecha/completa', async (req, res) => { const { fecha } = req.params; diff --git a/servicios/nginx/dtic-bitacoras/frontend/src/App.tsx b/servicios/nginx/dtic-bitacoras/frontend/src/App.tsx index 24ef23a9..7afcc994 100644 --- a/servicios/nginx/dtic-bitacoras/frontend/src/App.tsx +++ b/servicios/nginx/dtic-bitacoras/frontend/src/App.tsx @@ -99,6 +99,9 @@ function App() { const [proyectos, setProyectos] = useState<{ id: number, codigo: string, nombre: string }[]>([]); const [selectedProyecto, setSelectedProyecto] = useState('P2601'); const [loading, setLoading] = useState(true); + const [fechasActivas, setFechasActivas] = useState>(new Set()); + const [showCal, setShowCal] = useState(false); + const [calMonth, setCalMonth] = useState(() => { const d = new Date(); return { year: d.getFullYear(), month: d.getMonth() }; }); // Form: nueva entrada const [newInicio, setNewInicio] = useState(''); @@ -212,6 +215,14 @@ function App() { } catch (err) { console.error('Error:', err); } }; + const fetchFechasActivas = async () => { + try { + const res = await fetch(`${API_URL}/bitacoras/fechas/activas`); + const data: string[] = await res.json(); + setFechasActivas(new Set(data)); + } catch (err) { console.error('Error fetchFechasActivas:', err); } + }; + const fetchProyectos = async () => { try { const res = await fetch(`${API_URL}/proyectos`); @@ -275,7 +286,34 @@ function App() { }; useEffect(() => { fetchBitacora(); }, [fetchBitacora]); - useEffect(() => { fetchNodos(); fetchProyectos(); }, []); + useEffect(() => { fetchNodos(); fetchProyectos(); fetchFechasActivas(); }, []); + + // Rebuild calendar view month when fecha changes + useEffect(() => { + const d = new Date(fecha + 'T12:00:00'); + setCalMonth({ year: d.getFullYear(), month: d.getMonth() }); + }, [fecha]); + + // Mini calendario builder + const buildCalDays = () => { + const { year, month } = calMonth; + const firstDay = new Date(year, month, 1).getDay(); // 0=Sun + const daysInMonth = new Date(year, month + 1, 0).getDate(); + const rows: { day: number; dateStr: string; active: boolean; selected: boolean; today: boolean }[][] = []; + let row: typeof rows[0] = []; + const todayStr = new Date().toISOString().split('T')[0]; + // Fill blanks + for (let i = 0; i < firstDay; i++) row.push({ day: 0, dateStr: '', active: false, selected: false, today: false }); + for (let d = 1; d <= daysInMonth; d++) { + const ds = `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`; + row.push({ day: d, dateStr: ds, active: fechasActivas.has(ds), selected: ds === fecha, today: ds === todayStr }); + if (row.length === 7) { rows.push(row); row = []; } + } + if (row.length > 0) { while (row.length < 7) row.push({ day: 0, dateStr: '', active: false, selected: false, today: false }); rows.push(row); } + return rows; + }; + + const MONTH_NAMES = ['Ene','Feb','Mar','Abr','May','Jun','Jul','Ago','Sep','Oct','Nov','Dic']; // === ENTRADAS CRUD === const handleSubmit = async (e: React.FormEvent) => { @@ -430,20 +468,54 @@ function App() { {/* =================== TAB BITÁCORA =================== */} {tab === 'bitacora' && ( <> - {/* Date Selector */} + {/* Date Selector con Mini Calendario */}
- setFecha(e.target.value)} /> +
+ {showCal && ( +
+
+ + {MONTH_NAMES[calMonth.month]} {calMonth.year} + +
+ + {'DLMMJVS'.split('').map((d,i)=>)} + + {buildCalDays().map((row, ri) => ( + {row.map((c, ci) => ( + + ))} + ))} + +
{d}
{ if (c.day > 0) { setFecha(c.dateStr); setShowCal(false); } }} + >{c.day > 0 ? c.day : ''}
+
+ )} {/* Stats */}
diff --git a/servicios/nginx/dtic-bitacoras/frontend/src/index.css b/servicios/nginx/dtic-bitacoras/frontend/src/index.css index 2bc9a088..b96971e8 100644 --- a/servicios/nginx/dtic-bitacoras/frontend/src/index.css +++ b/servicios/nginx/dtic-bitacoras/frontend/src/index.css @@ -308,23 +308,92 @@ body { .date-selector { display: flex; align-items: center; - gap: 1rem; - margin-bottom: 2rem; + gap: 0.5rem; + margin-bottom: 0.5rem; + position: relative; } -.date-selector input[type="date"] { +/* Mini Calendario */ +.mini-cal { background: var(--bg-card); border: 1px solid var(--border); - color: var(--text); - padding: 0.6rem 1rem; - border-radius: 8px; - font-size: 1rem; - font-family: inherit; + border-radius: 12px; + padding: 0.75rem; + margin-bottom: 1.5rem; + box-shadow: 0 8px 32px rgba(0,0,0,0.25); + max-width: 320px; + animation: calSlide 0.2s ease-out; } +@keyframes calSlide { from { opacity:0; transform:translateY(-8px); } to { opacity:1; transform:translateY(0); } } -.date-selector input[type="date"]:focus { - outline: none; - border-color: var(--primary); +.mini-cal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; + font-weight: 700; + font-size: 0.95rem; + color: var(--text); +} +.mini-cal-header button { + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + padding: 0.25rem 0.5rem; + border-radius: 6px; + transition: background 0.15s; +} +.mini-cal-header button:hover { background: var(--bg-hover); } + +.mini-cal-grid { + width: 100%; + border-collapse: collapse; + table-layout: fixed; +} +.mini-cal-grid th { + color: var(--text-secondary); + font-size: 0.7rem; + font-weight: 600; + text-align: center; + padding: 0.25rem; +} +.mini-cal-grid td { + text-align: center; + padding: 0.3rem; + font-size: 0.82rem; + cursor: pointer; + border-radius: 8px; + transition: all 0.15s; + position: relative; + color: var(--text-secondary); +} +.mini-cal-grid td:hover:not(.empty) { background: var(--bg-hover); } +.mini-cal-grid td.empty { cursor: default; } +.mini-cal-grid td.has-data { + color: var(--text); + font-weight: 700; +} +.mini-cal-grid td.has-data::after { + content: ''; + display: block; + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--primary); + margin: 2px auto 0; + box-shadow: 0 0 6px var(--primary); +} +.mini-cal-grid td.selected { + background: var(--primary); + color: #fff !important; + font-weight: 700; + box-shadow: 0 0 12px rgba(99,102,241,0.5); +} +.mini-cal-grid td.selected::after { background: #fff; box-shadow: none; } +.mini-cal-grid td.today:not(.selected) { + outline: 2px solid var(--primary); + outline-offset: -2px; } /* Stats bar */