Merge pull request 'feat: dashboard badge filters + Elite Standing 0–4 pts' (#43) from feature/dashboard-badge-filters into master
Reviewed-on: #43
This commit was merged in pull request #43.
This commit is contained in:
@@ -42,15 +42,24 @@ function useMediaQuery(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' },
|
||||
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' },
|
||||
@@ -119,6 +128,7 @@ export default function Dashboard() {
|
||||
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(() => {
|
||||
@@ -130,20 +140,50 @@ export default function Dashboard() {
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
// Apply search + badge filter together
|
||||
useEffect(() => {
|
||||
const q = search.toLowerCase();
|
||||
setFiltered(employees.filter(e =>
|
||||
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)
|
||||
));
|
||||
}, [search, employees]);
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
const cleanCount = 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 (
|
||||
<>
|
||||
<style>{mobileStyles}</style>
|
||||
@@ -151,7 +191,19 @@ export default function Dashboard() {
|
||||
<div style={s.header} className="dashboard-header">
|
||||
<div>
|
||||
<div style={s.title} className="dashboard-title">Company Dashboard</div>
|
||||
<div style={s.subtitle} className="dashboard-subtitle">Click any employee name to view their full profile</div>
|
||||
<div style={s.subtitle} className="dashboard-subtitle">
|
||||
Click any employee name to view their full profile
|
||||
{activeFilter && activeFilter !== FILTER_NONE && (
|
||||
<span style={{ marginLeft: '10px', color: '#d4af37', fontWeight: 600 }}>
|
||||
· Filtered: {activeFilter === FILTER_ELITE ? 'Elite Standing (0–4 pts)' : activeFilter === FILTER_ACTIVE ? 'With Active Points' : activeFilter === FILTER_AT_RISK ? 'At Risk' : 'All'}
|
||||
<button
|
||||
onClick={() => setActiveFilter(FILTER_NONE)}
|
||||
style={{ marginLeft: '6px', background: 'none', border: 'none', color: '#9ca0b8', cursor: 'pointer', fontSize: '12px' }}
|
||||
title="Clear filter"
|
||||
>✕</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={s.toolbarRight} className="toolbar-right">
|
||||
<input
|
||||
@@ -167,23 +219,59 @@ export default function Dashboard() {
|
||||
</div>
|
||||
|
||||
<div style={s.statsRow} className="dashboard-stats">
|
||||
<div style={s.statCard} className="dashboard-stat-card">
|
||||
{/* Total Employees — clicking shows all */}
|
||||
<div
|
||||
style={cardStyle(FILTER_TOTAL)}
|
||||
className="dashboard-stat-card"
|
||||
onClick={() => handleBadgeClick(FILTER_TOTAL)}
|
||||
title="Click to show all employees"
|
||||
>
|
||||
<div style={s.statNum} className="stat-num">{employees.length}</div>
|
||||
<div style={s.statLbl} className="stat-lbl">Total Employees</div>
|
||||
{activeFilter === FILTER_TOTAL && <div style={s.filterBadge}>▼ Showing All</div>}
|
||||
</div>
|
||||
<div style={{ ...s.statCard, borderTop: '3px solid #28a745' }} className="dashboard-stat-card">
|
||||
<div style={{ ...s.statNum, color: '#6ee7b7' }} className="stat-num">{cleanCount}</div>
|
||||
<div style={s.statLbl} className="stat-lbl">Elite Standing (0 pts)</div>
|
||||
|
||||
{/* Elite Standing: 0–4 pts */}
|
||||
<div
|
||||
style={cardStyle(FILTER_ELITE, { borderTop: '3px solid #28a745' })}
|
||||
className="dashboard-stat-card"
|
||||
onClick={() => handleBadgeClick(FILTER_ELITE)}
|
||||
title="Click to filter: Elite Standing (0–4 pts)"
|
||||
>
|
||||
<div style={{ ...s.statNum, color: '#6ee7b7' }} className="stat-num">{eliteCount}</div>
|
||||
<div style={s.statLbl} className="stat-lbl">Elite Standing (0–4 pts)</div>
|
||||
{activeFilter === FILTER_ELITE && <div style={s.filterBadge}>▼ Filtered</div>}
|
||||
</div>
|
||||
<div style={{ ...s.statCard, borderTop: '3px solid #d4af37' }} className="dashboard-stat-card">
|
||||
|
||||
{/* With Active Points */}
|
||||
<div
|
||||
style={cardStyle(FILTER_ACTIVE, { borderTop: '3px solid #d4af37' })}
|
||||
className="dashboard-stat-card"
|
||||
onClick={() => handleBadgeClick(FILTER_ACTIVE)}
|
||||
title="Click to filter: employees with active points"
|
||||
>
|
||||
<div style={{ ...s.statNum, color: '#ffd666' }} className="stat-num">{activeCount}</div>
|
||||
<div style={s.statLbl} className="stat-lbl">With Active Points</div>
|
||||
{activeFilter === FILTER_ACTIVE && <div style={s.filterBadge}>▼ Filtered</div>}
|
||||
</div>
|
||||
<div style={{ ...s.statCard, borderTop: '3px solid #ffb020' }} className="dashboard-stat-card">
|
||||
|
||||
{/* At Risk */}
|
||||
<div
|
||||
style={cardStyle(FILTER_AT_RISK, { borderTop: '3px solid #ffb020' })}
|
||||
className="dashboard-stat-card"
|
||||
onClick={() => handleBadgeClick(FILTER_AT_RISK)}
|
||||
title={`Click to filter: at risk (≤${AT_RISK_THRESHOLD} pts to next tier)`}
|
||||
>
|
||||
<div style={{ ...s.statNum, color: '#ffdf8a' }} className="stat-num">{atRiskCount}</div>
|
||||
<div style={s.statLbl} className="stat-lbl">At Risk (≤{AT_RISK_THRESHOLD} pts to next tier)</div>
|
||||
{activeFilter === FILTER_AT_RISK && <div style={s.filterBadge}>▼ Filtered</div>}
|
||||
</div>
|
||||
<div style={{ ...s.statCard, borderTop: '3px solid #c0392b' }} className="dashboard-stat-card">
|
||||
|
||||
{/* Highest Score — display only, no filter */}
|
||||
<div
|
||||
style={{ ...s.statCard, borderTop: '3px solid #c0392b', cursor: 'default' }}
|
||||
className="dashboard-stat-card"
|
||||
>
|
||||
<div style={{ ...s.statNum, color: '#ff8a80' }} className="stat-num">{maxPoints}</div>
|
||||
<div style={s.statLbl} className="stat-lbl">Highest Active Score</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user