Feat(Web): Mini Calendario con indicadores de días activos en bitácoras - DB-First

This commit is contained in:
Ricardo Monla
2026-03-10 17:45:46 -03:00
parent 8d27a8958e
commit c3da4cb6f0
3 changed files with 168 additions and 16 deletions
@@ -99,6 +99,9 @@ function App() {
const [proyectos, setProyectos] = useState<{ id: number, codigo: string, nombre: string }[]>([]);
const [selectedProyecto, setSelectedProyecto] = useState<string>('P2601');
const [loading, setLoading] = useState(true);
const [fechasActivas, setFechasActivas] = useState<Set<string>>(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 */}
<div className="date-selector">
<button className="btn btn-sm btn-primary" onClick={() => {
const d = new Date(fecha); d.setDate(d.getDate() - 1); setFecha(d.toISOString().split('T')[0]);
const d = new Date(fecha + 'T12:00:00'); d.setDate(d.getDate() - 1); setFecha(d.toISOString().split('T')[0]);
}}><i className="fas fa-chevron-left"></i></button>
<input type="date" value={fecha} onChange={e => setFecha(e.target.value)} />
<button className="btn btn-sm cal-toggle" onClick={() => setShowCal(v => !v)}
style={{ background: 'var(--bg-card)', color: 'var(--text)', border: '1px solid var(--border)', minWidth: 160, fontWeight: 600 }}>
<i className="fas fa-calendar-alt"></i> {fecha}
</button>
<button className="btn btn-sm btn-primary" onClick={() => {
const d = new Date(fecha); d.setDate(d.getDate() + 1); setFecha(d.toISOString().split('T')[0]);
const d = new Date(fecha + 'T12:00:00'); d.setDate(d.getDate() + 1); setFecha(d.toISOString().split('T')[0]);
}}><i className="fas fa-chevron-right"></i></button>
<button className="btn btn-sm" onClick={() => setFecha(new Date().toISOString().split('T')[0])}
style={{ background: 'var(--bg-card)', color: 'var(--text-secondary)', border: '1px solid var(--border)' }}>
<i className="fas fa-calendar-day"></i> Hoy
</button>
</div>
{showCal && (
<div className="mini-cal">
<div className="mini-cal-header">
<button onClick={() => setCalMonth(p => p.month === 0 ? { year: p.year - 1, month: 11 } : { ...p, month: p.month - 1 })}>
<i className="fas fa-chevron-left"></i>
</button>
<span>{MONTH_NAMES[calMonth.month]} {calMonth.year}</span>
<button onClick={() => setCalMonth(p => p.month === 11 ? { year: p.year + 1, month: 0 } : { ...p, month: p.month + 1 })}>
<i className="fas fa-chevron-right"></i>
</button>
</div>
<table className="mini-cal-grid">
<thead><tr>{'DLMMJVS'.split('').map((d,i)=><th key={i}>{d}</th>)}</tr></thead>
<tbody>
{buildCalDays().map((row, ri) => (
<tr key={ri}>{row.map((c, ci) => (
<td key={ci}
className={[
c.day === 0 ? 'empty' : '',
c.active ? 'has-data' : '',
c.selected ? 'selected' : '',
c.today ? 'today' : ''
].join(' ')}
onClick={() => { if (c.day > 0) { setFecha(c.dateStr); setShowCal(false); } }}
>{c.day > 0 ? c.day : ''}</td>
))}</tr>
))}
</tbody>
</table>
</div>
)}
{/* Stats */}
<div className="stats-bar">