Update Dashboard with responsive mobile/desktop layouts
This commit is contained in:
@@ -3,6 +3,7 @@ import axios from 'axios';
|
|||||||
import CpasBadge, { getTier } from './CpasBadge';
|
import CpasBadge, { getTier } from './CpasBadge';
|
||||||
import EmployeeModal from './EmployeeModal';
|
import EmployeeModal from './EmployeeModal';
|
||||||
import AuditLog from './AuditLog';
|
import AuditLog from './AuditLog';
|
||||||
|
import DashboardMobile from './DashboardMobile';
|
||||||
|
|
||||||
const AT_RISK_THRESHOLD = 2;
|
const AT_RISK_THRESHOLD = 2;
|
||||||
|
|
||||||
@@ -28,13 +29,26 @@ function isAtRisk(points) {
|
|||||||
return boundary !== null && (boundary - points) <= AT_RISK_THRESHOLD;
|
return boundary !== null && (boundary - points) <= AT_RISK_THRESHOLD;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Media query hook
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
const s = {
|
const s = {
|
||||||
wrap: { padding: '32px 40px', color: '#f8f9fa' },
|
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: '#f8f9fa' },
|
title: { fontSize: '24px', fontWeight: 700, color: '#f8f9fa' },
|
||||||
subtitle: { fontSize: '13px', color: '#b5b5c0', 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: '#181924', border: '1px solid #30313f', borderRadius: '8px', padding: '16px', textAlign: 'center' },
|
statCard: { flex: '1', minWidth: '140px', background: '#181924', border: '1px solid #303136', borderRadius: '8px', padding: '16px', textAlign: 'center' },
|
||||||
statNum: { fontSize: '28px', fontWeight: 800, color: '#f8f9fa' },
|
statNum: { fontSize: '28px', fontWeight: 800, color: '#f8f9fa' },
|
||||||
statLbl: { fontSize: '11px', color: '#b5b5c0', marginTop: '4px' },
|
statLbl: { fontSize: '11px', color: '#b5b5c0', marginTop: '4px' },
|
||||||
search: { padding: '10px 14px', border: '1px solid #333544', borderRadius: '6px', fontSize: '14px', width: '260px', background: '#050608', color: '#f8f9fa' },
|
search: { padding: '10px 14px', border: '1px solid #333544', borderRadius: '6px', fontSize: '14px', width: '260px', background: '#050608', color: '#f8f9fa' },
|
||||||
@@ -49,6 +63,55 @@ const s = {
|
|||||||
auditBtn: { padding: '9px 18px', background: 'none', color: '#9ca0b8', border: '1px solid #2a2b3a', 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() {
|
export default function Dashboard() {
|
||||||
const [employees, setEmployees] = useState([]);
|
const [employees, setEmployees] = useState([]);
|
||||||
const [filtered, setFiltered] = useState([]);
|
const [filtered, setFiltered] = useState([]);
|
||||||
@@ -56,6 +119,7 @@ export default function Dashboard() {
|
|||||||
const [selectedId, setSelectedId] = useState(null);
|
const [selectedId, setSelectedId] = useState(null);
|
||||||
const [showAudit, setShowAudit] = useState(false);
|
const [showAudit, setShowAudit] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const isMobile = useMediaQuery('(max-width: 768px)');
|
||||||
|
|
||||||
const load = useCallback(() => {
|
const load = useCallback(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -82,49 +146,53 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={s.wrap}>
|
<style>{mobileStyles}</style>
|
||||||
<div style={s.header}>
|
<div style={s.wrap} className="dashboard-wrap">
|
||||||
|
<div style={s.header} className="dashboard-header">
|
||||||
<div>
|
<div>
|
||||||
<div style={s.title}>Company Dashboard</div>
|
<div style={s.title} className="dashboard-title">Company Dashboard</div>
|
||||||
<div style={s.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</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={s.toolbarRight}>
|
<div style={s.toolbarRight} className="toolbar-right">
|
||||||
<input
|
<input
|
||||||
style={s.search}
|
style={s.search}
|
||||||
|
className="search-input"
|
||||||
placeholder="Search name, dept, supervisor…"
|
placeholder="Search name, dept, supervisor…"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={e => setSearch(e.target.value)}
|
onChange={e => setSearch(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<button style={s.auditBtn} onClick={() => setShowAudit(true)}>📋 Audit Log</button>
|
<button style={s.auditBtn} className="toolbar-btn" onClick={() => setShowAudit(true)}>📋 Audit Log</button>
|
||||||
<button style={s.refreshBtn} onClick={load}>↻ Refresh</button>
|
<button style={s.refreshBtn} className="toolbar-btn" onClick={load}>↻ Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={s.statsRow}>
|
<div style={s.statsRow} className="dashboard-stats">
|
||||||
<div style={s.statCard}>
|
<div style={s.statCard} className="dashboard-stat-card">
|
||||||
<div style={s.statNum}>{employees.length}</div>
|
<div style={s.statNum} className="stat-num">{employees.length}</div>
|
||||||
<div style={s.statLbl}>Total Employees</div>
|
<div style={s.statLbl} className="stat-lbl">Total Employees</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ ...s.statCard, borderTop: '3px solid #28a745' }}>
|
<div style={{ ...s.statCard, borderTop: '3px solid #28a745' }} className="dashboard-stat-card">
|
||||||
<div style={{ ...s.statNum, color: '#6ee7b7' }}>{cleanCount}</div>
|
<div style={{ ...s.statNum, color: '#6ee7b7' }} className="stat-num">{cleanCount}</div>
|
||||||
<div style={s.statLbl}>Elite Standing (0 pts)</div>
|
<div style={s.statLbl} className="stat-lbl">Elite Standing (0 pts)</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ ...s.statCard, borderTop: '3px solid #d4af37' }}>
|
<div style={{ ...s.statCard, borderTop: '3px solid #d4af37' }} className="dashboard-stat-card">
|
||||||
<div style={{ ...s.statNum, color: '#ffd666' }}>{activeCount}</div>
|
<div style={{ ...s.statNum, color: '#ffd666' }} className="stat-num">{activeCount}</div>
|
||||||
<div style={s.statLbl}>With Active Points</div>
|
<div style={s.statLbl} className="stat-lbl">With Active Points</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ ...s.statCard, borderTop: '3px solid #ffb020' }}>
|
<div style={{ ...s.statCard, borderTop: '3px solid #ffb020' }} className="dashboard-stat-card">
|
||||||
<div style={{ ...s.statNum, color: '#ffdf8a' }}>{atRiskCount}</div>
|
<div style={{ ...s.statNum, color: '#ffdf8a' }} className="stat-num">{atRiskCount}</div>
|
||||||
<div style={s.statLbl}>At Risk (≤{AT_RISK_THRESHOLD} pts to next tier)</div>
|
<div style={s.statLbl} className="stat-lbl">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' }} className="dashboard-stat-card">
|
||||||
<div style={{ ...s.statNum, color: '#ff8a80' }}>{maxPoints}</div>
|
<div style={{ ...s.statNum, color: '#ff8a80' }} className="stat-num">{maxPoints}</div>
|
||||||
<div style={s.statLbl}>Highest Active Score</div>
|
<div style={s.statLbl} className="stat-lbl">Highest Active Score</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p style={{ color: '#77798a', textAlign: 'center', padding: '40px' }}>Loading…</p>
|
<p style={{ color: '#77798a', textAlign: 'center', padding: '40px' }}>Loading…</p>
|
||||||
|
) : isMobile ? (
|
||||||
|
<DashboardMobile employees={filtered} onEmployeeClick={setSelectedId} />
|
||||||
) : (
|
) : (
|
||||||
<table style={s.table}>
|
<table style={s.table}>
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
Reference in New Issue
Block a user