feat(calendar): live calendar with Ivanti due date indicators
- Replace hardcoded Feb 2024 static HTML with dynamic CalendarWidget component
- Auto-displays current month on load; prev/next chevron navigation
- Fetches /api/ivanti/findings on mount and builds a date→count map
- Days with findings due: date number rendered in red bold + red glowing dot below
- Today: sky-blue highlight + bold (combined with red if also a due date)
- Legend appears automatically when the displayed month has any due dates
- Tooltip on due-date cells shows count ("3 findings due")
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import NvdSyncModal from './components/NvdSyncModal';
|
||||
import KnowledgeBaseModal from './components/KnowledgeBaseModal';
|
||||
import KnowledgeBaseViewer from './components/KnowledgeBaseViewer';
|
||||
import NavDrawer from './components/NavDrawer';
|
||||
import CalendarWidget from './components/CalendarWidget';
|
||||
import ReportingPage from './components/pages/ReportingPage';
|
||||
import KnowledgeBasePage from './components/pages/KnowledgeBasePage';
|
||||
import ExportsPage from './components/pages/ExportsPage';
|
||||
@@ -2219,63 +2220,7 @@ export default function App() {
|
||||
Calendar
|
||||
</h2>
|
||||
|
||||
{/* Simple Calendar Grid */}
|
||||
<div className="mb-2">
|
||||
<div className="text-center mb-3">
|
||||
<span className="text-white font-semibold font-mono">February 2024</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-7 gap-1 text-center text-xs mb-2">
|
||||
<div className="text-gray-400 font-mono">Su</div>
|
||||
<div className="text-gray-400 font-mono">Mo</div>
|
||||
<div className="text-gray-400 font-mono">Tu</div>
|
||||
<div className="text-gray-400 font-mono">We</div>
|
||||
<div className="text-gray-400 font-mono">Th</div>
|
||||
<div className="text-gray-400 font-mono">Fr</div>
|
||||
<div className="text-gray-400 font-mono">Sa</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-7 gap-1 text-center">
|
||||
{/* Week 1 */}
|
||||
<div className="text-gray-600 font-mono text-xs p-1">28</div>
|
||||
<div className="text-gray-600 font-mono text-xs p-1">29</div>
|
||||
<div className="text-gray-600 font-mono text-xs p-1">30</div>
|
||||
<div className="text-gray-600 font-mono text-xs p-1">31</div>
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">1</div>
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">2</div>
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">3</div>
|
||||
{/* Week 2 */}
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">4</div>
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">5</div>
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">6</div>
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">7</div>
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">8</div>
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">9</div>
|
||||
<div className="bg-intel-accent/30 text-white font-mono text-xs p-1 rounded font-bold border border-intel-accent">10</div>
|
||||
{/* Week 3 */}
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">11</div>
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">12</div>
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">13</div>
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">14</div>
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">15</div>
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">16</div>
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">17</div>
|
||||
{/* Week 4 */}
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">18</div>
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">19</div>
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">20</div>
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">21</div>
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">22</div>
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">23</div>
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">24</div>
|
||||
{/* Week 5 */}
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">25</div>
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">26</div>
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">27</div>
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">28</div>
|
||||
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">29</div>
|
||||
<div className="text-gray-600 font-mono text-xs p-1">1</div>
|
||||
<div className="text-gray-600 font-mono text-xs p-1">2</div>
|
||||
</div>
|
||||
</div>
|
||||
<CalendarWidget />
|
||||
</div>
|
||||
|
||||
{/* Open Vendor Tickets */}
|
||||
|
||||
163
frontend/src/components/CalendarWidget.js
Normal file
163
frontend/src/components/CalendarWidget.js
Normal file
@@ -0,0 +1,163 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
|
||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
||||
|
||||
const MONTH_NAMES = [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December'
|
||||
];
|
||||
const DAY_NAMES = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
||||
|
||||
function toLocalDateStr(date) {
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(date.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
export default function CalendarWidget() {
|
||||
const today = new Date();
|
||||
const todayStr = toLocalDateStr(today);
|
||||
|
||||
const [calYear, setCalYear] = useState(today.getFullYear());
|
||||
const [calMonth, setCalMonth] = useState(today.getMonth()); // 0-indexed
|
||||
|
||||
// Map of "YYYY-MM-DD" → count of findings due that day
|
||||
const [dueDates, setDueDates] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/ivanti/findings`, { credentials: 'include' })
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((data) => {
|
||||
if (!data?.findings) return;
|
||||
const counts = {};
|
||||
data.findings.forEach((f) => {
|
||||
if (f.dueDate) {
|
||||
counts[f.dueDate] = (counts[f.dueDate] || 0) + 1;
|
||||
}
|
||||
});
|
||||
setDueDates(counts);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const prevMonth = () => {
|
||||
if (calMonth === 0) { setCalMonth(11); setCalYear((y) => y - 1); }
|
||||
else { setCalMonth((m) => m - 1); }
|
||||
};
|
||||
|
||||
const nextMonth = () => {
|
||||
if (calMonth === 11) { setCalMonth(0); setCalYear((y) => y + 1); }
|
||||
else { setCalMonth((m) => m + 1); }
|
||||
};
|
||||
|
||||
// Build cell array: null = padding, number = day of month
|
||||
const firstDow = new Date(calYear, calMonth, 1).getDay(); // 0=Sun
|
||||
const daysInMonth = new Date(calYear, calMonth + 1, 0).getDate();
|
||||
const cells = [
|
||||
...Array(firstDow).fill(null),
|
||||
...Array.from({ length: daysInMonth }, (_, i) => i + 1),
|
||||
];
|
||||
while (cells.length % 7 !== 0) cells.push(null); // complete last row
|
||||
|
||||
const hasDueDatesThisMonth = cells.some((day) => {
|
||||
if (!day) return false;
|
||||
const ds = `${calYear}-${String(calMonth + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
return !!dueDates[ds];
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Month navigation */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '0.75rem' }}>
|
||||
<button
|
||||
onClick={prevMonth}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#64748B', padding: '2px 4px', borderRadius: '4px', lineHeight: 1, transition: 'color 0.15s' }}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.color = '#0EA5E9'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.color = '#64748B'; }}
|
||||
>
|
||||
<ChevronLeft style={{ width: '14px', height: '14px' }} />
|
||||
</button>
|
||||
|
||||
<span style={{ color: '#E2E8F0', fontFamily: 'monospace', fontWeight: '600', fontSize: '0.85rem' }}>
|
||||
{MONTH_NAMES[calMonth]} {calYear}
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={nextMonth}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#64748B', padding: '2px 4px', borderRadius: '4px', lineHeight: 1, transition: 'color 0.15s' }}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.color = '#0EA5E9'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.color = '#64748B'; }}
|
||||
>
|
||||
<ChevronRight style={{ width: '14px', height: '14px' }} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Day-of-week headers */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: '2px', textAlign: 'center', marginBottom: '4px' }}>
|
||||
{DAY_NAMES.map((d) => (
|
||||
<div key={d} style={{ fontSize: '0.6rem', color: '#475569', fontFamily: 'monospace', fontWeight: '600', textTransform: 'uppercase' }}>
|
||||
{d}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Day cells */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: '2px' }}>
|
||||
{cells.map((day, idx) => {
|
||||
if (!day) return <div key={idx} />;
|
||||
|
||||
const dateStr = `${calYear}-${String(calMonth + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
const isToday = dateStr === todayStr;
|
||||
const dueCount = dueDates[dateStr] || 0;
|
||||
const hasDue = dueCount > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
title={hasDue ? `${dueCount} finding${dueCount > 1 ? 's' : ''} due` : undefined}
|
||||
style={{
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||
gap: '2px', padding: '3px 1px',
|
||||
borderRadius: '4px',
|
||||
background: isToday ? 'rgba(14,165,233,0.2)' : 'transparent',
|
||||
border: isToday ? '1px solid rgba(14,165,233,0.5)' : '1px solid transparent',
|
||||
cursor: hasDue ? 'default' : 'default',
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
fontSize: '0.7rem', fontFamily: 'monospace', lineHeight: 1,
|
||||
color: isToday ? '#0EA5E9' : hasDue ? '#EF4444' : '#CBD5E1',
|
||||
fontWeight: (isToday || hasDue) ? '700' : '400',
|
||||
}}>
|
||||
{day}
|
||||
</span>
|
||||
{/* Red dot indicator for due dates */}
|
||||
{hasDue ? (
|
||||
<div style={{
|
||||
width: '4px', height: '4px', borderRadius: '50%',
|
||||
background: '#EF4444',
|
||||
boxShadow: '0 0 4px rgba(239,68,68,0.6)',
|
||||
flexShrink: 0,
|
||||
}} />
|
||||
) : (
|
||||
<div style={{ width: '4px', height: '4px' }} /> // spacer to keep rows even
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Legend — only shown when there are due dates this month */}
|
||||
{hasDueDatesThisMonth && (
|
||||
<div style={{ marginTop: '0.75rem', paddingTop: '0.625rem', borderTop: '1px solid rgba(255,255,255,0.05)', display: 'flex', alignItems: 'center', gap: '0.375rem' }}>
|
||||
<div style={{ width: '6px', height: '6px', borderRadius: '50%', background: '#EF4444', boxShadow: '0 0 4px rgba(239,68,68,0.5)', flexShrink: 0 }} />
|
||||
<span style={{ fontSize: '0.62rem', color: '#64748B', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
||||
Ivanti finding due
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user