Add mobile-optimized Dashboard component with card layout
This commit is contained in:
157
client/src/components/DashboardMobile.jsx
Normal file
157
client/src/components/DashboardMobile.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user