Feat(Web): Mini Calendario con indicadores de días activos en bitácoras - DB-First
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user