diff --git a/app-frontend/employer-panel/src/pages/EmployerDashboard.css b/app-frontend/employer-panel/src/pages/EmployerDashboard.css index 14ac5e1ca..555930414 100644 --- a/app-frontend/employer-panel/src/pages/EmployerDashboard.css +++ b/app-frontend/employer-panel/src/pages/EmployerDashboard.css @@ -1,285 +1,218 @@ :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-h1 { - font-size:30px; - font-weight:700; - margin:0 0 16px; +.ss-main { + padding: 34px 36px; } -.ss-h1--spaced {margin-top:28px;} -/* -------- Overview Panel -------- */ -.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; +.ss-h1 { + font-size: 30px; + font-weight: 700; + margin: 0 0 16px; } -.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 */ - align-items: center; - margin-bottom: 14px; +.ss-h1--spaced { + margin-top: 28px; } -.ss-controls-right { +.ss-panel__top { display: flex; - align-items: center; - gap: 10px; /* space between button and toggle */ + justify-content: flex-end; + gap: 10px; + margin-bottom: 12px; } -.ss-primary--wide { - display: inline-flex; /* flex keeps icon + text aligned */ - align-items: center; /* vertical centering */ - gap: 6px; /* space between icon and text */ - padding: 8px 20px; - font-size: 15px; - white-space: nowrap; +.ss-viewtoggle__btn svg { + width: 16px; + height: 16px; } .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: 8px 16px; + 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-shifts--grid { - 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; -} -/* 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 */ +.ss-viewtoggle__btn.is-active { + background: #fff; + color: #141b2e; } -.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-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-overview { + display: flex; + align-items: center; + gap: 12px; } -.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-arrow:hover{background:#e5e8f2;} - -/* Stars */ -.star{width:16px;height:16px;fill:#c7d0ea;} -.star.filled{fill:#2453ff;} - -/* -------- 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-panel { + flex: 1; + min-width: 0; + background: var(--ss-panel); + border-radius: var(--radius-xl); + padding: 18px; + min-height: 420px; + overflow: hidden; } -.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-shifts { + overflow: auto; + scrollbar-width: none; } -.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-shifts::-webkit-scrollbar { + display: none; } -.ss-reviewcard { - background: #fff; - border-radius: 16px; - padding: 16px; - box-shadow: var(--shadow); - height: 100%; /* stretch evenly */ +.ss-shifts--grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 16px; } -/* -------- LIST VIEW -------- */ -.ss-shifts--list { +.ss-shifts--list, +.ss-incidents-list, +.ss-priority-list { display: flex; flex-direction: column; gap: 10px; } -.ss-row { - display: grid; - grid-template-columns: 1.2fr 2fr 1fr 1.2fr 1.5fr 1.5fr; /* equal spacing across row */ - align-items: center; +.ss-card { background: #fff; - border-radius: 10px; - padding: 12px 18px; - font-size: 15px; + border-radius: 14px; + padding: 14px; box-shadow: var(--shadow-soft); - border: 1px solid #e0e4eb; + display: flex; + flex-direction: column; + gap: 8px; } -.ss-cell { - padding: 0 6px; +.ss-card .ss-status { + text-align: left; + margin-top: 0; } -.ss-role { - font-weight: 700; - color: #222; +.ss-card__head { + display: flex; + justify-content: space-between; + font-weight: 600; } -.ss-company { - font-size: 14px; - color: #444; +.ss-card__createicon { + display: flex; + align-items: center; + justify-content: center; } -.ss-rate { - font-weight: 700; - color: var(--ss-green); - text-align: center; +.ss-card--create { + 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-date, .ss-time { - font-size: 14px; - color: #333; - text-align: center; +.ss-card--create:hover { + background: #f3f5fa; } -.ss-status { - font-size: 14px; +.ss-card__createtext { font-weight: 600; - text-align: right; } -.ss-status--confirmed { color: var(--ss-green); } -.ss-status--pending { color: var(--ss-orange); } -.ss-status--rejected { color: var(--ss-red); } - -/* 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); } - - -/* 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; - transition: background 0.2s; +.ss-role { + font-weight: 700; + font-size: 16px; } -.ss-card--create:hover { - background:#f3f5fa; + +.ss-rate { + color: var(--ss-green); + font-weight: 700; } -.ss-card__createicon svg { - width:28px; - height:28px; + +.ss-meta { + color: #6a7280; + font-size: 14px; } -.ss-card__createtext { - font-weight:600; + +.ss-when { + margin-top: auto; + display: flex; + gap: 14px; + font-size: 13px; + color: #555; + flex-wrap: wrap; } -/* Incident Management */ +.ss-when__item, +.ss-date, +.ss-time { + display: inline-flex; + align-items: center; + gap: 6px; +} .ss-incident-toolbar { display: grid; @@ -321,19 +254,247 @@ body { min-height: 68px; } -.ss-row .ss-incident-id { - color: #000; - font-weight: 700; +.ss-row { + display: grid; + align-items: center; + gap: 12px; + background: #fff; + border-radius: 12px; + padding: 12px 18px; + font-size: 15px; + box-shadow: var(--shadow-soft); + border: 1px solid #e0e4eb; +} + +.ss-shifts--list .ss-row { + grid-template-columns: 1.2fr 2fr 1fr 1.2fr 1.5fr 1.5fr; +} + +.ss-row--incident { + grid-template-columns: 1.2fr 2fr 1fr 1.2fr 1fr 120px; +} + +.ss-col { + min-width: 0; +} + +.ss-company { + font-size: 14px; + color: #444; +} + +.ss-status { + font-size: 14px; + font-weight: 600; +} + +.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-status--resolved { color: #2e7d32; } -/* Modal Layout */ +.ss-incident-id { + color: #000; + font-weight: 700; +} + +.ss-incident-action { + display: flex; + justify-content: flex-end; +} + +.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); +} + +.ss-secondary--small { + width: 100px; +} + +.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; + flex-shrink: 0; +} + +.ss-arrow:hover { + background: #e5e8f2; +} + +.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-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; +} + +.ss-reviews { + display: flex; + align-items: center; + gap: 12px; + margin-top: 16px; +} + +.ss-reviews__track { + flex: 1; + display: grid; + grid-template-columns: repeat(3, minmax(220px, 1fr)); + gap: 16px; +} + +.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-avatar--lg { + width: 44px; + height: 44px; + background: #f0f2f7; + border-radius: 50%; + display: grid; + place-items: center; +} + .create-shift-modal-backdrop { position: fixed; - top: 0; left: 0; right: 0; bottom: 0; + inset: 0; background: rgba(10, 43, 102, 0.5); display: grid; place-items: center; @@ -351,14 +512,21 @@ 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 { display: flex; justify-content: space-between; align-items: flex-start; + gap: 12px; margin-bottom: 24px; border-bottom: 1px solid var(--ss-border); padding-bottom: 16px; @@ -438,12 +606,58 @@ body { cursor: pointer; } +@media (max-width: 1100px) { + .ss-shifts--list .ss-row, + .ss-row--incident, + .ss-priority-simple-row, + .ss-priority-topstats, + .ss-reviews__track, + .form-grid { + grid-template-columns: 1fr; + } + + .ss-priority-right, + .ss-incident-action { + align-items: flex-start; + justify-content: flex-start; + } + + .ss-reviews { + align-items: stretch; + } +} + @media (max-width: 980px) { .ss-incident-toolbar { grid-template-columns: 1fr 1fr; } } +@media (max-width: 768px) { + .ss-main { + padding: 20px; + } + + .ss-controls { + justify-content: flex-start; + } + + .ss-overview { + flex-direction: column; + align-items: stretch; + } + + .ss-arrow { + display: none; + } + + .create-shift-header, + .actions { + flex-direction: column; + align-items: stretch; + } +} + @media (max-width: 640px) { .ss-incident-toolbar { grid-template-columns: 1fr; diff --git a/app-frontend/employer-panel/src/pages/EmployerDashboard.js b/app-frontend/employer-panel/src/pages/EmployerDashboard.js index c9bd2ba85..e6ad1ea23 100644 --- a/app-frontend/employer-panel/src/pages/EmployerDashboard.js +++ b/app-frontend/employer-panel/src/pages/EmployerDashboard.js @@ -1,53 +1,100 @@ 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 IconCalendar = ({ className = "", ...props }) => ( + + + + + ); -const IconClock = (props) => ( - - - - + +const IconClock = ({ className = "", ...props }) => ( + + + + ); -const IconPlus = (props) => ( - - - + +const IconPlus = ({ className = "", ...props }) => ( + + + ); -const IconGrid = (props) => ( - - - - - + +const IconGrid = ({ className = "", ...props }) => ( + + + + + ); -const IconList = (props) => ( - - - - + +const IconList = ({ className = "", ...props }) => ( + + + + ); -const IconUser = (props) => ( - - - + +const IconUser = ({ className = "", ...props }) => ( + + + ); + const Star = ({ filled }) => ( - + ); @@ -75,16 +122,52 @@ const parseIncidentDateTime = (incident) => { return baseDate.getTime(); }; +const formatLocation = (locationOrVenue) => { + if (!locationOrVenue) return "N/A"; + if (typeof locationOrVenue === "string") return locationOrVenue; + + if (typeof locationOrVenue === "object") { + return [ + locationOrVenue.street, + locationOrVenue.suburb, + locationOrVenue.state, + locationOrVenue.postcode, + ] + .filter(Boolean) + .join(", "); + } + + return String(locationOrVenue); +}; + +const formatDateValue = (value) => { + if (!value) return "N/A"; + if (typeof value !== "string") return String(value); + + if (/^\d{2}-\d{2}-\d{4}$/.test(value)) return value; + + const parsed = new Date(value); + if (Number.isNaN(parsed.getTime())) return value; + + return parsed.toLocaleDateString("en-GB"); +}; + +const formatTimeRange = (shift) => { + if (shift.time) return shift.time; + if (shift.startTime && shift.endTime) return `${shift.startTime} - ${shift.endTime}`; + return "N/A"; +}; + 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(""); @@ -92,19 +175,20 @@ export default function EmployerDashboard() { const [incidentSeverityFilter, setIncidentSeverityFilter] = useState("All"); const [incidentSort, setIncidentSort] = useState("Newest"); 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 +198,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,53 +211,163 @@ 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("http://localhost:5000/api/v1/shifts", { + 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 rawItems = Array.isArray(data.items) + ? data.items + : Array.isArray(data.data) + ? data.data + : Array.isArray(data) + ? data + : []; + + const mappedShifts = rawItems.map((shift, index) => { + const rawStatus = shift.status; + const statusObject = + typeof rawStatus === "object" && rawStatus !== null + ? rawStatus + : { + 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 || index, + role: shift.role || shift.title || "Shift", + company: shift.company || shift.companyName || "SecureShift", + venue: formatLocation(shift.venue || shift.location), + rate: shift.rate || shift.hourlyRate || shift.payRate || 0, + date: formatDateValue(shift.date || shift.shiftDate), + time: formatTimeRange(shift), + status: statusObject, + priority: + shift.priority || (index % 3 === 0 ? "High" : index % 3 === 1 ? "Medium" : "Low"), + assignedGuards: shift.assignedGuards ?? shift.guardsAssigned ?? 0, + }; + }); + + setShifts(mappedShifts); + } catch (err) { + setError(err.message || "Failed to load shifts."); + + setShifts([ + { + id: 1, + role: "Crowd Control", + company: "AIG Solutions", + venue: "Marvel Stadium", + rate: 55, + status: { text: "Confirmed", tone: "confirmed" }, + date: "22-03-2026", + time: "5:00 pm - 1:00 am", + priority: "High", + assignedGuards: 2, + }, + { + id: 2, + role: "Shopping Centre Security", + company: "Vicinity Centres", + venue: "Chadstone Shopping Centre", + rate: 75, + status: { text: "Pending", tone: "pending" }, + date: "24-03-2026", + time: "1:00 pm - 9:00 pm", + priority: "Medium", + assignedGuards: 1, + }, + { + id: 3, + role: "Event Security", + company: "SecureShift", + venue: "Rod Laver Arena", + rate: 65, + status: { text: "Confirmed", tone: "confirmed" }, + date: "26-03-2026", + time: "2:00 pm - 10:00 pm", + priority: "High", + assignedGuards: 3, + }, + { + id: 4, + role: "Static Guarding", + company: "AIG Solutions", + venue: "Corporate Office", + rate: 50, + status: { text: "Completed (Rated)", tone: "completed" }, + date: "27-03-2026", + time: "8:00 am - 4:00 pm", + 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); - } - }; + const priorityOrder = { High: 0, Medium: 1, Low: 2 }; + + const priorityShifts = useMemo(() => { + return [...shifts].sort((a, b) => { + const aPriority = priorityOrder[a.priority] ?? 99; + const bPriority = priorityOrder[b.priority] ?? 99; + return aPriority - bPriority; + }); + }, [shifts]); - fetchShifts(); -}, []); + const highPriorityCount = useMemo( + () => shifts.filter((shift) => shift.priority === "High").length, + [shifts] + ); + + const totalAssignedGuards = useMemo( + () => shifts.reduce((total, shift) => total + (shift.assignedGuards || 0), 0), + [shifts] + ); - 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 upcomingShiftCount = shifts.length; const scrollByAmount = (ref, amt) => { if (!ref.current) return; @@ -180,9 +375,13 @@ useEffect(() => { }; const updateIncident = (id, newStatus, newSeverity, newComments) => { - setIncidents(prev => prev.map(inc => - inc.id === id ? { ...inc, status: newStatus, severity: newSeverity, comments: newComments } : inc - )); + setIncidents((prev) => + prev.map((inc) => + inc.id === id + ? { ...inc, status: newStatus, severity: newSeverity, comments: newComments } + : inc + ) + ); setSelectedIncident(null); }; @@ -215,18 +414,11 @@ useEffect(() => { return matchesQuery && matchesStatus && matchesSeverity; }) .sort((a, b) => { - if (incidentSort === "Newest") { - return parseIncidentDateTime(b) - parseIncidentDateTime(a); - } - - if (incidentSort === "Oldest") { - return parseIncidentDateTime(a) - parseIncidentDateTime(b); - } - + 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]); @@ -245,134 +437,173 @@ useEffect(() => { return (
- - {/* -------- Overview -------- */}

Overview

- - {/* Controls ABOVE grey grid */} +
+
- - {/* Grey Grid */} +
- +
- - - + {view === "grid" && ( +
navigate("/create-shift")}> +
+ +
+
Create Shift
+
+ )} + {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" ? ( -
+ {!loading && !error && shifts.length === 0 &&
No shifts found.
} + + {shifts.map((s, idx) => + view === "grid" ? ( +
-
{displayTitle}
-
${displayRate} p/h
+
{s.role}
+
${s.rate} p/h
+
+
+ {s.company} — {s.venue}
- -
{displayLocation}
- -
- Status: {displayStatus} +
+ Status: {s.status.text || s.status}
-
- {displayDate} + {s.date} - {displayTime} + {s.time}
) : ( -
-
{displayTitle}
-
{displayLocation}
-
${displayRate} p/h
+
+
{s.role}
+
{s.company} — {s.venue}
+
${s.rate} p/h
- {displayDate} + + {s.date}
- {displayTime} + + {s.time}
-
- Status: {displayStatus} +
+ Status: {s.status.text || s.status}
- ); - })} + ) + )}
- +
- {/* Incident Reports */} +

