Add mobile-optimized Dashboard component with card layout

This commit is contained in:
2026-03-08 22:05:04 -05:00
parent 74492142a1
commit 15a2b89350

View File

@@ -0,0 +1,157 @@
import React from 'react';
import CpasBadge, { getTier } from './CpasBadge';
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;
}
const s = {
card: {
background: '#181924',
border: '1px solid #2a2b3a',
borderRadius: '10px',
padding: '16px',
marginBottom: '12px',
boxShadow: '0 1px 4px rgba(0,0,0,0.4)',
},
cardAtRisk: {
background: '#181200',
border: '1px solid #d4af37',
},
row: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '8px 0',
borderBottom: '1px solid rgba(255,255,255,0.05)',
},
rowLast: {
borderBottom: 'none',
},
label: {
fontSize: '11px',
fontWeight: 600,
color: '#9ca0b8',
textTransform: 'uppercase',
letterSpacing: '0.5px',
},
value: {
fontSize: '14px',
fontWeight: 600,
color: '#f8f9fa',
textAlign: 'right',
},
name: {
fontSize: '16px',
fontWeight: 700,
color: '#d4af37',
marginBottom: '8px',
cursor: 'pointer',
textDecoration: 'underline dotted',
background: 'none',
border: 'none',
padding: 0,
textAlign: 'left',
width: '100%',
},
atRiskBadge: {
display: 'inline-block',
marginTop: '4px',
padding: '3px 8px',
borderRadius: '10px',
fontSize: '10px',
fontWeight: 700,
background: '#3b2e00',
color: '#ffd666',
border: '1px solid #d4af37',
},
points: {
fontSize: '28px',
fontWeight: 800,
textAlign: 'center',
margin: '8px 0',
},
};
export default function DashboardMobile({ employees, onEmployeeClick }) {
if (!employees || employees.length === 0) {
return (
<div style={{ padding: '20px', textAlign: 'center', color: '#77798a', fontStyle: 'italic' }}>
No employees found.
</div>
);
}
return (
<div style={{ padding: '12px' }}>
{employees.map((emp) => {
const risk = isAtRisk(emp.active_points);
const tier = getTier(emp.active_points);
const boundary = nextTierBoundary(emp.active_points);
const cardStyle = risk ? { ...s.card, ...s.cardAtRisk } : s.card;
return (
<div key={emp.id} style={cardStyle}>
<button style={s.name} onClick={() => onEmployeeClick(emp.id)}>
{emp.name}
</button>
{risk && (
<div style={s.atRiskBadge}>
{boundary - emp.active_points} pt{boundary - emp.active_points > 1 ? 's' : ''} to {getTier(boundary).label.split('—')[0].trim()}
</div>
)}
<div style={{ ...s.row, marginTop: '12px' }}>
<span style={s.label}>Tier / Standing</span>
<span style={s.value}><CpasBadge points={emp.active_points} /></span>
</div>
<div style={s.row}>
<span style={s.label}>Active Points</span>
<span style={{ ...s.points, color: tier.color }}>{emp.active_points}</span>
</div>
<div style={s.row}>
<span style={s.label}>90-Day Violations</span>
<span style={s.value}>{emp.violation_count}</span>
</div>
{emp.department && (
<div style={s.row}>
<span style={s.label}>Department</span>
<span style={{ ...s.value, color: '#c0c2d6' }}>{emp.department}</span>
</div>
)}
{emp.supervisor && (
<div style={{ ...s.row, ...s.rowLast }}>
<span style={s.label}>Supervisor</span>
<span style={{ ...s.value, color: '#c0c2d6' }}>{emp.supervisor}</span>
</div>
)}
</div>
);
})}
</div>
);
}