import React, { useState, useEffect, useCallback } from 'react'; import axios from 'axios'; import CpasBadge, { getTier } from './CpasBadge'; import EmployeeModal from './EmployeeModal'; import AuditLog from './AuditLog'; import DashboardMobile from './DashboardMobile'; const AT_RISK_THRESHOLD = 2; const TIERS = [ { min: 0, max: 4 }, { min: 5, max: 9 }, { min: 10, max: 14 }, { min: 15, max: 19 }, { min: 20, max: 24 }, { min: 25, max: 29 }, { min: 30, max: 999 }, ]; function nextTierBoundary(points) { for (const t of TIERS) { if (points >= t.min && points <= t.max && t.max < 999) return t.max + 1; } return null; } function isAtRisk(points) { const boundary = nextTierBoundary(points); return boundary !== null && (boundary - points) <= AT_RISK_THRESHOLD; } // TODO [MAJOR #8]: Same hook is defined in App.jsx — extract to src/hooks/useMediaQuery.js // Also: `matches` in the dep array can cause a loop on strict-mode initial mount. function useMediaQuery(query) { const [matches, setMatches] = useState(false); useEffect(() => { const media = window.matchMedia(query); if (media.matches !== matches) setMatches(media.matches); const listener = () => setMatches(media.matches); media.addEventListener('change', listener); return () => media.removeEventListener('change', listener); }, [matches, query]); return matches; } // Filter keys const FILTER_NONE = null; const FILTER_TOTAL = 'total'; const FILTER_ELITE = 'elite'; const FILTER_ACTIVE = 'active'; const FILTER_AT_RISK = 'at_risk'; const s = { wrap: { padding: '32px 40px', color: '#f8f9fa' }, header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px', flexWrap: 'wrap', gap: '12px' }, title: { fontSize: '24px', fontWeight: 700, color: '#f8f9fa' }, subtitle: { fontSize: '13px', color: '#b5b5c0', marginTop: '3px' }, statsRow: { display: 'flex', gap: '16px', flexWrap: 'wrap', marginBottom: '28px' }, statCard: { flex: '1', minWidth: '140px', background: '#181924', border: '1px solid #303136', borderRadius: '8px', padding: '16px', textAlign: 'center', cursor: 'pointer', transition: 'border-color 0.15s, box-shadow 0.15s' }, statCardActive: { boxShadow: '0 0 0 2px #d4af37', border: '1px solid #d4af37' }, statNum: { fontSize: '28px', fontWeight: 800, color: '#f8f9fa' }, statLbl: { fontSize: '11px', color: '#b5b5c0', marginTop: '4px' }, filterBadge: { fontSize: '10px', color: '#d4af37', marginTop: '4px', fontWeight: 600 }, search: { padding: '10px 14px', border: '1px solid #333544', borderRadius: '6px', fontSize: '14px', width: '260px', background: '#050608', color: '#f8f9fa' }, table: { width: '100%', borderCollapse: 'collapse', background: '#111217', borderRadius: '8px', overflow: 'hidden', boxShadow: '0 1px 8px rgba(0,0,0,0.6)', border: '1px solid #222' }, th: { background: '#000000', color: '#f8f9fa', padding: '10px 14px', textAlign: 'left', fontSize: '12px', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.5px' }, td: { padding: '11px 14px', borderBottom: '1px solid #1c1d29', fontSize: '13px', verticalAlign: 'middle', color: '#f8f9fa' }, nameBtn: { background: 'none', border: 'none', cursor: 'pointer', fontWeight: 600, color: '#d4af37', fontSize: '14px', padding: 0, textDecoration: 'underline dotted' }, atRiskBadge: { display: 'inline-block', marginLeft: '8px', padding: '2px 8px', borderRadius: '10px', fontSize: '10px', fontWeight: 700, background: '#3b2e00', color: '#ffd666', border: '1px solid #d4af37', verticalAlign: 'middle' }, zeroRow: { color: '#77798a', fontStyle: 'italic', fontSize: '12px' }, toolbarRight: { display: 'flex', gap: '10px', alignItems: 'center' }, refreshBtn: { padding: '9px 18px', background: '#d4af37', color: '#000', border: 'none', borderRadius: '6px', cursor: 'pointer', fontWeight: 600, fontSize: '13px' }, auditBtn: { padding: '9px 18px', background: 'none', color: '#9ca0b8', border: '1px solid #2a2b3a', borderRadius: '6px', cursor: 'pointer', fontWeight: 600, fontSize: '13px' }, }; // Mobile styles const mobileStyles = ` @media (max-width: 768px) { .dashboard-wrap { padding: 16px !important; } .dashboard-header { flex-direction: column; align-items: flex-start !important; } .dashboard-title { font-size: 20px !important; } .dashboard-subtitle { font-size: 12px !important; } .dashboard-stats { gap: 10px !important; } .dashboard-stat-card { min-width: calc(50% - 5px) !important; padding: 12px !important; } .stat-num { font-size: 24px !important; } .stat-lbl { font-size: 10px !important; } .toolbar-right { width: 100%; flex-direction: column; } .search-input { width: 100% !important; } .toolbar-btn { width: 100%; justify-content: center; } } @media (max-width: 480px) { .dashboard-stat-card { min-width: 100% !important; } } `; export default function Dashboard() { const [employees, setEmployees] = useState([]); const [filtered, setFiltered] = useState([]); const [search, setSearch] = useState(''); const [selectedId, setSelectedId] = useState(null); const [showAudit, setShowAudit] = useState(false); const [loading, setLoading] = useState(true); const [activeFilter, setActiveFilter] = useState(FILTER_NONE); const isMobile = useMediaQuery('(max-width: 768px)'); const load = useCallback(() => { setLoading(true); axios.get('/api/dashboard') .then(r => { setEmployees(r.data); setFiltered(r.data); }) .finally(() => setLoading(false)); }, []); useEffect(() => { load(); }, [load]); // Apply search + badge filter together useEffect(() => { const q = search.toLowerCase(); let base = employees; if (activeFilter === FILTER_ELITE) { base = base.filter(e => e.active_points >= 0 && e.active_points <= 4); } else if (activeFilter === FILTER_ACTIVE) { base = base.filter(e => e.active_points > 0); } else if (activeFilter === FILTER_AT_RISK) { base = base.filter(e => isAtRisk(e.active_points)); } // FILTER_TOTAL and FILTER_NONE show all if (q) { base = base.filter(e => e.name.toLowerCase().includes(q) || (e.department || '').toLowerCase().includes(q) || (e.supervisor || '').toLowerCase().includes(q) ); } setFiltered(base); }, [search, employees, activeFilter]); const atRiskCount = employees.filter(e => isAtRisk(e.active_points)).length; const activeCount = employees.filter(e => e.active_points > 0).length; // Elite Standing: 0–4 pts (Tier 0-1) const eliteCount = employees.filter(e => e.active_points >= 0 && e.active_points <= 4).length; const maxPoints = employees.reduce((m, e) => Math.max(m, e.active_points), 0); function handleBadgeClick(filterKey) { setActiveFilter(prev => prev === filterKey ? FILTER_NONE : filterKey); } function cardStyle(filterKey, extra = {}) { const isActive = activeFilter === filterKey; return { ...s.statCard, ...(isActive ? s.statCardActive : {}), ...extra, }; } return ( <> {/* TODO [MAJOR #9]: Same mobileStyles block exists in App.jsx. Move to mobile.css */}
Company Dashboard
Click any employee name to view their full profile {activeFilter && activeFilter !== FILTER_NONE && ( · Filtered: {activeFilter === FILTER_ELITE ? 'Elite Standing (0–4 pts)' : activeFilter === FILTER_ACTIVE ? 'With Active Points' : activeFilter === FILTER_AT_RISK ? 'At Risk' : 'All'} )}
setSearch(e.target.value)} />
{/* Total Employees — clicking shows all */}
handleBadgeClick(FILTER_TOTAL)} title="Click to show all employees" >
{employees.length}
Total Employees
{activeFilter === FILTER_TOTAL &&
▼ Showing All
}
{/* Elite Standing: 0–4 pts */}
handleBadgeClick(FILTER_ELITE)} title="Click to filter: Elite Standing (0–4 pts)" >
{eliteCount}
Elite Standing (0–4 pts)
{activeFilter === FILTER_ELITE &&
▼ Filtered
}
{/* With Active Points */}
handleBadgeClick(FILTER_ACTIVE)} title="Click to filter: employees with active points" >
{activeCount}
With Active Points
{activeFilter === FILTER_ACTIVE &&
▼ Filtered
}
{/* At Risk */}
handleBadgeClick(FILTER_AT_RISK)} title={`Click to filter: at risk (≤${AT_RISK_THRESHOLD} pts to next tier)`} >
{atRiskCount}
At Risk (≤{AT_RISK_THRESHOLD} pts to next tier)
{activeFilter === FILTER_AT_RISK &&
▼ Filtered
}
{/* Highest Score — display only, no filter */}
{maxPoints}
Highest Active Score
{loading ? (

Loading…

) : isMobile ? ( ) : ( {filtered.length === 0 && ( )} {filtered.map((emp, i) => { const risk = isAtRisk(emp.active_points); const tier = getTier(emp.active_points); const boundary = nextTierBoundary(emp.active_points); return ( ); })}
# Employee Department Supervisor Tier / Standing Active Points 90-Day Violations
No employees found.
{i + 1} {risk && ( ⚠ {boundary - emp.active_points} pt{boundary - emp.active_points > 1 ? 's' : ''} to {getTier(boundary).label.split('—')[0].trim()} )} {emp.department || '—'} {emp.supervisor || '—'} {emp.active_points} {emp.violation_count}
)}
{selectedId && ( { setSelectedId(null); load(); }} /> )} {showAudit && setShowAudit(false)} />} ); }