Priority Shifts

+
+
+
+
+

{upcomingShiftCount}

+

Upcoming Shifts

+
+
+

{highPriorityCount}

+

High Priority

+
+
+

{totalAssignedGuards}

+

Guards Assigned

+
+
+ +
+ {priorityShifts.map((shift, idx) => ( +
+
+
{shift.role}
+
+ {shift.company} — {shift.venue} +
+
+ +
+
+ + {shift.date} +
+
+ + {shift.time} +
+
+ +
+ + {shift.priority} + +
{shift.assignedGuards} guards assigned
+
+
+ ))} +
+
+
+

Incident Reports

{ Reset
+
{incidentSummary.total} Total {incidentSummary.pending} Pending {incidentSummary.resolved} Resolved {filteredIncidents.length} Showing
+
-
-
+
{filteredIncidents.length === 0 && (
No incident reports match the current filters.
)} + {filteredIncidents.map((inc, i) => ( -
-
{inc.guard}
+
+
+ {inc.guard} +
{inc.shift}
{inc.id}
- {inc.date} + + {inc.date}
{inc.status}
-
- +
+
))}
-
- {/* Reviews */}

Recent Review

- + +
{reviews.map((r, i) => (
-
+
+ +
{r.name}
{r.role}
+
- {[0,1,2,3,4].map((k) => )} + {[0, 1, 2, 3, 4].map((k) => ( + + ))}
- + +
))}
- + +
- {/* 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}

+ {selectedIncident.status}
-
+
-
{selectedIncident.guard}
+
+ {selectedIncident.guard} +
+