From f0ebdd02b365891a2eb6cd90d62f8b5c7eadbdc6 Mon Sep 17 00:00:00 2001 From: ujwalsharma07 Date: Sun, 3 May 2026 19:23:24 +1000 Subject: [PATCH] Add shift swap and leave request approval system --- .../src/pages/EmployerDashboard.css | 636 ++++++++++++----- .../src/pages/EmployerDashboard.js | 658 ++++++++++++------ 2 files changed, 920 insertions(+), 374 deletions(-) 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 */} +
- - +
- - - {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}
- ); - })} + ) + )}
- -
- {/* 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}
+
+ +
+ + +
+
+ ))} +
+
+
+

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}
- +
))} @@ -444,39 +688,47 @@ useEffect(() => {
- {/* 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}

@@ -485,17 +737,19 @@ useEffect(() => {
-
+
-
{selectedIncident.guard}
+
+ {selectedIncident.guard} +