diff --git a/app-frontend/employer-panel/src/pages/EmployerDashboard.css b/app-frontend/employer-panel/src/pages/EmployerDashboard.css
index 14ac5e1ca..3a5bf5c78 100644
--- a/app-frontend/employer-panel/src/pages/EmployerDashboard.css
+++ b/app-frontend/employer-panel/src/pages/EmployerDashboard.css
@@ -1,52 +1,68 @@
:root {
- --ss-blue-900:#0a2b66;
- --ss-blue-800:#0f3a8a;
- --ss-blue-700:#12499d;
- --ss-panel:#e7e9ee;
- --ss-card:#ffffff;
- --ss-text:#1a1a1a;
- --ss-muted:#667085;
- --ss-border:#d8dbe2;
- --ss-green:#18a058;
- --ss-orange:#ff9f2e;
- --ss-red:#e14b4b;
- --radius-lg:16px;
- --radius-xl:22px;
- --shadow:0 6px 14px rgba(20,31,71,.12);
- --shadow-soft:0 3px 8px rgba(20,31,71,.08);
+ --ss-blue-900: #0a2b66;
+ --ss-blue-800: #0f3a8a;
+ --ss-blue-700: #12499d;
+ --ss-panel: #e7e9ee;
+ --ss-card: #ffffff;
+ --ss-text: #1a1a1a;
+ --ss-muted: #667085;
+ --ss-border: #d8dbe2;
+ --ss-green: #18a058;
+ --ss-orange: #ff9f2e;
+ --ss-red: #e14b4b;
+ --radius-lg: 16px;
+ --radius-xl: 22px;
+ --shadow: 0 6px 14px rgba(20, 31, 71, 0.12);
+ --shadow-soft: 0 3px 8px rgba(20, 31, 71, 0.08);
}
body {
- margin:0;
- font:15px/1.5 "Segoe UI",Roboto,Arial,sans-serif;
- color:var(--ss-text);
+ margin: 0;
+ font: 15px/1.5 "Segoe UI", Roboto, Arial, sans-serif;
+ color: var(--ss-text);
}
/* -------- Main -------- */
-.ss-main {padding:34px 36px;}
+.ss-main {
+ padding: 34px 36px;
+}
+
.ss-h1 {
- font-size:30px;
- font-weight:700;
- margin:0 0 16px;
+ font-size: 30px;
+ font-weight: 700;
+ margin: 0 0 16px;
+}
+
+.ss-h1--spaced {
+ margin-top: 28px;
}
-.ss-h1--spaced {margin-top:28px;}
/* -------- Overview Panel -------- */
-.ss-overview {display:flex;align-items:center;}
+.ss-overview {
+ display: flex;
+ align-items: center;
+}
+
.ss-panel {
- flex:1;
- background:var(--ss-panel);
- border-radius:var(--radius-xl);
- padding:18px;
- min-height:420px; /* bigger for 2 rows */
- overflow:hidden;
+ flex: 1;
+ background: var(--ss-panel);
+ border-radius: var(--radius-xl);
+ padding: 18px;
+ min-height: 420px;
+ overflow: hidden;
+}
+
+.ss-panel__top {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+ margin-bottom: 12px;
}
-.ss-panel__top {display:flex;justify-content:flex-end;gap:10px;margin-bottom:12px;}
/* Controls row ABOVE grey grid */
.ss-controls {
display: flex;
- justify-content: flex-end; /* push everything to the right */
+ justify-content: flex-end;
align-items: center;
margin-bottom: 14px;
}
@@ -54,147 +70,160 @@ body {
.ss-controls-right {
display: flex;
align-items: center;
- gap: 10px; /* space between button and toggle */
+ gap: 10px;
}
.ss-primary--wide {
- display: inline-flex; /* flex keeps icon + text aligned */
- align-items: center; /* vertical centering */
- gap: 6px; /* space between icon and text */
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
padding: 8px 20px;
font-size: 15px;
- white-space: nowrap;
+ white-space: nowrap;
}
.ss-plus {
width: 16px;
height: 16px;
- flex-shrink: 0;
+ flex-shrink: 0;
}
-/* Grey grid panel*/
-.ss-panel {
- flex:1;
- background:var(--ss-panel);
- border-radius:var(--radius-xl);
- padding:18px;
- min-height:420px;
- overflow:hidden;
+.ss-primary {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ background: var(--ss-blue-800);
+ color: #fff;
+ padding: 6px 14px;
+ border: none;
+ border-radius: 999px;
+ font-weight: 600;
+ cursor: pointer;
}
+.ss-primary:hover {
+ background: var(--ss-blue-700);
+}
-
-.ss-primary {
- display:inline-flex;align-items:center;gap:6px;
- background:var(--ss-blue-800);color:#fff;
- padding:6px 14px;
- border:none;border-radius:999px;
- font-weight:600;
- cursor:pointer;
-}
-.ss-primary:hover{background:var(--ss-blue-700)}
.ss-viewtoggle {
- display:flex;gap:4px;background:#141b2e;border-radius:999px;padding:4px;
+ display: flex;
+ gap: 4px;
+ background: #141b2e;
+ border-radius: 999px;
+ padding: 4px;
}
+
.ss-viewtoggle__btn {
- width:30px;height:30px;border:none;border-radius:50%;
- background:transparent;color:#cfd6ea;cursor:pointer;
- display:grid;place-items:center;
+ width: 30px;
+ height: 30px;
+ border: none;
+ border-radius: 50%;
+ background: transparent;
+ color: #cfd6ea;
+ cursor: pointer;
+ display: grid;
+ place-items: center;
+}
+
+.ss-viewtoggle__btn.is-active {
+ background: #fff;
+ color: #141b2e;
+}
+
+.ss-viewtoggle__btn svg {
+ width: 16px;
+ height: 16px;
+}
+
+.ss-shifts {
+ overflow: auto;
+ scrollbar-width: none;
+}
+
+.ss-shifts::-webkit-scrollbar {
+ display: none;
}
-.ss-viewtoggle__btn.is-active {background:#fff;color:#141b2e;}
-.ss-viewtoggle__btn svg{width:16px;height:16px;}
-.ss-shifts {overflow:auto;scrollbar-width:none;}
-.ss-shifts::-webkit-scrollbar{display:none;}
.ss-shifts--grid {
- display:grid;
- grid-template-columns:repeat(auto-fill, minmax(280px,1fr));
- gap:16px;
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: 16px;
}
+
.ss-card {
- background:#fff;
- border-radius:14px;
- padding:14px;
- box-shadow:var(--shadow-soft);
- display:flex;flex-direction:column;gap:8px;
+ background: #fff;
+ border-radius: 14px;
+ padding: 14px;
+ box-shadow: var(--shadow-soft);
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
}
-/* Move status to the left in card view */
+
.ss-card .ss-status {
- text-align: left; /* aligns text to the left */
- margin-top: 0; /* optional, remove any top spacing */
+ text-align: left;
+ margin-top: 0;
}
-.ss-card__head{display:flex;justify-content:space-between;font-weight:600;}
-.ss-role{font-weight:700;font-size:16px;}
-.ss-rate{color:var(--ss-green);font-weight:700;}
-.ss-meta{color:#6a7280;font-size:14px;}
-.ss-status--confirmed{color:var(--ss-green);}
-.ss-status--pending{color:var(--ss-orange);}
-.ss-status--rejected{color:var(--ss-red);}
-.ss-when{margin-top:auto;display:flex;gap:14px;font-size:13px;color:#555;}
-.ss-ico{width:14px;height:14px;color:#666;}
+.ss-card__head {
+ display: flex;
+ justify-content: space-between;
+ font-weight: 600;
+}
-.ss-shifts--list {display:flex;flex-direction:column;gap:10px;}
-.ss-row {
- background:#fff;border-radius:999px;padding:12px 16px;
- display:grid;grid-template-columns: 240px 120px 1fr 200px;gap:12px;
- box-shadow:var(--shadow-soft);
+.ss-role {
+ font-weight: 700;
+ font-size: 16px;
}
-.ss-chip{background:#eef0f6;border-radius:999px;padding:6px 12px;font-weight:700;width:max-content;}
-.ss-row__rate{color:var(--ss-green);font-weight:700;}
-.ss-row__when{display:flex;gap:14px;}
-.ss-row__status{font-weight:700;}
-/* Arrows */
-.ss-arrow {
- border:none;width:38px;height:38px;border-radius:50%;
- background:#f1f3f7;color:#223357;font-size:24px;
- display:grid;place-items:center;cursor:pointer;
+.ss-rate {
+ color: var(--ss-green);
+ font-weight: 700;
}
-.ss-arrow:hover{background:#e5e8f2;}
-/* Stars */
-.star{width:16px;height:16px;fill:#c7d0ea;}
-.star.filled{fill:#2453ff;}
+.ss-meta {
+ color: #6a7280;
+ font-size: 14px;
+}
-/* -------- Reviews -------- */
-.ss-reviews{display:flex;align-items:center;margin-top:6px;}
-.ss-reviews__track{flex:1;display:flex;gap:16px;overflow:auto;}
-.ss-reviewcard{background:#fff;border-radius:16px;padding:16px;box-shadow:var(--shadow);min-width:260px;flex-shrink:0;}
-.ss-reviewcard__top{display:flex;align-items:center;gap:12px;margin-bottom:10px;}
-.ss-review__name{font-weight:700;font-size:16px;}
-.ss-review__role{color:#666;font-size:14px;}
-.ss-review__stars{display:flex;gap:4px;margin-bottom:12px;}
-.ss-secondary {
- background:var(--ss-blue-800);color:#fff;border:none;
- width:100%;padding:8px;border-radius:999px;font-weight:600;cursor:pointer;
+.ss-status--confirmed {
+ color: var(--ss-green);
}
-.ss-secondary:hover{background:var(--ss-blue-700);}
-.ss-avatar--lg{width:44px;height:44px;background:#f0f2f7;border-radius:50%;display:grid;place-items:center;}
-/* -------- Reviews -------- */
-.ss-reviews {
- display: flex;
- align-items: stretch; /* cards stretch vertically too */
- margin-top: 16px;
+
+.ss-status--pending {
+ color: var(--ss-orange);
}
-.ss-reviews__track {
- flex: 1;
- display: grid; /* grid instead of flex for even layout */
- grid-template-columns: repeat(3, 1fr); /* 3 equal columns */
- gap: 16px;
- overflow: hidden; /* no more horizontal scroll */
+.ss-status--rejected {
+ color: var(--ss-red);
}
-.ss-reviewcard {
- background: #fff;
- border-radius: 16px;
- padding: 16px;
- box-shadow: var(--shadow);
- height: 100%; /* stretch evenly */
+.ss-status--approved {
+ color: var(--ss-green);
+}
+
+.ss-status--completed {
+ color: var(--ss-blue-700);
+}
+
+.ss-status--resolved {
+ color: #2e7d32;
+}
+
+.ss-when {
+ margin-top: auto;
+ display: flex;
+ gap: 14px;
+ font-size: 13px;
+ color: #555;
+}
+
+.ss-ico {
+ width: 14px;
+ height: 14px;
+ color: #666;
}
-/* -------- LIST VIEW -------- */
.ss-shifts--list {
display: flex;
flex-direction: column;
@@ -203,7 +232,7 @@ body {
.ss-row {
display: grid;
- grid-template-columns: 1.2fr 2fr 1fr 1.2fr 1.5fr 1.5fr; /* equal spacing across row */
+ grid-template-columns: 1.2fr 2fr 1fr 1.2fr 1.5fr 1.5fr;
align-items: center;
background: #fff;
border-radius: 10px;
@@ -217,23 +246,13 @@ body {
padding: 0 6px;
}
-.ss-role {
- font-weight: 700;
- color: #222;
-}
-
.ss-company {
font-size: 14px;
color: #444;
}
-.ss-rate {
- font-weight: 700;
- color: var(--ss-green);
- text-align: center;
-}
-
-.ss-date, .ss-time {
+.ss-date,
+.ss-time {
font-size: 14px;
color: #333;
text-align: center;
@@ -245,42 +264,282 @@ body {
text-align: right;
}
-.ss-status--confirmed { color: var(--ss-green); }
-.ss-status--pending { color: var(--ss-orange); }
-.ss-status--rejected { color: var(--ss-red); }
+.ss-row .ss-incident-id {
+ color: #000;
+ font-weight: 700;
+}
+
+/* Arrows */
+.ss-arrow {
+ border: none;
+ width: 38px;
+ height: 38px;
+ border-radius: 50%;
+ background: #f1f3f7;
+ color: #223357;
+ font-size: 24px;
+ display: grid;
+ place-items: center;
+ cursor: pointer;
+}
+
+.ss-arrow:hover {
+ background: #e5e8f2;
+}
+
+/* Stars */
+.star {
+ width: 16px;
+ height: 16px;
+ fill: #c7d0ea;
+}
+
+.star.filled {
+ fill: #2453ff;
+}
+
+/* -------- Reviews -------- */
+.ss-reviews {
+ display: flex;
+ align-items: stretch;
+ margin-top: 16px;
+}
+
+.ss-reviews__track {
+ flex: 1;
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 16px;
+ overflow: hidden;
+}
+
+.ss-reviewcard {
+ background: #fff;
+ border-radius: 16px;
+ padding: 16px;
+ box-shadow: var(--shadow);
+ height: 100%;
+}
+
+.ss-reviewcard__top {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 10px;
+}
+
+.ss-review__name {
+ font-weight: 700;
+ font-size: 16px;
+}
+
+.ss-review__role {
+ color: #666;
+ font-size: 14px;
+}
+
+.ss-review__stars {
+ display: flex;
+ gap: 4px;
+ margin-bottom: 12px;
+}
+
+.ss-secondary {
+ background: var(--ss-blue-800);
+ color: #fff;
+ border: none;
+ width: 100%;
+ padding: 8px;
+ border-radius: 999px;
+ font-weight: 600;
+ cursor: pointer;
+}
+
+.ss-secondary:hover {
+ background: var(--ss-blue-700);
+}
-/* status colors */
-.ss-status--confirmed { color: var(--ss-green); }
-.ss-status--pending { color: var(--ss-orange); }
-.ss-status--rejected { color: var(--ss-red); }
-.ss-status--completed { color: var(--ss-blue-700); }
+.ss-secondary:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+.ss-secondary--small {
+ width: 100px;
+}
+
+.ss-secondary--danger {
+ background: #fbe9e9;
+ color: var(--ss-red);
+}
+
+.ss-secondary--danger:hover {
+ background: #f6dede;
+}
+
+.ss-avatar--lg {
+ width: 44px;
+ height: 44px;
+ background: #f0f2f7;
+ border-radius: 50%;
+ display: grid;
+ place-items: center;
+}
/* Create Shift Card */
.ss-card--create {
- display:flex;
- flex-direction:column;
- justify-content:center;
- align-items:center;
- gap:10px;
- cursor:pointer;
- border:2px dashed #b8c2db;
- color:#4b5a7e;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+ cursor: pointer;
+ border: 2px dashed #b8c2db;
+ color: #4b5a7e;
transition: background 0.2s;
}
+
.ss-card--create:hover {
- background:#f3f5fa;
+ background: #f3f5fa;
}
+
.ss-card__createicon svg {
- width:28px;
- height:28px;
+ width: 28px;
+ height: 28px;
}
+
.ss-card__createtext {
- font-weight:600;
+ font-weight: 600;
}
-/* Incident Management */
+/* Priority Shifts */
+.ss-priority-topstats {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 14px;
+ margin-bottom: 16px;
+}
+
+.ss-priority-mini-card {
+ background: #fff;
+ border: 1px solid #e0e4eb;
+ border-radius: 12px;
+ padding: 14px 16px;
+ box-shadow: var(--shadow-soft);
+}
+
+.ss-priority-mini-card h3 {
+ margin: 0;
+ font-size: 24px;
+ color: var(--ss-blue-800);
+}
+
+.ss-priority-mini-card p {
+ margin: 4px 0 0;
+ font-size: 13px;
+ color: var(--ss-muted);
+ font-weight: 600;
+}
+
+.ss-priority-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.ss-priority-simple-row {
+ display: grid;
+ grid-template-columns: 1.5fr 1.2fr 0.9fr;
+ align-items: center;
+ gap: 16px;
+ background: #fff;
+ border-radius: 12px;
+ padding: 14px 18px;
+ border: 1px solid #e0e4eb;
+ box-shadow: var(--shadow-soft);
+}
+
+.ss-priority-left,
+.ss-priority-middle {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.ss-priority-right {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: 8px;
+}
+
+.ss-priority-pill {
+ display: inline-block;
+ padding: 6px 12px;
+ border-radius: 999px;
+ font-size: 12px;
+ font-weight: 700;
+}
+
+.ss-priority-pill--high {
+ background: #fdeaea;
+ color: var(--ss-red);
+}
+
+.ss-priority-pill--medium {
+ background: #fff3df;
+ color: var(--ss-orange);
+}
+
+.ss-priority-pill--low {
+ background: #eaf7ef;
+ color: var(--ss-green);
+}
+
+.ss-priority-guards {
+ font-size: 13px;
+ color: var(--ss-muted);
+ font-weight: 600;
+}
+
+/* Request Approval System */
+.ss-requests-list {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.ss-request-row {
+ display: grid;
+ grid-template-columns: 2fr 1fr 220px;
+ gap: 16px;
+ align-items: center;
+ background: #fff;
+ border-radius: 12px;
+ padding: 16px 18px;
+ border: 1px solid #e0e4eb;
+ box-shadow: var(--shadow-soft);
+}
+
+.ss-request-left,
+.ss-request-middle {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+.ss-request-reason {
+ font-size: 13px;
+ color: var(--ss-muted);
+}
+
+.ss-request-actions {
+ display: flex;
+ gap: 10px;
+ justify-content: flex-end;
+}
+
+/* Incident Management */
.ss-incident-toolbar {
display: grid;
grid-template-columns: 2fr repeat(3, minmax(140px, 1fr)) auto;
@@ -321,19 +580,13 @@ body {
min-height: 68px;
}
-.ss-row .ss-incident-id {
- color: #000;
- font-weight: 700;
-}
-
-.ss-status--resolved {
- color: #2e7d32;
-}
-
/* Modal Layout */
.create-shift-modal-backdrop {
position: fixed;
- top: 0; left: 0; right: 0; bottom: 0;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
background: rgba(10, 43, 102, 0.5);
display: grid;
place-items: center;
@@ -351,8 +604,14 @@ body {
}
@keyframes modalSlideUp {
- from { transform: translateY(20px); opacity: 0; }
- to { transform: translateY(0); opacity: 1; }
+ from {
+ transform: translateY(20px);
+ opacity: 0;
+ }
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
}
.create-shift-header {
@@ -442,10 +701,29 @@ body {
.ss-incident-toolbar {
grid-template-columns: 1fr 1fr;
}
+
+ .ss-request-row,
+ .ss-priority-simple-row {
+ grid-template-columns: 1fr;
+ }
+
+ .ss-request-actions,
+ .ss-priority-right {
+ justify-content: flex-start;
+ align-items: flex-start;
+ }
+
+ .ss-reviews__track {
+ grid-template-columns: 1fr;
+ }
}
@media (max-width: 640px) {
.ss-incident-toolbar {
grid-template-columns: 1fr;
}
+
+ .ss-priority-topstats {
+ grid-template-columns: 1fr;
+ }
}
\ No newline at end of file
diff --git a/app-frontend/employer-panel/src/pages/EmployerDashboard.js b/app-frontend/employer-panel/src/pages/EmployerDashboard.js
index c9bd2ba85..ce7bcd5c2 100644
--- a/app-frontend/employer-panel/src/pages/EmployerDashboard.js
+++ b/app-frontend/employer-panel/src/pages/EmployerDashboard.js
@@ -1,53 +1,59 @@
import React, { useMemo, useRef, useState, useEffect } from "react";
-import { useNavigate } from "react-router-dom";
+import { useNavigate } from "react-router-dom";
import "./EmployerDashboard.css";
/* --- icons --- */
const IconCalendar = (props) => (
);
+
const IconClock = (props) => (
);
+
const IconPlus = (props) => (
);
+
const IconGrid = (props) => (
);
+
const IconList = (props) => (
);
+
const IconUser = (props) => (
);
+
const Star = ({ filled }) => (
);
@@ -57,6 +63,25 @@ const severityRank = {
Low: 1,
};
+const formatLocation = (location) => {
+ if (!location) return "No location";
+ if (typeof location === "string") return location;
+
+ return [location.street, location.suburb, location.state, location.postcode]
+ .filter(Boolean)
+ .join(", ");
+};
+
+const formatShiftDate = (value) => {
+ if (!value) return "--";
+ if (typeof value !== "string") return String(value);
+
+ if (/^\d{2}-\d{2}-\d{4}$/.test(value)) return value;
+
+ const parsed = new Date(value);
+ return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString("en-GB");
+};
+
const parseIncidentDateTime = (incident) => {
const [day, month, year] = incident.date.split("-").map(Number);
const baseDate = new Date(year, month - 1, day);
@@ -76,35 +101,76 @@ const parseIncidentDateTime = (incident) => {
};
export default function EmployerDashboard() {
- const [view, setView] = useState("list"); // default list view
+ const [view, setView] = useState("list");
const overviewScroller = useRef(null);
const reviewScroller = useRef(null);
- const navigate = useNavigate();
+ const navigate = useNavigate();
+
const [shifts, setShifts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
- // States for Incident Management
const [selectedIncident, setSelectedIncident] = useState(null);
const [incidentDraft, setIncidentDraft] = useState({ severity: "Medium", comments: "" });
const [incidentQuery, setIncidentQuery] = useState("");
const [incidentStatusFilter, setIncidentStatusFilter] = useState("All");
const [incidentSeverityFilter, setIncidentSeverityFilter] = useState("All");
const [incidentSort, setIncidentSort] = useState("Newest");
+
+ const [requests, setRequests] = useState([
+ {
+ id: "REQ-1001",
+ type: "Shift Swap",
+ employee: "John Doe",
+ shift: "Crowd Control - Marvel Stadium",
+ requestDate: "28-04-2026",
+ reason: "Requested shift swap due to university exam.",
+ status: "Pending",
+ },
+ {
+ id: "REQ-1002",
+ type: "Leave",
+ employee: "Amy Huggins",
+ shift: "Shopping Centre Security - Chadstone",
+ requestDate: "29-04-2026",
+ reason: "Medical appointment.",
+ status: "Pending",
+ },
+ {
+ id: "REQ-1003",
+ type: "Shift Swap",
+ employee: "Andrew Goddard",
+ shift: "Event Security - Rod Laver Arena",
+ requestDate: "30-04-2026",
+ reason: "Requested swap with another available guard.",
+ status: "Approved",
+ },
+ {
+ id: "REQ-1004",
+ type: "Leave",
+ employee: "Amy Huggins",
+ shift: "Shopping Centre Security - Chadstone",
+ requestDate: "01-05-2026",
+ reason: "Family commitment.",
+ status: "Rejected",
+ },
+ ]);
+
const [incidents, setIncidents] = useState([
- {
- id: "INC-9921",
- guard: "John Doe",
- shift: "Crowd Control - Marvel",
- date: "09-08-2025",
- time: "10:45 PM",
- status: "Pending",
+ {
+ id: "INC-9921",
+ guard: "John Doe",
+ shift: "Crowd Control - Marvel",
+ date: "09-08-2025",
+ time: "10:45 PM",
+ status: "Pending",
severity: "High",
- description: "A patron was found attempting to bypass security with restricted items. Incident was recorded and patron escorted out.",
-
- // Demo Image
- photos: ["https://images.unsplash.com/photo-1582139329536-e7284fece509?auto=format&fit=crop&w=300&q=80"],
- comments: ""
+ description:
+ "A patron was found attempting to bypass security with restricted items. Incident was recorded and patron escorted out.",
+ photos: [
+ "https://images.unsplash.com/photo-1582139329536-e7284fece509?auto=format&fit=crop&w=300&q=80",
+ ],
+ comments: "",
},
{
id: "INC-9920",
@@ -114,9 +180,10 @@ export default function EmployerDashboard() {
time: "08:15 PM",
status: "Resolved",
severity: "Medium",
- description: "A disagreement between attendees escalated near Gate 2. Security separated both parties and incident was de-escalated without injury.",
+ description:
+ "A disagreement between attendees escalated near Gate 2. Security separated both parties and incident was de-escalated without injury.",
photos: [],
- comments: "Resolved on site, no further action required."
+ comments: "Resolved on site, no further action required.",
},
{
id: "INC-9919",
@@ -126,73 +193,159 @@ export default function EmployerDashboard() {
time: "03:05 PM",
status: "Pending",
severity: "Low",
- description: "Minor slip hazard reported in food court area. Zone was isolated and cleaning team notified.",
- photos: ["https://images.unsplash.com/photo-1517292987719-0369a794ec0f?auto=format&fit=crop&w=300&q=80"],
- comments: ""
- }
+ description:
+ "Minor slip hazard reported in food court area. Zone was isolated and cleaning team notified.",
+ photos: [
+ "https://images.unsplash.com/photo-1517292987719-0369a794ec0f?auto=format&fit=crop&w=300&q=80",
+ ],
+ comments: "",
+ },
]);
- // Fetch shifts for the logged-in employer
-useEffect(() => {
- const fetchShifts = async () => {
- try {
- const token = localStorage.getItem("token");
+ useEffect(() => {
+ const fetchShifts = async () => {
+ try {
+ const token = localStorage.getItem("token");
- const response = await fetch(`${process.env.REACT_APP_API_BASE_URL}/shifts/myshifts`, {
- headers: {
- Authorization: `Bearer ${token}`,
- },
- });
+ const response = await fetch(`${process.env.REACT_APP_API_BASE_URL}/shifts/myshifts`, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
- console.log("fetch url:", `${process.env.REACT_APP_API_BASE_URL}/shifts/myshifts`);
- console.log("response status:", response.status);
- console.log("content-type:", response.headers.get("content-type"));
- console.log("response url:", response.url);
+ const data = await response.json();
- const data = await response.json();
- console.log("shift response:", data);
+ if (!response.ok) {
+ throw new Error(data.message || "Failed to load shifts.");
+ }
- if (!response.ok) {
- throw new Error(data.message || "Failed to load shifts.");
+ const rawShifts = Array.isArray(data?.data) ? data.data : Array.isArray(data) ? data : [];
+
+ const normalizedShifts = rawShifts.map((shift, idx) => {
+ const rawStatus = shift.status;
+ const normalizedStatus =
+ typeof rawStatus === "object" && rawStatus !== null
+ ? {
+ text: rawStatus.text || "Pending",
+ tone: rawStatus.tone || "pending",
+ }
+ : {
+ text: rawStatus || "Pending",
+ tone:
+ String(rawStatus || "pending").toLowerCase().includes("confirm")
+ ? "confirmed"
+ : String(rawStatus || "pending").toLowerCase().includes("complete")
+ ? "completed"
+ : String(rawStatus || "pending").toLowerCase().includes("reject")
+ ? "rejected"
+ : "pending",
+ };
+
+ return {
+ id: shift._id || shift.id || idx,
+ title: shift.title || shift.role || "Shift",
+ location: formatLocation(shift.location || shift.venue),
+ date: formatShiftDate(shift.date || shift.shiftDate),
+ time:
+ shift.startTime && shift.endTime
+ ? `${shift.startTime} - ${shift.endTime}`
+ : shift.time || "--",
+ status: normalizedStatus,
+ payRate: shift.payRate ?? shift.rate ?? shift.hourlyRate ?? 0,
+ priority:
+ shift.priority || (idx % 3 === 0 ? "High" : idx % 3 === 1 ? "Medium" : "Low"),
+ assignedGuards: shift.assignedGuards ?? shift.guardsAssigned ?? 0,
+ };
+ });
+
+ setShifts(normalizedShifts);
+ } catch (err) {
+ setError(err.message || "Failed to load shifts.");
+
+ setShifts([
+ {
+ id: 1,
+ title: "Crowd Control",
+ location: "Marvel Stadium",
+ date: "22-03-2026",
+ time: "5:00 pm - 1:00 am",
+ status: { text: "Confirmed", tone: "confirmed" },
+ payRate: 55,
+ priority: "High",
+ assignedGuards: 2,
+ },
+ {
+ id: 2,
+ title: "Shopping Centre Security",
+ location: "Chadstone Shopping Centre",
+ date: "24-03-2026",
+ time: "1:00 pm - 9:00 pm",
+ status: { text: "Pending", tone: "pending" },
+ payRate: 75,
+ priority: "Medium",
+ assignedGuards: 1,
+ },
+ {
+ id: 3,
+ title: "Event Security",
+ location: "Rod Laver Arena",
+ date: "26-03-2026",
+ time: "2:00 pm - 10:00 pm",
+ status: { text: "Confirmed", tone: "confirmed" },
+ payRate: 65,
+ priority: "High",
+ assignedGuards: 3,
+ },
+ {
+ id: 4,
+ title: "Static Guarding",
+ location: "Corporate Office",
+ date: "27-03-2026",
+ time: "8:00 am - 4:00 pm",
+ status: { text: "Completed (Rated)", tone: "completed" },
+ payRate: 50,
+ priority: "Low",
+ assignedGuards: 1,
+ },
+ ]);
+ } finally {
+ setLoading(false);
}
+ };
+
+ fetchShifts();
+ }, []);
+
+ const reviews = useMemo(
+ () => [
+ { name: "John Smith", role: "Crowd Control", stars: 5 },
+ { name: "Andrew Goddard", role: "Crowd Control", stars: 4 },
+ { name: "Amy Huggins", role: "Crowd Control", stars: 4 },
+ ],
+ []
+ );
- setShifts(Array.isArray(data?.data) ? data.data : Array.isArray(data) ? data : []);
- } catch (err) {
- setError(err.message || "Failed to load shifts.");
- console.error(err);
- } finally {
- setLoading(false);
- }
- };
-
- fetchShifts();
-}, []);
+ const priorityOrder = { High: 0, Medium: 1, Low: 2 };
- const reviews = useMemo(() => [
- { name: "John Smith", role: "Crowd Control", stars: 5 },
- { name: "Andrew Goddard", role: "Crowd Control", stars: 4 },
- { name: "Amy Huggins", role: "Crowd Control", stars: 4 },
- ], []);
+ const priorityShifts = useMemo(() => {
+ return [...shifts].sort((a, b) => {
+ const aPriority = priorityOrder[a.priority] ?? 99;
+ const bPriority = priorityOrder[b.priority] ?? 99;
+ return aPriority - bPriority;
+ });
+ }, [shifts]);
- const scrollByAmount = (ref, amt) => {
- if (!ref.current) return;
- ref.current.scrollBy({ left: amt, behavior: "smooth" });
- };
+ const highPriorityCount = useMemo(
+ () => shifts.filter((shift) => shift.priority === "High").length,
+ [shifts]
+ );
- const updateIncident = (id, newStatus, newSeverity, newComments) => {
- setIncidents(prev => prev.map(inc =>
- inc.id === id ? { ...inc, status: newStatus, severity: newSeverity, comments: newComments } : inc
- ));
- setSelectedIncident(null);
- };
+ const totalAssignedGuards = useMemo(
+ () => shifts.reduce((total, shift) => total + (shift.assignedGuards || 0), 0),
+ [shifts]
+ );
- const openIncidentModal = (incident) => {
- setSelectedIncident(incident);
- setIncidentDraft({
- severity: incident.severity,
- comments: incident.comments || "",
- });
- };
+ const upcomingShiftCount = shifts.length;
const filteredIncidents = useMemo(() => {
const normalizedQuery = incidentQuery.trim().toLowerCase();
@@ -218,15 +371,12 @@ useEffect(() => {
if (incidentSort === "Newest") {
return parseIncidentDateTime(b) - parseIncidentDateTime(a);
}
-
if (incidentSort === "Oldest") {
return parseIncidentDateTime(a) - parseIncidentDateTime(b);
}
-
if (incidentSort === "Severity") {
return (severityRank[b.severity] || 0) - (severityRank[a.severity] || 0);
}
-
return 0;
});
}, [incidents, incidentQuery, incidentSeverityFilter, incidentSort, incidentStatusFilter]);
@@ -243,136 +393,222 @@ useEffect(() => {
);
}, [incidents]);
+ const scrollByAmount = (ref, amt) => {
+ if (!ref.current) return;
+ ref.current.scrollBy({ left: amt, behavior: "smooth" });
+ };
+
+ const updateIncident = (id, newStatus, newSeverity, newComments) => {
+ setIncidents((prev) =>
+ prev.map((inc) =>
+ inc.id === id ? { ...inc, status: newStatus, severity: newSeverity, comments: newComments } : inc
+ )
+ );
+ setSelectedIncident(null);
+ };
+
+ const openIncidentModal = (incident) => {
+ setSelectedIncident(incident);
+ setIncidentDraft({
+ severity: incident.severity,
+ comments: incident.comments || "",
+ });
+ };
+
+ const updateRequestStatus = (id, newStatus) => {
+ setRequests((prev) =>
+ prev.map((req) => (req.id === id ? { ...req, status: newStatus } : req))
+ );
+ };
+
return (
-
- {/* -------- Overview -------- */}
Overview
-
- {/* Controls ABOVE grey grid */}
+
-
-
- {/* Grey Grid */}
+
-
scrollByAmount(overviewScroller, -320)}
- >
+ scrollByAmount(overviewScroller, -320)}>
‹
-
+
-
-
-
{loading &&
Loading shifts...
}
{error &&
{error}
}
{!loading && !error && shifts.length === 0 &&
No shifts yet. Create your first shift.
}
-
- {shifts.map((s, idx) => {
- const displayLocation =
- typeof s.location === "string"
- ? s.location
- : s.location
- ? [s.location.street, s.location.suburb, s.location.state]
- .filter(Boolean)
- .join(", ")
- : "No location";
-
- const displayDate = s.date
- ? new Date(s.date).toLocaleDateString()
- : "--";
-
- const displayTime =
- s.startTime && s.endTime
- ? `${s.startTime} - ${s.endTime}`
- : s.time || "--";
-
- const displayStatus = s.status || "open";
- const displayRate = s.payRate ?? s.rate ?? 0;
- const displayTitle = s.title || s.role || "Shift";
-
- return view === "grid" ? (
-
+
+ {shifts.map((s, idx) =>
+ view === "grid" ? (
+
-
{displayTitle}
-
${displayRate} p/h
+
{s.title}
+
${s.payRate} p/h
-
-
{displayLocation}
-
-
- Status: {displayStatus}
+
+
{s.location}
+
+
+ Status: {s.status.text}
-
+
- {displayDate}
+ {s.date}
- {displayTime}
+ {s.time}
) : (
-
-
{displayTitle}
-
{displayLocation}
-
${displayRate} p/h
+
+
{s.title}
+
{s.location}
+
${s.payRate} p/h
- {displayDate}
+ {s.date}
- {displayTime}
+ {s.time}
-
- Status: {displayStatus}
+
+ Status: {s.status.text}
- );
- })}
+ )
+ )}
-
-
scrollByAmount(overviewScroller, 320)}
- >
+
+ scrollByAmount(overviewScroller, 320)}>
›
- {/* Incident Reports */}
+
Priority Shifts
+
+
+
+
+
{upcomingShiftCount}
+
Upcoming Shifts
+
+
+
{highPriorityCount}
+
High Priority
+
+
+
{totalAssignedGuards}
+
Guards Assigned
+
+
+
+
+ {priorityShifts.map((shift, idx) => (
+
+
+
{shift.title}
+
{shift.location}
+
+
+
+
+
+ {shift.date}
+
+
+
+ {shift.time}
+
+
+
+
+
+ {shift.priority}
+
+
{shift.assignedGuards} guards assigned
+
+
+ ))}
+
+
+
+
+
Shift Swap / Leave Requests
+
+
+
+ {requests.map((req) => (
+
+
+
{req.type}
+
+ {req.employee} — {req.shift}
+
+
{req.reason}
+
+
+
+
+
+ {req.requestDate}
+
+
{req.status}
+
+
+
+ updateRequestStatus(req.id, "Approved")}
+ disabled={req.status === "Approved"}
+ >
+ Approve
+
+ updateRequestStatus(req.id, "Rejected")}
+ disabled={req.status === "Rejected"}
+ >
+ Reject
+
+
+
+ ))}
+
+
+
+
Incident Reports
{
Reset
+
{incidentSummary.total} Total
{incidentSummary.pending} Pending
{incidentSummary.resolved} Resolved
{filteredIncidents.length} Showing
+
@@ -425,17 +663,23 @@ useEffect(() => {
)}
{filteredIncidents.map((inc, i) => (
-
{inc.guard}
+
+ {inc.guard}
+
{inc.shift}
{inc.id}
{inc.date}
-
- {inc.status}
-
+
{inc.status}
- openIncidentModal(inc)}>Review
+ openIncidentModal(inc)}
+ >
+ Review
+
))}
@@ -444,39 +688,47 @@ useEffect(() => {
- {/* Reviews */}
Recent Review
-
scrollByAmount(reviewScroller, -300)}>‹
+
scrollByAmount(reviewScroller, -300)}>
+ ‹
+
{reviews.map((r, i) => (
- {[0,1,2,3,4].map((k) => )}
+ {[0, 1, 2, 3, 4].map((k) => (
+
+ ))}
View Review
))}
-
scrollByAmount(reviewScroller, 300)}>›
+
scrollByAmount(reviewScroller, 300)}>
+ ›
+
- {/* Incident Detail Modal */}
{selectedIncident && (
setSelectedIncident(null)}>
-
e.stopPropagation()} style={{ maxWidth: '700px' }}>
+
e.stopPropagation()} style={{ maxWidth: "700px" }}>
-
Incident Details ({selectedIncident.id})
-
+
+ Incident Details ({selectedIncident.id})
+
+
Recorded on: {selectedIncident.date} at {selectedIncident.time}
@@ -485,17 +737,19 @@ useEffect(() => {
-
+
-
{selectedIncident.guard}
+
+ {selectedIncident.guard}
+
-
-
+
+
{selectedIncident.description}
-
+
-
+
{(selectedIncident.photos || []).map((url, idx) => (

))}
{(!selectedIncident.photos || selectedIncident.photos.length === 0) && (
-
No evidence photos attached.
+
No evidence photos attached.
)}
@@ -527,31 +781,45 @@ useEffect(() => {
placeholder="Add internal notes..."
value={incidentDraft.comments}
onChange={(e) => setIncidentDraft((prev) => ({ ...prev, comments: e.target.value }))}
- style={{ border: '1px solid #ddd', borderRadius: '4px', padding: '10px' }}
+ style={{ border: "1px solid #ddd", borderRadius: "4px", padding: "10px" }}
rows={4}
/>
-
+
updateIncident(selectedIncident.id, "Resolved", incidentDraft.severity, incidentDraft.comments)}
+ onClick={() =>
+ updateIncident(
+ selectedIncident.id,
+ "Resolved",
+ incidentDraft.severity,
+ incidentDraft.comments
+ )
+ }
>
Mark as Resolved
updateIncident(selectedIncident.id, "Pending", incidentDraft.severity, incidentDraft.comments)}
+ onClick={() =>
+ updateIncident(
+ selectedIncident.id,
+ "Pending",
+ incidentDraft.severity,
+ incidentDraft.comments
+ )
+ }
>
Save as Pending
- setSelectedIncident(null)}>Close
+ setSelectedIncident(null)}>
+ Close
+
)}
-
-
);
-}
+}
\ No newline at end of file