p4-hotfixes #10

Merged
jason merged 4 commits from p4-hotfixes into master 2026-03-06 14:13:15 -06:00
Showing only changes of commit 2383e3cc94 - Show all commits

View File

@@ -3,7 +3,7 @@ import axios from 'axios';
import CpasBadge, { getTier } from './CpasBadge'; import CpasBadge, { getTier } from './CpasBadge';
import EmployeeModal from './EmployeeModal'; import EmployeeModal from './EmployeeModal';
const AT_RISK_THRESHOLD = 2; // points within next tier boundary const AT_RISK_THRESHOLD = 2;
const TIERS = [ const TIERS = [
{ min: 0, max: 4 }, { min: 0, max: 4 },
@@ -17,8 +17,7 @@ const TIERS = [
function nextTierBoundary(points) { function nextTierBoundary(points) {
for (const t of TIERS) { for (const t of TIERS) {
if (points >= t.min && points <= t.max && t.max < 999) if (points >= t.min && points <= t.max && t.max < 999) return t.max + 1;
return t.max + 1;
} }
return null; return null;
} }
@@ -29,29 +28,29 @@ function isAtRisk(points) {
} }
const s = { const s = {
wrap: { padding: '40px' }, wrap: { padding: '32px 40px', color: '#f8f9fa' },
header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px', flexWrap: 'wrap', gap: '12px' }, header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px', flexWrap: 'wrap', gap: '12px' },
title: { fontSize: '24px', fontWeight: 700, color: '#2c3e50' }, title: { fontSize: '24px', fontWeight: 700, color: '#f8f9fa' },
subtitle: { fontSize: '13px', color: '#888', marginTop: '3px' }, subtitle: { fontSize: '13px', color: '#b5b5c0', marginTop: '3px' },
statsRow: { display: 'flex', gap: '16px', flexWrap: 'wrap', marginBottom: '28px' }, statsRow: { display: 'flex', gap: '16px', flexWrap: 'wrap', marginBottom: '28px' },
statCard: { flex: '1', minWidth: '140px', background: '#f8f9fa', border: '1px solid #dee2e6', borderRadius: '8px', padding: '16px', textAlign: 'center' }, statCard: { flex: '1', minWidth: '140px', background: '#181924', border: '1px solid #30313f', borderRadius: '8px', padding: '16px', textAlign: 'center' },
statNum: { fontSize: '28px', fontWeight: 800, color: '#2c3e50' }, statNum: { fontSize: '28px', fontWeight: 800, color: '#f8f9fa' },
statLbl: { fontSize: '11px', color: '#888', marginTop: '4px' }, statLbl: { fontSize: '11px', color: '#b5b5c0', marginTop: '4px' },
search: { padding: '10px 14px', border: '1px solid #ddd', borderRadius: '6px', fontSize: '14px', width: '260px' }, search: { padding: '10px 14px', border: '1px solid #333544', borderRadius: '6px', fontSize: '14px', width: '260px', background: '#050608', color: '#f8f9fa' },
table: { width: '100%', borderCollapse: 'collapse', background: 'white', borderRadius: '8px', overflow: 'hidden', boxShadow: '0 1px 4px rgba(0,0,0,0.08)' }, 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: '#34495e', color: 'white', padding: '10px 14px', textAlign: 'left', fontSize: '12px', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.5px' }, 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 #f0f0f0', fontSize: '13px', verticalAlign: 'middle' }, td: { padding: '11px 14px', borderBottom: '1px solid #1c1d29', fontSize: '13px', verticalAlign: 'middle', color: '#f8f9fa' },
nameBtn: { background: 'none', border: 'none', cursor: 'pointer', fontWeight: 600, color: '#667eea', fontSize: '14px', padding: 0, textDecoration: 'underline dotted' }, 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: '#fff3cd', color: '#856404', border: '1px solid #ffc107', verticalAlign: 'middle' }, 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: '#aaa', fontStyle: 'italic', fontSize: '12px' }, zeroRow: { color: '#77798a', fontStyle: 'italic', fontSize: '12px' },
refreshBtn:{ padding: '9px 18px', background: '#667eea', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer', fontWeight: 600, fontSize: '13px' }, refreshBtn:{ padding: '9px 18px', background: '#d4af37', color: '#000', border: 'none', borderRadius: '6px', cursor: 'pointer', fontWeight: 600, fontSize: '13px' },
}; };
export default function Dashboard() { export default function Dashboard() {
const [employees, setEmployees] = useState([]); const [employees, setEmployees] = useState([]);
const [filtered, setFiltered] = useState([]); const [filtered, setFiltered] = useState([]);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [selectedId, setSelectedId] = useState(null); const [selectedId,setSelectedId] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const load = useCallback(() => { const load = useCallback(() => {
@@ -90,33 +89,31 @@ export default function Dashboard() {
</div> </div>
</div> </div>
{/* ── Stat cards ───────────────────────────────────────── */}
<div style={s.statsRow}> <div style={s.statsRow}>
<div style={s.statCard}> <div style={s.statCard}>
<div style={s.statNum}>{employees.length}</div> <div style={s.statNum}>{employees.length}</div>
<div style={s.statLbl}>Total Employees</div> <div style={s.statLbl}>Total Employees</div>
</div> </div>
<div style={{ ...s.statCard, borderTop: '3px solid #28a745' }}> <div style={{ ...s.statCard, borderTop: '3px solid #28a745' }}>
<div style={{ ...s.statNum, color: '#28a745' }}>{cleanCount}</div> <div style={{ ...s.statNum, color: '#6ee7b7' }}>{cleanCount}</div>
<div style={s.statLbl}>Elite Standing (0 pts)</div> <div style={s.statLbl}>Elite Standing (0 pts)</div>
</div> </div>
<div style={{ ...s.statCard, borderTop: '3px solid #856404' }}> <div style={{ ...s.statCard, borderTop: '3px solid #d4af37' }}>
<div style={{ ...s.statNum, color: '#856404' }}>{activeCount}</div> <div style={{ ...s.statNum, color: '#ffd666' }}>{activeCount}</div>
<div style={s.statLbl}>With Active Points</div> <div style={s.statLbl}>With Active Points</div>
</div> </div>
<div style={{ ...s.statCard, borderTop: '3px solid #ffc107' }}> <div style={{ ...s.statCard, borderTop: '3px solid #ffb020' }}>
<div style={{ ...s.statNum, color: '#856404' }}>{atRiskCount}</div> <div style={{ ...s.statNum, color: '#ffdf8a' }}>{atRiskCount}</div>
<div style={s.statLbl}>At Risk ({AT_RISK_THRESHOLD} pts to next tier)</div> <div style={s.statLbl}>At Risk ({AT_RISK_THRESHOLD} pts to next tier)</div>
</div> </div>
<div style={{ ...s.statCard, borderTop: '3px solid #c0392b' }}> <div style={{ ...s.statCard, borderTop: '3px solid #c0392b' }}>
<div style={{ ...s.statNum, color: '#c0392b' }}>{maxPoints}</div> <div style={{ ...s.statNum, color: '#ff8a80' }}>{maxPoints}</div>
<div style={s.statLbl}>Highest Active Score</div> <div style={s.statLbl}>Highest Active Score</div>
</div> </div>
</div> </div>
{/* ── Scoreboard table ─────────────────────────────────── */}
{loading ? ( {loading ? (
<p style={{ color: '#aaa', textAlign: 'center', padding: '40px' }}>Loading</p> <p style={{ color: '#77798a', textAlign: 'center', padding: '40px' }}>Loading</p>
) : ( ) : (
<table style={s.table}> <table style={s.table}>
<thead> <thead>
@@ -139,8 +136,8 @@ export default function Dashboard() {
const tier = getTier(emp.active_points); const tier = getTier(emp.active_points);
const boundary = nextTierBoundary(emp.active_points); const boundary = nextTierBoundary(emp.active_points);
return ( return (
<tr key={emp.id} style={{ background: risk ? '#fffdf0' : i % 2 === 0 ? 'white' : '#fafafa' }}> <tr key={emp.id} style={{ background: risk ? '#181200' : i % 2 === 0 ? '#111217' : '#151622' }}>
<td style={{ ...s.td, color: '#aaa', fontSize: '12px' }}>{i + 1}</td> <td style={{ ...s.td, color: '#77798a', fontSize: '12px' }}>{i + 1}</td>
<td style={s.td}> <td style={s.td}>
<button style={s.nameBtn} onClick={() => setSelectedId(emp.id)}>{emp.name}</button> <button style={s.nameBtn} onClick={() => setSelectedId(emp.id)}>{emp.name}</button>
{risk && ( {risk && (
@@ -149,11 +146,11 @@ export default function Dashboard() {
</span> </span>
)} )}
</td> </td>
<td style={{ ...s.td, color: '#666' }}>{emp.department || '—'}</td> <td style={{ ...s.td, color: '#c0c2d6' }}>{emp.department || '—'}</td>
<td style={{ ...s.td, color: '#666' }}>{emp.supervisor || '—'}</td> <td style={{ ...s.td, color: '#c0c2d6' }}>{emp.supervisor || '—'}</td>
<td style={s.td}><CpasBadge points={emp.active_points} /></td> <td style={s.td}><CpasBadge points={emp.active_points} /></td>
<td style={{ ...s.td, fontWeight: 700, color: tier.color, fontSize: '16px' }}>{emp.active_points}</td> <td style={{ ...s.td, fontWeight: 700, color: tier.color, fontSize: '16px' }}>{emp.active_points}</td>
<td style={{ ...s.td, color: '#666' }}>{emp.violation_count}</td> <td style={{ ...s.td, color: '#c0c2d6' }}>{emp.violation_count}</td>
</tr> </tr>
); );
})} })}
@@ -161,7 +158,6 @@ export default function Dashboard() {
</table> </table>
)} )}
{/* ── Employee profile modal ───────────────────────────── */}
{selectedId && ( {selectedId && (
<EmployeeModal <EmployeeModal
employeeId={selectedId} employeeId={selectedId}