From f428137c60258ca927a8447b03d5e71c393f08b8 Mon Sep 17 00:00:00 2001 From: Evangeline-d Date: Fri, 27 Mar 2026 20:05:58 +1100 Subject: [PATCH 1/5] Fix guards page backend integration and error handling --- app-frontend/employer-panel/.env | 2 +- .../employer-panel/src/pages/GuardProfile.js | 378 ++++++------------ docker-compose.yml | 4 +- 3 files changed, 131 insertions(+), 253 deletions(-) diff --git a/app-frontend/employer-panel/.env b/app-frontend/employer-panel/.env index 2b00470c9..31d35cbf7 100644 --- a/app-frontend/employer-panel/.env +++ b/app-frontend/employer-panel/.env @@ -1,2 +1,2 @@ -REACT_APP_API_BASE_URL=http://localhost:5000/api/v1 +REACT_APP_API_BASE_URL=http://localhost:5001/api/v1 NODE_ENV=development diff --git a/app-frontend/employer-panel/src/pages/GuardProfile.js b/app-frontend/employer-panel/src/pages/GuardProfile.js index 2466563e6..4eb9c78e5 100644 --- a/app-frontend/employer-panel/src/pages/GuardProfile.js +++ b/app-frontend/employer-panel/src/pages/GuardProfile.js @@ -1,30 +1,17 @@ -import React, { useState, useRef, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -import translations from "../i18n/translations"; +import React, { useState, useRef, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import http from "../lib/http"; // ❌ removed local dummy guardData -const allSkills = [ - 'CCTV Monitoring', - 'Crowd Control', - 'Defensive Driving', - 'First Aid', - 'Gate Control', - 'K9 Patrol', - 'Loss Prevention', - 'Patrolling', - 'Perimeter Security', - 'Surveillance', - 'Vehicle Patrol', -]; -const availabilityOptions = ['Available', 'Unavailable', 'On Leave']; +const allSkills = ["CCTV Monitoring","Crowd Control","Defensive Driving","First Aid","Gate Control","K9 Patrol","Loss Prevention","Patrolling","Perimeter Security","Surveillance","Vehicle Patrol"]; +const availabilityOptions = ["Available","Unavailable","On Leave"]; // NEW: read API base from env (Vite or CRA) --------------------------------- -const API_BASE = process.env.REACT_APP_API_BASE_URL || 'http://localhost:5000'; -// NEW +const API_BASE = process.env.REACT_APP_API_BASE_URL || "http://localhost:5000"; + // NEW -function GuardProfiles({ language }) { - const t = translations[language || "en"] || translations.en; +function GuardProfiles() { const navigate = useNavigate(); const [currentPage, setCurrentPage] = useState(1); const [selectedSkills, setSelectedSkills] = useState([]); @@ -36,99 +23,75 @@ function GuardProfiles({ language }) { const availabilityRef = useRef(); // NEW: state for API data / loading / error --------------------------------- - const [guards, setGuards] = useState([]); // NEW - const [loading, setLoading] = useState(true); // NEW - const [error, setError] = useState(''); // NEW + const [guards, setGuards] = useState([]); // NEW + const [loading, setLoading] = useState(true); // NEW + const [error, setError] = useState(""); // NEW useEffect(() => { const handleClickOutside = (event) => { if (skillsRef.current && !skillsRef.current.contains(event.target)) setSkillsOpen(false); - if (availabilityRef.current && !availabilityRef.current.contains(event.target)) - setAvailabilityOpen(false); + if (availabilityRef.current && !availabilityRef.current.contains(event.target)) setAvailabilityOpen(false); }; - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); }, []); // NEW: fetch guards from backend -------------------------------------------- - useEffect(() => { - // NEW - let mounted = true; // NEW - (async () => { - // NEW - try { - // NEW - setLoading(true); // NEW - setError(''); // NEW - - const token = localStorage.getItem('token'); // NEW (if you use JWT) - const res = await fetch(`${API_BASE}/api/v1/users/guards`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, // ✅ send token - }, - }); // NEW - - if (!res.ok) { - // NEW - const text = await res.text().catch(() => ''); // NEW - throw new Error(text || `Request failed (${res.status})`); // NEW - } // NEW + useEffect(() => { // NEW + let mounted = true; // NEW + (async () => { // NEW + try { // NEW + setLoading(true); // NEW + setError(""); // NEW - const data = await res.json(); // NEW + const { data } = await http.get("/users/guards"); // NEW // Accept both shapes: array OR {guards:[...]} ----------------- // NEW const list = Array.isArray(data) ? data : Array.isArray(data?.guards) ? data.guards : []; // NEW // Normalize fields so UI always has name/skills/availability/photo - const normalized = list.map((g, i) => ({ - // NEW - id: g._id || g.id || String(i), // NEW - name: g.name || [g.firstName, g.lastName].filter(Boolean).join(' ') || 'Unknown', // NEW + const normalized = list.map((g, i) => ({ // NEW + id: g._id || g.id || String(i), // NEW + name: + g.name || + [g.firstName, g.lastName].filter(Boolean).join(" ") || + "Unknown", // NEW skills: Array.isArray(g.skills) ? g.skills - : typeof g.skills === 'string' - ? g.skills.split(',').map((s) => s.trim()) - : Array.isArray(g.skillset) - ? g.skillset - : [], // NEW - availability: g.availability ?? g.status ?? (g.available ? 'Available' : 'Unavailable'), // NEW - photo: g.photo?.url || g.photo || g.avatar || g.imageUrl || '/GuardPicPlaceholder.png', // NEW - })); // NEW - - if (mounted) setGuards(normalized); // NEW + : typeof g.skills === "string" + ? g.skills.split(",").map((s) => s.trim()) + : Array.isArray(g.skillset) + ? g.skillset + : [], // NEW + availability: + g.availability ?? g.status ?? (g.available ? "Available" : "Unavailable"), // NEW + photo: g.photo?.url || g.photo || g.avatar || g.imageUrl || "/GuardPicPlaceholder.png", // NEW + })); // NEW + + if (mounted) setGuards(normalized); // NEW } catch (e) { - // NEW - if (mounted) setError(e.message || 'Failed to fetch guards'); // NEW - } finally { - // NEW - if (mounted) setLoading(false); // NEW - } // NEW - })(); // NEW - return () => { - mounted = false; - }; // NEW - }, []); // NEW - - const toggleSkill = (skill) => - setSelectedSkills((prev) => - prev.includes(skill) ? prev.filter((s) => s !== skill) : [...prev, skill] - ); - const toggleAvailability = (avail) => - setSelectedAvailability((prev) => - prev.includes(avail) ? prev.filter((a) => a !== avail) : [...prev, avail] - ); + if (!mounted) return; + + if (e.response?.status === 403) { + setError("You don’t have permission to view guards yet."); + } else { + setError(e.response?.data?.message || e.message || "Failed to fetch guards"); + } +} finally { // NEW + if (mounted) setLoading(false); // NEW + } // NEW + })(); // NEW + return () => { mounted = false; }; // NEW + }, []); // NEW + + const toggleSkill = (skill) => setSelectedSkills(prev => prev.includes(skill) ? prev.filter(s => s !== skill) : [...prev, skill]); + const toggleAvailability = (avail) => setSelectedAvailability(prev => prev.includes(avail) ? prev.filter(a => a !== avail) : [...prev, avail]); // UPDATED: filter the fetched guards (not the old dummy array) -------------- - const filteredGuards = guards.filter( - ( - guard // UPDATED - ) => - (selectedSkills.length === 0 || - selectedSkills.every((skill) => (guard.skills || []).includes(skill))) && - (selectedAvailability.length === 0 || selectedAvailability.includes(guard.availability)) - ); // UPDATED + const filteredGuards = guards.filter(guard => // UPDATED + (selectedSkills.length === 0 || selectedSkills.every(skill => (guard.skills || []).includes(skill))) && + (selectedAvailability.length === 0 || selectedAvailability.includes(guard.availability)) + ); // UPDATED const indexOfLastCard = currentPage * cardsPerPage; const indexOfFirstCard = indexOfLastCard - cardsPerPage; @@ -136,152 +99,81 @@ function GuardProfiles({ language }) { const totalPages = Math.max(1, Math.ceil(filteredGuards.length / cardsPerPage)); // small safety // ===== Inline Styles ===== - const pageStyle = { - display: 'flex', - flexDirection: 'column', - minHeight: '100vh', - backgroundColor: '#fafafa', - }; - const contentStyle = { flex: 1, padding: '1rem' }; - const filtersStyle = { display: 'flex', justifyContent: 'center', gap: '1rem', margin: '1rem 0' }; - const dropdownStyle = { position: 'relative', display: 'inline-block' }; - + const pageStyle = { display: "flex", flexDirection: "column", minHeight: "100vh", backgroundColor: "#fafafa" }; + const contentStyle = { flex: 1, padding: "1rem" }; + const filtersStyle = { display: "flex", justifyContent: "center", gap: "1rem", margin: "1rem 0" }; + const dropdownStyle = { position: "relative", display: "inline-block" }; + const dropdownButtonStyle = (open = false, selectedCount = 0) => ({ - padding: '8px 16px', - borderRadius: '9999px', - border: 'none', - backgroundColor: open || selectedCount > 0 ? '#ababab' : '#274b93', - color: '#fff', - fontFamily: 'Poppins, sans-serif', + padding: "8px 16px", + borderRadius: "9999px", + border: "none", + backgroundColor: open || selectedCount > 0 ? "#ababab" : "#274b93", + color: "#fff", + fontFamily: "Poppins, sans-serif", fontWeight: 500, - fontSize: '16px', - cursor: 'pointer', - minWidth: '180px', - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - transition: 'background 0.2s ease', + fontSize: "16px", + cursor: "pointer", + minWidth: "180px", + display: "flex", + justifyContent: "space-between", + alignItems: "center", + transition: "background 0.2s ease" }); const dropdownContentStyle = { - position: 'absolute', - top: '110%', + position: "absolute", + top: "110%", left: 0, - backgroundColor: '#fff', - border: '1px solid #ccc', - borderRadius: '12px', - padding: '0.5rem 1rem', - boxShadow: '0 2px 6px rgba(0,0,0,0.15)', + backgroundColor: "#fff", + border: "1px solid #ccc", + borderRadius: "12px", + padding: "0.5rem 1rem", + boxShadow: "0 2px 6px rgba(0,0,0,0.15)", zIndex: 10, - maxHeight: '250px', - overflowY: 'auto', - whiteSpace: 'nowrap', - width: 'max-content', - minWidth: '180px', + maxHeight: "250px", + overflowY: "auto", + whiteSpace: "nowrap", + width: "max-content", + minWidth: "180px" }; - const dropdownLabelStyle = { - display: 'flex', - alignItems: 'center', - gap: '8px', - fontFamily: 'Poppins, sans-serif', - fontSize: '16px', - fontWeight: 500, - marginBottom: '6px', - cursor: 'pointer', - }; - const checkboxStyle = { - width: '18px', - height: '18px', - accentColor: '#274b93', - cursor: 'pointer', - }; - const guardContainerStyle = { - display: 'flex', - flexWrap: 'wrap', - justifyContent: 'center', - padding: '2rem', - gap: '1rem', - }; - const guardCardStyle = { - border: '1px solid #e0e0e0', - borderRadius: '12px', - padding: '20px 24px', - backgroundColor: '#ababab', - boxShadow: '0 2px 6px rgba(0,0,0,0.08)', - textAlign: 'center', - fontFamily: 'Poppins, sans-serif', - flex: '0 0 calc(25% - 1rem)', - boxSizing: 'border-box', - }; - const guardImageStyle = { - borderRadius: '50%', - width: '100px', - height: '100px', - marginBottom: '1rem', - objectFit: 'cover', - }; // tiny improvement - const backButtonStyle = { - display: 'block', - margin: '2rem auto', - padding: '12px 32px', - border: 'none', - borderRadius: '9999px', - backgroundColor: '#274b93', - color: '#fff', - fontSize: '16px', - fontWeight: 500, - cursor: 'pointer', - transition: 'background 0.2s ease', - textAlign: 'center', - }; - const paginationStyle = { textAlign: 'center', margin: '1rem 0' }; - const paginationButtonStyle = (active = false) => ({ - margin: '0 4px', - padding: '8px 12px', - borderRadius: '12px', - border: 'none', - backgroundColor: active ? '#ababab' : '#274b93', - color: active ? '#000' : '#fff', - fontFamily: 'Poppins, sans-serif', - fontWeight: 500, - fontSize: '16px', - cursor: 'pointer', - transition: 'background 0.2s ease', - }); + const dropdownLabelStyle = { display: "flex", alignItems: "center", gap: "8px", fontFamily: "Poppins, sans-serif", fontSize: "16px", fontWeight: 500, marginBottom: "6px", cursor: "pointer" }; + const checkboxStyle = { width: "18px", height: "18px", accentColor: "#274b93", cursor: "pointer" }; + const guardContainerStyle = { display: "flex", flexWrap: "wrap", justifyContent: "center", padding: "2rem", gap: "1rem" }; + const guardCardStyle = { border: "1px solid #e0e0e0", borderRadius: "12px", padding: "20px 24px", backgroundColor: "#ababab", boxShadow: "0 2px 6px rgba(0,0,0,0.08)", textAlign: "center", fontFamily: "Poppins, sans-serif", flex: "0 0 calc(25% - 1rem)", boxSizing: "border-box" }; + const guardImageStyle = { borderRadius: "50%", width: "100px", height: "100px", marginBottom: "1rem", objectFit: "cover" }; // tiny improvement + const backButtonStyle = { display: "block", margin: "2rem auto", padding: "12px 32px", border: "none", borderRadius: "9999px", backgroundColor: "#274b93", color: "#fff", fontSize: "16px", fontWeight: 500, cursor: "pointer", transition: "background 0.2s ease", textAlign: "center" }; + const paginationStyle = { textAlign: "center", margin: "1rem 0" }; + const paginationButtonStyle = (active = false) => ({ margin: "0 4px", padding: "8px 12px", borderRadius: "12px", border: "none", backgroundColor: active ? "#ababab" : "#274b93", color: active ? "#000" : "#fff", fontFamily: "Poppins, sans-serif", fontWeight: 500, fontSize: "16px", cursor: "pointer", transition: "background 0.2s ease" }); return (
-

