-
+
{tab === 'dashboard' ? : }
diff --git a/client/src/components/Dashboard.jsx b/client/src/components/Dashboard.jsx
index d333e7a..a27bb73 100755
--- a/client/src/components/Dashboard.jsx
+++ b/client/src/components/Dashboard.jsx
@@ -3,6 +3,7 @@ import axios from 'axios';
import CpasBadge, { getTier } from './CpasBadge';
import EmployeeModal from './EmployeeModal';
import AuditLog from './AuditLog';
+import DashboardMobile from './DashboardMobile';
const AT_RISK_THRESHOLD = 2;
@@ -28,13 +29,26 @@ function isAtRisk(points) {
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 = {
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 #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' },
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' },
@@ -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' },
};
+// 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() {
const [employees, setEmployees] = useState([]);
const [filtered, setFiltered] = useState([]);
@@ -56,6 +119,7 @@ export default function Dashboard() {
const [selectedId, setSelectedId] = useState(null);
const [showAudit, setShowAudit] = useState(false);
const [loading, setLoading] = useState(true);
+ const isMobile = useMediaQuery('(max-width: 768px)');
const load = useCallback(() => {
setLoading(true);
@@ -82,49 +146,53 @@ export default function Dashboard() {
return (
<>
-
-
+
+
+
-
Company Dashboard
-
Click any employee name to view their full profile
+
Company Dashboard
+
Click any employee name to view their full profile
-
-
-
-
{employees.length}
-
Total Employees
+
+
+
{employees.length}
+
Total Employees
-
-
{cleanCount}
-
Elite Standing (0 pts)
+
+
{cleanCount}
+
Elite Standing (0 pts)
-
-
{activeCount}
-
With Active Points
+
+
{activeCount}
+
With Active Points
-
-
{atRiskCount}
-
At Risk (β€{AT_RISK_THRESHOLD} pts to next tier)
+
+
{atRiskCount}
+
At Risk (β€{AT_RISK_THRESHOLD} pts to next tier)
-
-
{maxPoints}
-
Highest Active Score
+
+
{maxPoints}
+
Highest Active Score
{loading ? (
Loadingβ¦
+ ) : isMobile ? (
+
) : (
diff --git a/client/src/components/DashboardMobile.jsx b/client/src/components/DashboardMobile.jsx
new file mode 100644
index 0000000..cc7f2c1
--- /dev/null
+++ b/client/src/components/DashboardMobile.jsx
@@ -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 (
+
+ No employees found.
+
+ );
+ }
+
+ return (
+
+ {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 (
+
+
+ {risk && (
+
+ β {boundary - emp.active_points} pt{boundary - emp.active_points > 1 ? 's' : ''} to {getTier(boundary).label.split('β')[0].trim()}
+
+ )}
+
+
+ Tier / Standing
+
+
+
+
+ Active Points
+ {emp.active_points}
+
+
+
+ 90-Day Violations
+ {emp.violation_count}
+
+
+ {emp.department && (
+
+ Department
+ {emp.department}
+
+ )}
+
+ {emp.supervisor && (
+
+ Supervisor
+ {emp.supervisor}
+
+ )}
+
+ );
+ })}
+
+ );
+}
diff --git a/client/src/styles/mobile.css b/client/src/styles/mobile.css
new file mode 100644
index 0000000..89c02f4
--- /dev/null
+++ b/client/src/styles/mobile.css
@@ -0,0 +1,113 @@
+/* Mobile-Responsive Utilities for CPAS Tracker */
+/* Target: Standard phones 375px+ with graceful degradation */
+
+/* Base responsive utilities */
+@media (max-width: 768px) {
+ /* Hide scrollbars but keep functionality */
+ * {
+ -webkit-overflow-scrolling: touch;
+ }
+
+ /* Touch-friendly tap targets (min 44px) */
+ button, a, input, select {
+ min-height: 44px;
+ }
+
+ /* Improve form input sizing on mobile */
+ input, select, textarea {
+ font-size: 16px !important; /* Prevents iOS zoom on focus */
+ }
+}
+
+/* Tablet and below */
+@media (max-width: 1024px) {
+ .hide-tablet {
+ display: none !important;
+ }
+}
+
+/* Mobile portrait and landscape */
+@media (max-width: 768px) {
+ .hide-mobile {
+ display: none !important;
+ }
+
+ .mobile-full-width {
+ width: 100% !important;
+ }
+
+ .mobile-text-center {
+ text-align: center !important;
+ }
+
+ .mobile-no-padding {
+ padding: 0 !important;
+ }
+
+ .mobile-small-padding {
+ padding: 12px !important;
+ }
+
+ /* Stack flex containers vertically */
+ .mobile-stack {
+ flex-direction: column !important;
+ }
+
+ /* Allow horizontal scroll for tables */
+ .mobile-scroll-x {
+ overflow-x: auto !important;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ /* Card-based layout helpers */
+ .mobile-card {
+ display: block !important;
+ padding: 16px;
+ margin-bottom: 12px;
+ border-radius: 8px;
+ background: #181924;
+ border: 1px solid #2a2b3a;
+ }
+
+ .mobile-card-row {
+ display: flex;
+ justify-content: space-between;
+ padding: 8px 0;
+ border-bottom: 1px solid #1c1d29;
+ }
+
+ .mobile-card-row:last-child {
+ border-bottom: none;
+ }
+
+ .mobile-card-label {
+ font-weight: 600;
+ color: #9ca0b8;
+ font-size: 12px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ }
+
+ .mobile-card-value {
+ font-weight: 600;
+ color: #f8f9fa;
+ text-align: right;
+ }
+}
+
+/* Small mobile phones */
+@media (max-width: 480px) {
+ .hide-small-mobile {
+ display: none !important;
+ }
+}
+
+/* Utility for sticky positioning on mobile */
+@media (max-width: 768px) {
+ .mobile-sticky-top {
+ position: sticky;
+ top: 0;
+ z-index: 100;
+ background: #000000;
+ }
+}