{t.guardProfiles}

+

Guard Profiles

+ {/* NEW: loading / error / empty states */} - {loading &&

Loading guards…

} {/* NEW */} - {!loading && error /* NEW */ && ( -

Failed to load: {error}

/* NEW */ - )} - {!loading && !error && guards.length === 0 /* NEW */ && ( -

No guards found.

/* NEW */ - )} + {loading &&

Loading guards…

} {/* NEW */} + {!loading && error && ( /* NEW */ +

{error}

/* NEW */ + + )} + {!loading && !error && guards.length === 0 && ( /* NEW */ +

No guards found.

/* NEW */ + )} + {/* Filters */}
- {skillsOpen && (
- {allSkills.map((skill) => ( + {allSkills.map(skill => ( ))} @@ -290,24 +182,15 @@ function GuardProfiles({ language }) {
- {availabilityOpen && (
- {availabilityOptions.map((avail) => ( + {availabilityOptions.map(avail => ( ))} @@ -315,39 +198,34 @@ function GuardProfiles({ language }) { )}
+ {/* Guard Cards */}
- {currentCards.map((guard) => ( + {currentCards.map(guard => (
{guard.name}

{guard.name}

-

- Skills: {(guard.skills || []).join(', ')} -

-

- Availability: {guard.availability} -

+

Skills: {(guard.skills || []).join(", ")}

+

Availability: {guard.availability}

))}
+ {/* Pagination */}
{Array.from({ length: totalPages }, (_, i) => ( - ))}
+ {/* Back Button */} @@ -356,4 +234,4 @@ function GuardProfiles({ language }) { ); } -export default GuardProfiles; +export default GuardProfiles; diff --git a/docker-compose.yml b/docker-compose.yml index 4cd4d3b14..489ddbc69 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: context: ./app-backend container_name: secureshift-backend ports: - - "5000:5000" + - "5001:5000" env_file: - ./app-backend/.env depends_on: @@ -46,4 +46,4 @@ volumes: mongo-data: networks: - secureshift: \ No newline at end of file + secureshift: From 2236a57601c3c41b40baefe5edddb72c1db48e3f Mon Sep 17 00:00:00 2001 From: Evangeline-d Date: Thu, 16 Apr 2026 19:06:43 +1000 Subject: [PATCH 2/5] Update guards page based on feedback - allow viewing instead of blocking --- app-frontend/employer-panel/src/pages/GuardProfile.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app-frontend/employer-panel/src/pages/GuardProfile.js b/app-frontend/employer-panel/src/pages/GuardProfile.js index 4eb9c78e5..a80cf2ede 100644 --- a/app-frontend/employer-panel/src/pages/GuardProfile.js +++ b/app-frontend/employer-panel/src/pages/GuardProfile.js @@ -72,11 +72,8 @@ function GuardProfiles() { } catch (e) { if (!mounted) return; - if (e.response?.status === 403) { - setError("You don’t have permission to view guards yet."); - } else { - setError(e.response?.data?.message || e.message || "Failed to fetch guards"); - } + setError(""); // don't block UI + } finally { // NEW if (mounted) setLoading(false); // NEW } // NEW From 53ec32437967470bae0e36a73f9c99ba322e999e Mon Sep 17 00:00:00 2001 From: Evangeline-d Date: Sat, 9 May 2026 23:39:55 +1000 Subject: [PATCH 3/5] Fix CreateShift filename casing --- .../employer-panel/src/pages/CreateShift.css | 443 ------------------ 1 file changed, 443 deletions(-) delete mode 100644 app-frontend/employer-panel/src/pages/CreateShift.css diff --git a/app-frontend/employer-panel/src/pages/CreateShift.css b/app-frontend/employer-panel/src/pages/CreateShift.css deleted file mode 100644 index 1d8631593..000000000 --- a/app-frontend/employer-panel/src/pages/CreateShift.css +++ /dev/null @@ -1,443 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap'); - -:root { - --ink: #0f172a; - --panel: #0b1531; - --muted: #7c8aa1; - --line: rgba(255, 255, 255, 0.1); - --accent: #4f8bff; - --accent-strong: #7df3e1; - --error: #ff6b6b; - --success: #7ed957; -} - -.cs-shell { - min-height: calc(100vh - 120px); - background: - radial-gradient(circle at 10% 20%, rgba(127, 255, 212, 0.08), transparent 30%), - radial-gradient(circle at 70% 10%, rgba(79, 139, 255, 0.12), transparent 35%), - linear-gradient(135deg, #050915 0%, #0c1736 50%, #0a1026 100%); - color: #f4f6fb; - padding: 32px clamp(16px, 4vw, 48px) 48px; - font-family: 'Space Grotesk', 'Manrope', 'Segoe UI', sans-serif; -} - -.cs-topbar { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 16px; - margin-bottom: 18px; -} - -.cs-topbar__actions { - display: flex; - gap: 12px; -} - -.cs-kicker { - letter-spacing: 0.08em; - text-transform: uppercase; - font-size: 12px; - color: var(--muted); - margin: 0 0 4px; -} - -.cs-title { - margin: 0; - font-size: clamp(26px, 4vw, 34px); - font-weight: 700; -} - -.cs-subtitle { - margin: 6px 0 0; - color: var(--muted); - max-width: 620px; -} - -.cs-grid { - display: grid; - grid-template-columns: 2.1fr 1.1fr; - gap: 20px; -} - -.cs-card { - background: linear-gradient(150deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.02)); - border: 1px solid var(--line); - border-radius: 18px; - padding: 20px; - box-shadow: 0 20px 80px rgba(0, 0, 0, 0.25); -} - -.cs-card--form { - display: flex; - flex-direction: column; -} - -.cs-section { - display: flex; - flex-direction: column; - gap: 16px; -} - -.cs-field { - display: flex; - flex-direction: column; - gap: 8px; -} - -.cs-field label { - font-weight: 600; - color: #e8edfb; -} - -.cs-field input, -.cs-field select, -.cs-field textarea { - background: rgba(255, 255, 255, 0.04); - border: 1px solid var(--line); - border-radius: 12px; - padding: 12px 14px; - color: #f8fbff; - font-size: 14px; - outline: none; -} - -.cs-field select option { - color: #000000; - background: #ffffff; -} - -.cs-field input:focus, -.cs-field select:focus, -.cs-field textarea:focus { - border-color: var(--accent); - box-shadow: 0 0 0 3px rgba(79, 139, 255, 0.2); -} - -.cs-field__error { - color: var(--error); - font-size: 13px; -} - -.cs-hint { - margin: 0; - color: var(--muted); - font-size: 13px; -} - -.cs-site-row { - display: flex; - gap: 10px; - align-items: center; -} - -.cs-inline-card { - border: 1px dashed var(--line); - padding: 12px; - border-radius: 12px; - background: rgba(255, 255, 255, 0.03); - display: flex; - flex-direction: column; - gap: 10px; -} - -.cs-inline-actions { - display: flex; - justify-content: flex-end; - gap: 10px; -} - -.cs-two-col, -.cs-three-col { - display: grid; - gap: 12px; -} - -.cs-two-col { - grid-template-columns: repeat(2, 1fr); -} - -.cs-three-col { - grid-template-columns: repeat(3, 1fr); -} - -.cs-pillset { - display: flex; - gap: 10px; -} - -.cs-pill { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 10px 14px; - border-radius: 40px; - background: rgba(255, 255, 255, 0.05); - border: 1px solid var(--line); - cursor: pointer; -} - -.cs-pill input { - display: none; -} - -.cs-pill.is-active { - border-color: var(--accent); - background: rgba(79, 139, 255, 0.12); -} - -.cs-taglist { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: 10px; -} - -.cs-tag { - position: relative; - padding: 12px; - border: 1px solid var(--line); - border-radius: 14px; - background: rgba(255, 255, 255, 0.03); - display: flex; - flex-direction: column; - gap: 4px; - cursor: pointer; -} - -.cs-tag small { - color: var(--muted); -} - -.cs-tag input { - position: absolute; - opacity: 0; -} - -.cs-tag.is-active { - border-color: var(--accent-strong); - box-shadow: 0 10px 30px rgba(79, 139, 255, 0.15); -} - -.cs-actions { - display: flex; - justify-content: flex-end; - gap: 10px; - margin-top: 16px; -} - -.cs-ghost, -.cs-primary { - border-radius: 12px; - border: 1px solid var(--line); - padding: 11px 16px; - font-weight: 600; - cursor: pointer; - color: #f8fbff; - background: rgba(255, 255, 255, 0.04); -} - -.cs-primary { - background: linear-gradient(120deg, var(--accent), #6ae2ff); - color: #041024; - border: none; -} - -.cs-primary:disabled { - opacity: 0.65; - cursor: not-allowed; -} - -.cs-card--map { - display: flex; - flex-direction: column; - gap: 14px; -} - -.cs-card__header { - display: flex; - justify-content: space-between; - align-items: center; -} - -.cs-side-title { - margin: 6px 0 0; -} - -.cs-map { - height: 320px; - border-radius: 14px; - overflow: hidden; - background: #0c1225; - border: 1px solid var(--line); - position: relative; -} - -.cs-map__loading { - position: absolute; - inset: 0; - display: grid; - place-items: center; - color: var(--muted); - font-size: 14px; -} - -.cs-side-list { - display: flex; - flex-direction: column; - gap: 10px; -} - -.cs-side-row { - display: flex; - justify-content: space-between; - align-items: center; - color: #d8e2f6; - border-bottom: 1px solid var(--line); - padding: 8px 0; -} - -.cs-chip { - background: rgba(79, 139, 255, 0.12); - color: #cfe2ff; - padding: 6px 12px; - border-radius: 40px; - border: 1px solid var(--line); -} - -.cs-banner { - border-radius: 14px; - padding: 12px 16px; - margin-bottom: 14px; - border: 1px solid var(--line); - background: rgba(255, 255, 255, 0.05); -} - -.cs-banner--error { - border-color: rgba(255, 107, 107, 0.4); - background: rgba(255, 107, 107, 0.08); - color: #ffd4d4; -} - -.cs-modal { - position: fixed; - inset: 0; - background: rgba(3, 6, 15, 0.65); - display: grid; - place-items: center; - backdrop-filter: blur(6px); - padding: 20px; - z-index: 40; -} - -.cs-modal__dialog { - width: min(920px, 100%); - background: #0c1327; - border: 1px solid var(--line); - border-radius: 16px; - padding: 20px; - box-shadow: 0 25px 100px rgba(0, 0, 0, 0.45); -} - -.cs-modal__head { - display: flex; - justify-content: space-between; - align-items: center; -} - -.cs-preview-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 12px; - margin: 16px 0; -} - -.cs-label { - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--muted); - font-size: 12px; - margin: 0; -} - -.cs-subtle { - margin: 6px 0 0; - color: var(--muted); -} - -.cs-modal__actions { - display: flex; - justify-content: flex-end; - gap: 10px; -} - -.cs-banner + .cs-grid { - margin-top: 8px; -} - -.cs-side-row strong { - font-size: 15px; -} - -.cs-side-row span { - color: var(--muted); -} - -.cs-actions button, -.cs-modal__actions button { - min-width: 160px; -} - -.cs-topbar__actions button { - min-width: 140px; -} - -.cs-preview-grid h4 { - margin: 4px 0 0; -} - -.cs-subtle + .cs-subtle { - margin-top: 2px; -} - -.cs-grid textarea { - resize: vertical; -} - -@media (max-width: 1080px) { - .cs-grid { - grid-template-columns: 1fr; - } - - .cs-card--map { - order: -1; - } -} - -@media (max-width: 720px) { - .cs-topbar { - flex-direction: column; - } - - .cs-topbar__actions { - width: 100%; - justify-content: flex-start; - } - - .cs-two-col, - .cs-three-col { - grid-template-columns: 1fr; - } - - .cs-actions, - .cs-modal__actions, - .cs-topbar__actions { - flex-direction: column; - } - - .cs-actions button, - .cs-modal__actions button, - .cs-topbar__actions button { - width: 100%; - } -} From 91e600ce71c757f0800ea79b05b23f3a395ed2c5 Mon Sep 17 00:00:00 2001 From: Evangeline-d Date: Tue, 19 May 2026 01:43:18 +1000 Subject: [PATCH 4/5] feat(shifts): add read-only assigned guards section --- .../employer-panel/src/pages/CreateShift.css | 443 ++++++++++++++++++ .../employer-panel/src/pages/ManageShift.js | 86 +++- 2 files changed, 512 insertions(+), 17 deletions(-) create mode 100644 app-frontend/employer-panel/src/pages/CreateShift.css diff --git a/app-frontend/employer-panel/src/pages/CreateShift.css b/app-frontend/employer-panel/src/pages/CreateShift.css new file mode 100644 index 000000000..1d8631593 --- /dev/null +++ b/app-frontend/employer-panel/src/pages/CreateShift.css @@ -0,0 +1,443 @@ +@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap'); + +:root { + --ink: #0f172a; + --panel: #0b1531; + --muted: #7c8aa1; + --line: rgba(255, 255, 255, 0.1); + --accent: #4f8bff; + --accent-strong: #7df3e1; + --error: #ff6b6b; + --success: #7ed957; +} + +.cs-shell { + min-height: calc(100vh - 120px); + background: + radial-gradient(circle at 10% 20%, rgba(127, 255, 212, 0.08), transparent 30%), + radial-gradient(circle at 70% 10%, rgba(79, 139, 255, 0.12), transparent 35%), + linear-gradient(135deg, #050915 0%, #0c1736 50%, #0a1026 100%); + color: #f4f6fb; + padding: 32px clamp(16px, 4vw, 48px) 48px; + font-family: 'Space Grotesk', 'Manrope', 'Segoe UI', sans-serif; +} + +.cs-topbar { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 16px; + margin-bottom: 18px; +} + +.cs-topbar__actions { + display: flex; + gap: 12px; +} + +.cs-kicker { + letter-spacing: 0.08em; + text-transform: uppercase; + font-size: 12px; + color: var(--muted); + margin: 0 0 4px; +} + +.cs-title { + margin: 0; + font-size: clamp(26px, 4vw, 34px); + font-weight: 700; +} + +.cs-subtitle { + margin: 6px 0 0; + color: var(--muted); + max-width: 620px; +} + +.cs-grid { + display: grid; + grid-template-columns: 2.1fr 1.1fr; + gap: 20px; +} + +.cs-card { + background: linear-gradient(150deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.02)); + border: 1px solid var(--line); + border-radius: 18px; + padding: 20px; + box-shadow: 0 20px 80px rgba(0, 0, 0, 0.25); +} + +.cs-card--form { + display: flex; + flex-direction: column; +} + +.cs-section { + display: flex; + flex-direction: column; + gap: 16px; +} + +.cs-field { + display: flex; + flex-direction: column; + gap: 8px; +} + +.cs-field label { + font-weight: 600; + color: #e8edfb; +} + +.cs-field input, +.cs-field select, +.cs-field textarea { + background: rgba(255, 255, 255, 0.04); + border: 1px solid var(--line); + border-radius: 12px; + padding: 12px 14px; + color: #f8fbff; + font-size: 14px; + outline: none; +} + +.cs-field select option { + color: #000000; + background: #ffffff; +} + +.cs-field input:focus, +.cs-field select:focus, +.cs-field textarea:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(79, 139, 255, 0.2); +} + +.cs-field__error { + color: var(--error); + font-size: 13px; +} + +.cs-hint { + margin: 0; + color: var(--muted); + font-size: 13px; +} + +.cs-site-row { + display: flex; + gap: 10px; + align-items: center; +} + +.cs-inline-card { + border: 1px dashed var(--line); + padding: 12px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.03); + display: flex; + flex-direction: column; + gap: 10px; +} + +.cs-inline-actions { + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.cs-two-col, +.cs-three-col { + display: grid; + gap: 12px; +} + +.cs-two-col { + grid-template-columns: repeat(2, 1fr); +} + +.cs-three-col { + grid-template-columns: repeat(3, 1fr); +} + +.cs-pillset { + display: flex; + gap: 10px; +} + +.cs-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 10px 14px; + border-radius: 40px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--line); + cursor: pointer; +} + +.cs-pill input { + display: none; +} + +.cs-pill.is-active { + border-color: var(--accent); + background: rgba(79, 139, 255, 0.12); +} + +.cs-taglist { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 10px; +} + +.cs-tag { + position: relative; + padding: 12px; + border: 1px solid var(--line); + border-radius: 14px; + background: rgba(255, 255, 255, 0.03); + display: flex; + flex-direction: column; + gap: 4px; + cursor: pointer; +} + +.cs-tag small { + color: var(--muted); +} + +.cs-tag input { + position: absolute; + opacity: 0; +} + +.cs-tag.is-active { + border-color: var(--accent-strong); + box-shadow: 0 10px 30px rgba(79, 139, 255, 0.15); +} + +.cs-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 16px; +} + +.cs-ghost, +.cs-primary { + border-radius: 12px; + border: 1px solid var(--line); + padding: 11px 16px; + font-weight: 600; + cursor: pointer; + color: #f8fbff; + background: rgba(255, 255, 255, 0.04); +} + +.cs-primary { + background: linear-gradient(120deg, var(--accent), #6ae2ff); + color: #041024; + border: none; +} + +.cs-primary:disabled { + opacity: 0.65; + cursor: not-allowed; +} + +.cs-card--map { + display: flex; + flex-direction: column; + gap: 14px; +} + +.cs-card__header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.cs-side-title { + margin: 6px 0 0; +} + +.cs-map { + height: 320px; + border-radius: 14px; + overflow: hidden; + background: #0c1225; + border: 1px solid var(--line); + position: relative; +} + +.cs-map__loading { + position: absolute; + inset: 0; + display: grid; + place-items: center; + color: var(--muted); + font-size: 14px; +} + +.cs-side-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.cs-side-row { + display: flex; + justify-content: space-between; + align-items: center; + color: #d8e2f6; + border-bottom: 1px solid var(--line); + padding: 8px 0; +} + +.cs-chip { + background: rgba(79, 139, 255, 0.12); + color: #cfe2ff; + padding: 6px 12px; + border-radius: 40px; + border: 1px solid var(--line); +} + +.cs-banner { + border-radius: 14px; + padding: 12px 16px; + margin-bottom: 14px; + border: 1px solid var(--line); + background: rgba(255, 255, 255, 0.05); +} + +.cs-banner--error { + border-color: rgba(255, 107, 107, 0.4); + background: rgba(255, 107, 107, 0.08); + color: #ffd4d4; +} + +.cs-modal { + position: fixed; + inset: 0; + background: rgba(3, 6, 15, 0.65); + display: grid; + place-items: center; + backdrop-filter: blur(6px); + padding: 20px; + z-index: 40; +} + +.cs-modal__dialog { + width: min(920px, 100%); + background: #0c1327; + border: 1px solid var(--line); + border-radius: 16px; + padding: 20px; + box-shadow: 0 25px 100px rgba(0, 0, 0, 0.45); +} + +.cs-modal__head { + display: flex; + justify-content: space-between; + align-items: center; +} + +.cs-preview-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 12px; + margin: 16px 0; +} + +.cs-label { + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--muted); + font-size: 12px; + margin: 0; +} + +.cs-subtle { + margin: 6px 0 0; + color: var(--muted); +} + +.cs-modal__actions { + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.cs-banner + .cs-grid { + margin-top: 8px; +} + +.cs-side-row strong { + font-size: 15px; +} + +.cs-side-row span { + color: var(--muted); +} + +.cs-actions button, +.cs-modal__actions button { + min-width: 160px; +} + +.cs-topbar__actions button { + min-width: 140px; +} + +.cs-preview-grid h4 { + margin: 4px 0 0; +} + +.cs-subtle + .cs-subtle { + margin-top: 2px; +} + +.cs-grid textarea { + resize: vertical; +} + +@media (max-width: 1080px) { + .cs-grid { + grid-template-columns: 1fr; + } + + .cs-card--map { + order: -1; + } +} + +@media (max-width: 720px) { + .cs-topbar { + flex-direction: column; + } + + .cs-topbar__actions { + width: 100%; + justify-content: flex-start; + } + + .cs-two-col, + .cs-three-col { + grid-template-columns: 1fr; + } + + .cs-actions, + .cs-modal__actions, + .cs-topbar__actions { + flex-direction: column; + } + + .cs-actions button, + .cs-modal__actions button, + .cs-topbar__actions button { + width: 100%; + } +} diff --git a/app-frontend/employer-panel/src/pages/ManageShift.js b/app-frontend/employer-panel/src/pages/ManageShift.js index 2f038e3c6..bbdc7fa68 100644 --- a/app-frontend/employer-panel/src/pages/ManageShift.js +++ b/app-frontend/employer-panel/src/pages/ManageShift.js @@ -591,36 +591,78 @@ const ManageShift = ({ language }) => {
- + {formErrors.title && {formErrors.title}}
- + {formErrors.date && {formErrors.date}}
- + {formErrors.startTime && {formErrors.startTime}}
- + {formErrors.endTime && {formErrors.endTime}}
- +
- + {formErrors.payRate && {formErrors.payRate}}
- +
@@ -640,16 +682,26 @@ const ManageShift = ({ language }) => { {formErrors.status && {formErrors.status}}
+ +
+ + + {selectedShift?.assignedGuards?.length > 0 ? ( + selectedShift.assignedGuards.map((guard) => ( +
+ {guard.name} ({guard.email}) +
+ )) + ) : ( +

No guards assigned

+ )} +
+
- {!isEditing ? ( - - ) : ( - <> - - - - )} -
+ +
)} @@ -1389,4 +1441,4 @@ const aiReasonStyle = { fontSize: '12px', color: '#4b5563', marginBottom: '2px', -}; \ No newline at end of file +}; From 141d826a52bc3676ff4fe92bd09d74446f985c63 Mon Sep 17 00:00:00 2001 From: Evangeline-d Date: Tue, 19 May 2026 02:08:27 +1000 Subject: [PATCH 5/5] refactor(shifts): clean read-only shift details view --- app-frontend/employer-panel/src/pages/ManageShift.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app-frontend/employer-panel/src/pages/ManageShift.js b/app-frontend/employer-panel/src/pages/ManageShift.js index bbdc7fa68..54f062261 100644 --- a/app-frontend/employer-panel/src/pages/ManageShift.js +++ b/app-frontend/employer-panel/src/pages/ManageShift.js @@ -674,7 +674,12 @@ const ManageShift = ({ language }) => {
- {editableStatuses.map((statusOption) => ( ))}