From 81d56e4be62a38c410b14bc57fd55ba1b4f83498 Mon Sep 17 00:00:00 2001 From: Evangeline-d Date: Fri, 27 Mar 2026 20:05:58 +1100 Subject: [PATCH 1/4] Fix guards page backend integration and error handling --- app-frontend/employer-panel/.env | 2 +- .../employer-panel/src/pages/GuardProfile.js | 34 ++++++++----------- docker-compose.yml | 4 +-- 3 files changed, 17 insertions(+), 23 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 701bd2dd5..4eb9c78e5 100644 --- a/app-frontend/employer-panel/src/pages/GuardProfile.js +++ b/app-frontend/employer-panel/src/pages/GuardProfile.js @@ -1,5 +1,6 @@ import React, { useState, useRef, useEffect } from "react"; import { useNavigate } from "react-router-dom"; +import http from "../lib/http"; // ❌ removed local dummy guardData @@ -43,21 +44,7 @@ function GuardProfiles() { 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 - - 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 @@ -82,9 +69,15 @@ function GuardProfiles() { })); // NEW if (mounted) setGuards(normalized); // NEW - } catch (e) { // NEW - if (mounted) setError(e.message || "Failed to fetch guards"); // NEW - } finally { // NEW + } 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"); + } +} finally { // NEW if (mounted) setLoading(false); // NEW } // NEW })(); // NEW @@ -162,7 +155,8 @@ function GuardProfiles() { {/* NEW: loading / error / empty states */} {loading &&

Loading guards…

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

Failed to load: {error}

/* NEW */ +

{error}

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

No guards found.

/* NEW */ @@ -240,4 +234,4 @@ function GuardProfiles() { ); } -export default GuardProfiles; \ No newline at end of file +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 643fe4054fe7c9d8ef4771b3f37a8ff6ef395002 Mon Sep 17 00:00:00 2001 From: Evangeline-d Date: Thu, 16 Apr 2026 19:06:43 +1000 Subject: [PATCH 2/4] 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 a2f31910384926f920753017d423f7e3247b8ade Mon Sep 17 00:00:00 2001 From: Evangeline-d Date: Sat, 9 May 2026 23:19:18 +1000 Subject: [PATCH 3/4] Implemented read-only shift schedule view --- .../employer-panel/src/pages/ManageShift.js | 2582 ++++++----------- 1 file changed, 908 insertions(+), 1674 deletions(-) diff --git a/app-frontend/employer-panel/src/pages/ManageShift.js b/app-frontend/employer-panel/src/pages/ManageShift.js index 2646dae4e..1da6558ee 100644 --- a/app-frontend/employer-panel/src/pages/ManageShift.js +++ b/app-frontend/employer-panel/src/pages/ManageShift.js @@ -1,1806 +1,1040 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import http from '../lib/http'; // Map backend status to filter display const statusDisplayMap = { - completed: 'Completed', - assigned: 'In Progress', - applied: 'Pending', - open: 'Open', + completed: "Completed", + inprogress: "In Progress", + pending: "Pending", + open: "Open", }; const Filter = Object.freeze({ - All: 'All', - Completed: 'Completed', - InProgress: 'In Progress', - Pending: 'Pending', - Open: 'Open', + All: 'All', + Completed: 'Completed', + InProgress: 'In Progress', + Pending: 'Pending', + Open: 'Open', }); -const editableStatuses = [Filter.Open, Filter.Pending, Filter.InProgress, Filter.Completed]; - const Sort = Object.freeze({ - DateAsc: 'Date (Asc)', - DateDesc: 'Date (Desc)', + DateAsc: 'Date (Asc)', + DateDesc: 'Date (Desc)', }); -const TABS = Object.freeze({ DETAILS: 'details', APPLICANTS: 'applicants' }); - // Normalize shift data from backend const normalizeShift = (s) => ({ - id: s._id, - title: s.title || '--', - date: s.date, - startTime: s.startTime, - endTime: s.endTime, - dateTime: s.date && s.startTime ? `${s.date} ${s.startTime}` : s.date || '', - locationLabel: s.location - ? [s.location.street, s.location.suburb, s.location.state].filter(Boolean).join(', ') - : '--', - location: s.location || {}, - status: statusDisplayMap[s.status?.toLowerCase()] || 'Open', - payRate: s.payRate ?? s.price ?? '--', - urgency: s.urgency || 'normal', - field: s.field || '', - applicantCount: s.applicantCount ?? (Array.isArray(s.applicants) ? s.applicants.length : 0), - applicants: Array.isArray(s.applicants) ? s.applicants : [], - assignedGuard: s.assignedGuard || null, + id: s._id, + title: s.title || "--", + date: s.date, + startTime: s.startTime, + endTime: s.endTime, + dateTime: s.date && s.startTime ? `${s.date} ${s.startTime}` : s.date || "", + locationLabel: s.location + ? [s.location.street, s.location.suburb, s.location.state].filter(Boolean).join(', ') + : "--", + location: s.location || {}, + status: statusDisplayMap[s.status?.toLowerCase()] || "Open", + payRate: s.payRate ?? s.price ?? "--", + urgency: s.urgency || 'normal', + field: s.field || '', + applicantCount: s.applicantCount ?? (Array.isArray(s.applicants) ? s.applicants.length : 0), }); const ManageShift = () => { - const navigate = useNavigate(); - const [shifts, setShifts] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [currentPage, setCurrentPage] = useState(1); - const [selectedFilter, setSelectedFilter] = useState(Filter.All); - const [sortBy, setSortBy] = useState(Sort.DateAsc); - const [showSortModal, setShowSortModal] = useState(false); - const [selectedShift, setSelectedShift] = useState(null); - const [detailForm, setDetailForm] = useState(null); - const [isEditing, setIsEditing] = useState(false); - const [saving, setSaving] = useState(false); - const [feedback, setFeedback] = useState(''); - const [formErrors, setFormErrors] = useState({}); - const [optimisticSnapshot, setOptimisticSnapshot] = useState(null); - const [activeTab, setActiveTab] = useState(TABS.DETAILS); - const [applicantAction, setApplicantAction] = useState({}); - const itemsPerPage = 9; - - // Chat state - const [chatShift, setChatShift] = useState(null); - const [messages, setMessages] = useState([]); - const [newMessage, setNewMessage] = useState(''); - const [sendingMsg, setSendingMsg] = useState(false); - const [loadingMessages, setLoadingMessages] = useState(false); - const chatEndRef = useRef(null); - - useEffect(() => { + const navigate = useNavigate(); + const [shifts, setShifts] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + const [selectedFilter, setSelectedFilter] = useState(Filter.All); + const [sortBy, setSortBy] = useState(Sort.DateAsc); + const [showSortModal, setShowSortModal] = useState(false); + const [selectedShift, setSelectedShift] = useState(null); + const [detailForm, setDetailForm] = useState(null); + const [isEditing, setIsEditing] = useState(false); + const [saving, setSaving] = useState(false); + const [feedback, setFeedback] = useState(''); + const [formErrors, setFormErrors] = useState({}); + const [optimisticSnapshot, setOptimisticSnapshot] = useState(null); + const itemsPerPage = 8; + + useEffect(() => { const fetchShifts = async () => { - try { - const { data } = await http.get('/shifts'); - let apiShifts; - if (Array.isArray(data)) { - apiShifts = data; - } else if (Array.isArray(data.shifts)) { - apiShifts = data.shifts; - } else if (data.items && Array.isArray(data.items)) { - apiShifts = data.items; - } else { - apiShifts = []; + try { + const { data } = await http.get('/shifts'); + let apiShifts; + if (Array.isArray(data)) { + apiShifts = data; + } else if (Array.isArray(data.shifts)) { + apiShifts = data.shifts; + } else if (data.items && Array.isArray(data.items)) { + apiShifts = data.items; + } else { + apiShifts = []; + } + setShifts(apiShifts.map(normalizeShift)); + } catch (err) { + const message = err?.response?.data?.message || 'Error fetching shifts.'; + setError(message); + } finally { + setLoading(false); } - setShifts(apiShifts.map(normalizeShift)); - } catch (err) { - setError(err?.response?.data?.message || 'Error fetching shifts.'); - } finally { - setLoading(false); - } }; fetchShifts(); - }, []); - - useEffect(() => { - chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [messages]); - - // Chat handlers - const openChatModal = async (shift) => { - setChatShift(shift); - setMessages([]); - setNewMessage(''); - setLoadingMessages(true); - try { - const guardId = shift.assignedGuard?._id || shift.assignedGuard; - const { data } = await http.get(`/messages/conversation/${guardId}`); - setMessages(data.data?.conversation?.messages || []); - } catch (err) { - console.error('Failed to load messages', err); - } finally { - setLoadingMessages(false); - } - }; - - const closeChatModal = () => { - setChatShift(null); - setMessages([]); - setNewMessage(''); - }; - - const sendMessage = async () => { - if (!newMessage.trim() || !chatShift || sendingMsg) return; - setSendingMsg(true); - try { - const guardId = chatShift.assignedGuard?._id || chatShift.assignedGuard; - const { data } = await http.post(`/messages`, { - receiverId: guardId, - content: newMessage, - }); - setMessages((prev) => [ - ...prev, - { - _id: data.data?.messageId, - content: data.data?.content || newMessage, - sender: { email: localStorage.getItem('userEmail') }, - isOwn: true, - timestamp: data.data?.timestamp || new Date().toISOString(), - }, - ]); - setNewMessage(''); - } catch (err) { - console.error('Failed to send message', err); - } finally { - setSendingMsg(false); - } - }; - - const handleChatKeyDown = (e) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - sendMessage(); - } - }; - - // Filter / sort / pagination - - const filteredShifts = - selectedFilter === Filter.All - ? shifts - : shifts.filter((shift) => shift.status === selectedFilter); - - const sortedShifts = [...filteredShifts].sort((a, b) => { - const keyA = (a.date || '') + ' ' + (a.startTime || ''); - const keyB = (b.date || '') + ' ' + (b.startTime || ''); - if (keyA !== keyB) - return sortBy === Sort.DateAsc ? (keyA < keyB ? -1 : 1) : keyA > keyB ? -1 : 1; - const endA = a.endTime || ''; - const endB = b.endTime || ''; - if (endA === endB) return 0; - return sortBy === Sort.DateAsc ? (endA < endB ? -1 : 1) : endA > endB ? -1 : 1; - }); - - const totalPages = Math.ceil(sortedShifts.length / itemsPerPage); - - useEffect(() => { - if (totalPages === 0) { - if (currentPage !== 1) setCurrentPage(1); - return; - } - if (currentPage > totalPages) setCurrentPage(totalPages); - else if (currentPage < 1) setCurrentPage(1); - }, [totalPages, currentPage]); - - const indexStart = (currentPage - 1) * itemsPerPage; - const currentItems = sortedShifts.slice(indexStart, indexStart + itemsPerPage); - - const totalShifts = shifts.length; - const completedShifts = shifts.filter((s) => s.status === 'Completed').length; - const inProgressShifts = shifts.filter((s) => s.status === 'In Progress').length; - const pendingShifts = shifts.filter((s) => s.status === 'Pending').length; - - const goPrevPage = () => currentPage > 1 && setCurrentPage(currentPage - 1); - const goNextPage = () => currentPage < totalPages && setCurrentPage(currentPage + 1); - const goToPage = (page) => setCurrentPage(page); - - const getPaginationNumbers = () => { - const pages = []; - const maxVisiblePages = 5; - if (totalPages <= maxVisiblePages) { - for (let i = 1; i <= totalPages; i++) pages.push(i); - } else { - pages.push(1); - if (currentPage > 3) pages.push('...'); - const start = Math.max(2, currentPage - 1); - const end = Math.min(totalPages - 1, currentPage + 1); - for (let i = start; i <= end; i++) pages.push(i); - if (currentPage < totalPages - 2) pages.push('...'); - if (totalPages > 1) pages.push(totalPages); - } - return pages; - }; - - const selectSortBy = (sortOption) => { - setSortBy(sortOption); - setShowSortModal(false); - }; - - const formatDate = (dateString) => { - if (!dateString) return '--'; - const date = new Date(dateString); - if (isNaN(date)) return '--'; - return date.toLocaleDateString('en-US', { day: '2-digit', month: 'short', year: 'numeric' }); - }; - - const formatTimeRange = (start, end) => { - if (!start || !end) return '--'; - const [sh, sm] = start.split(':').map(Number); - const [eh, em] = end.split(':').map(Number); - if ([sh, sm, eh, em].some((n) => Number.isNaN(n))) return '--'; - return `${String(sh).padStart(2, '0')}:${String(sm).padStart(2, '0')} - ${String(eh).padStart(2, '0')}:${String(em).padStart(2, '0')}`; - }; - - // Detail modal handlers - const openShiftModal = (shift) => { - setSelectedShift(shift); - setDetailForm({ - title: shift.title || '', - date: shift.date ? shift.date.substring(0, 10) : '', - startTime: shift.startTime || '', - endTime: shift.endTime || '', - payRate: shift.payRate === '--' ? '' : (shift.payRate ?? ''), - street: shift.location?.street || '', - suburb: shift.location?.suburb || '', - state: shift.location?.state || '', - postcode: shift.location?.postcode || '', - field: shift.field || '', - urgency: shift.urgency || 'normal', - status: shift.status || Filter.Open, +}, []); + + + // Map frontend filter values to backend status + const filterToBackendStatus = { + Completed: "Completed", + InProgress: "In Progress", + Pending: "Pending", + Open: "Open", + }; + + const filteredShifts = selectedFilter === Filter.All + ? shifts + : shifts.filter(shift => shift.status === filterToBackendStatus[selectedFilter]); + + const sortedShifts = [...filteredShifts].sort((a, b) => { + const dateA = new Date(a?.dateTime || 0); + const dateB = new Date(b?.dateTime || 0); + return sortBy === Sort.DateAsc ? dateA - dateB : dateB - dateA; }); - setIsEditing(false); - setFeedback(''); - // Default to Applicants tab for Open shifts, Details for everything else - setActiveTab(shift.status === Filter.Open ? TABS.APPLICANTS : TABS.DETAILS); - setApplicantAction({}); - }; - - const closeShiftModal = () => { - setSelectedShift(null); - setDetailForm(null); - setIsEditing(false); - setSaving(false); - setFeedback(''); - setApplicantAction({}); - }; - - const handleDetailChange = (e) => { - const { name, value } = e.target; - setDetailForm((prev) => ({ ...prev, [name]: value })); - }; - - const validateDetailForm = () => { - const errs = {}; - if (!detailForm.title?.trim()) errs.title = 'Title required'; - if (!detailForm.date?.trim()) errs.date = 'Date required'; - if (!detailForm.startTime?.trim()) errs.startTime = 'Start time required'; - if (!detailForm.endTime?.trim()) errs.endTime = 'End time required'; - if (detailForm.payRate !== '' && Number(detailForm.payRate) < 0) - errs.payRate = 'Pay rate must be positive'; - if (!editableStatuses.includes(detailForm.status)) errs.status = 'Please select a valid status'; - setFormErrors(errs); - return Object.keys(errs).length === 0; - }; - - const handleSaveShift = async () => { - if (!selectedShift || !detailForm) return; - if (selectedShift.status === Filter.Completed) { - setFeedback('Completed shifts cannot be edited.'); - return; - } - if (!validateDetailForm()) return; - setSaving(true); - setFeedback(''); - try { - const cleanedLocation = { - street: detailForm.street?.trim() || undefined, - suburb: detailForm.suburb?.trim() || undefined, - state: detailForm.state?.trim() || undefined, - postcode: detailForm.postcode?.trim() || undefined, - }; - const hasLocation = Object.values(cleanedLocation).some(Boolean); - const payload = { - title: detailForm.title, - date: detailForm.date, - startTime: detailForm.startTime, - endTime: detailForm.endTime, - payRate: detailForm.payRate === '' ? undefined : Number(detailForm.payRate), - ...(detailForm.field?.trim() ? { field: detailForm.field.trim() } : {}), - urgency: detailForm.urgency, - ...(hasLocation ? { location: cleanedLocation } : {}), - }; - setOptimisticSnapshot({ shifts, selectedShift }); - const optimistic = { ...selectedShift, ...payload, status: detailForm.status }; - setShifts((prev) => - prev.map((s) => (s.id === selectedShift.id ? { ...s, ...optimistic } : s)) - ); - const { data } = await http.patch(`/shifts/${selectedShift.id}`, payload); - const updated = normalizeShift(data.shift || { ...selectedShift, ...payload }); - const updatedWithUiStatus = { ...updated, status: detailForm.status }; - setShifts((prev) => - prev.map((s) => (s.id === updatedWithUiStatus.id ? { ...s, ...updatedWithUiStatus } : s)) - ); - setSelectedShift(updatedWithUiStatus); - setDetailForm({ - title: updatedWithUiStatus.title || '', - date: updatedWithUiStatus.date ? updatedWithUiStatus.date.substring(0, 10) : '', - startTime: updatedWithUiStatus.startTime || '', - endTime: updatedWithUiStatus.endTime || '', - payRate: updatedWithUiStatus.payRate === '--' ? '' : (updatedWithUiStatus.payRate ?? ''), - street: updatedWithUiStatus.location?.street || '', - suburb: updatedWithUiStatus.location?.suburb || '', - state: updatedWithUiStatus.location?.state || '', - postcode: updatedWithUiStatus.location?.postcode || '', - field: updatedWithUiStatus.field || '', - urgency: updatedWithUiStatus.urgency || 'normal', - status: updatedWithUiStatus.status || Filter.Open, - }); - setIsEditing(false); - setFeedback('Saved successfully'); - } catch (err) { - const message = err?.response?.data?.message || 'Failed to update shift'; - setFeedback(message); - if (optimisticSnapshot) { - setShifts(optimisticSnapshot.shifts); - setSelectedShift(optimisticSnapshot.selectedShift); - } - } finally { - setSaving(false); - } - }; - - // Approval workflow - - const handleApproveGuard = async (guardId) => { - if (!selectedShift) return; - setApplicantAction((prev) => ({ ...prev, [guardId]: 'approving' })); - try { - const { data } = await http.put(`/shifts/${selectedShift.id}/approve`, { guardId }); - const updatedShift = normalizeShift( - data.shift || { ...selectedShift, status: 'assigned', assignedGuard: guardId } - ); - setShifts((prev) => prev.map((s) => (s.id === updatedShift.id ? updatedShift : s))); - setSelectedShift(updatedShift); - setApplicantAction((prev) => ({ ...prev, [guardId]: 'approved' })); - setFeedback('Guard approved. Shift is now In Progress.'); - } catch (err) { - setFeedback(err?.response?.data?.message || 'Failed to approve guard'); - setApplicantAction((prev) => ({ ...prev, [guardId]: undefined })); - } - }; - - const handleRejectGuard = async (guardId) => { - if (!selectedShift) return; - setApplicantAction((prev) => ({ ...prev, [guardId]: 'rejecting' })); - try { - await http.put(`/shifts/${selectedShift.id}/reject`, { guardId }); - const updatedApplicants = selectedShift.applicants.filter((a) => (a._id || a.id) !== guardId); - const updatedShift = { - ...selectedShift, - applicants: updatedApplicants, - applicantCount: updatedApplicants.length, - }; - setShifts((prev) => prev.map((s) => (s.id === updatedShift.id ? updatedShift : s))); - setSelectedShift(updatedShift); - setApplicantAction((prev) => { - const n = { ...prev }; - delete n[guardId]; - return n; - }); - } catch (err) { - setFeedback(err?.response?.data?.message || 'Failed to reject guard'); - setApplicantAction((prev) => ({ ...prev, [guardId]: undefined })); - } - }; - - // Only show Applicants tab for Open shifts - const showApplicantsTab = selectedShift?.status === Filter.Open; - - return ( -
-
-

Manage Shifts

- -
-
- - - - -
- { - setSelectedFilter(filter); - setCurrentPage(1); - }} - sortBy={sortBy} - setShowSortModal={setShowSortModal} - /> - {loading &&

Loading shifts...

} - {error &&

{error}

} - {!loading && !error && currentItems.length === 0 &&

No shifts found.

} -
- {currentItems.map((shift) => { - const [datePart] = shift.dateTime?.split(' ') || [null]; - return ( -
-
-

{shift.title}

-
-
{shift.status}
-
- {shift.payRate !== '--' ? `$${shift.payRate}` : '--'} -
-
-
-
-
- Location - {shift.locationLabel} -
-
-
- Date - {formatDate(datePart)} -
-
- Time - - {formatTimeRange(shift.startTime, shift.endTime)} - -
-
- {/* Applicant badge — only on Open shifts */} - {shift.status === Filter.Open && shift.applicantCount > 0 && ( -
- - {shift.applicantCount} applicant{shift.applicantCount !== 1 ? 's' : ''} pending - review -
- )} -
- - {/* Chat icon — only on In Progress shifts */} - {shift.status === 'In Progress' && ( - - )} -
-
-
- ); - })} -
- {!loading && !error && totalPages > 1 && ( - - )} - {showSortModal && ( - - )} - - {/* ─── Shift Detail Modal ─── */} - {selectedShift && detailForm && ( -
{ - if (e.target === e.currentTarget) closeShiftModal(); - }} - > -
e.stopPropagation()} - onClick={(e) => e.stopPropagation()} - > -
-
-

Secure Shift

-

- {activeTab === TABS.APPLICANTS - ? 'Applicants' - : isEditing - ? 'Edit Shift' - : 'Shift Details'} -

-

- {activeTab === TABS.APPLICANTS - ? `${selectedShift.applicants?.length ?? 0} applicant(s) for this shift.` - : 'Review and update shift fields.'} -

-
- -
- {/* Tab bar — Applicants tab only for Open shifts */} -
- - {showApplicantsTab && ( - - )} -
+ const totalPages = Math.ceil(sortedShifts.length / itemsPerPage); + const indexStart = (currentPage - 1) * itemsPerPage; + const currentItems = sortedShifts.slice(indexStart, indexStart + itemsPerPage); - {feedback && ( -
- {feedback} -
- )} + const totalShifts = shifts.length; + const completedShifts = shifts.filter(s => s.status === "Completed").length; + const inProgressShifts = shifts.filter(s => s.status === "In Progress").length; + const pendingShifts = shifts.filter(s => s.status === "Pending").length; - {/* ── Details tab ── */} - {activeTab === TABS.DETAILS && ( - <> -
-
- - - {formErrors.title && {formErrors.title}} -
-
- - - {formErrors.date && {formErrors.date}} -
-
- - - {formErrors.startTime && ( - {formErrors.startTime} - )} -
-
- - - {formErrors.endTime && {formErrors.endTime}} -
-
- - -
-
- - - {formErrors.payRate && {formErrors.payRate}} -
-
- - -
-
- - -
-
- - - {formErrors.status && {formErrors.status}} -
-
-
- {!isEditing ? ( - - ) : ( - <> - - - - )} -
- - )} + const goPrevPage = () => currentPage > 1 && setCurrentPage(currentPage - 1); + const goNextPage = () => currentPage < totalPages && setCurrentPage(currentPage + 1); + const goToPage = (page) => setCurrentPage(page); - {/* ── Applicants tab — only for Open shifts ── */} - {activeTab === TABS.APPLICANTS && showApplicantsTab && ( - - )} -
-
- )} - - {/* ─── Chat Modal ─── */} - {chatShift && ( -
-
e.stopPropagation()}> - {/* Header */} -
-
-
- SS { - e.target.style.display = 'none'; - }} - /> -
-
-

SECURE SHIFT

-

Shift Chat

-
-
- -
+ const getPaginationNumbers = () => { + const pages = []; + const maxVisiblePages = 5; + if (totalPages <= maxVisiblePages) { + for (let i = 1; i <= totalPages; i++) pages.push(i); + } else { + pages.push(1); + if (currentPage > 3) pages.push('...'); + const start = Math.max(2, currentPage - 1); + const end = Math.min(totalPages - 1, currentPage + 1); + for (let i = start; i <= end; i++) pages.push(i); + if (currentPage < totalPages - 2) pages.push('...'); + if (totalPages > 1) pages.push(totalPages); + } + return pages; + }; - {/* Shift info pills */} -
- - - {chatShift.title} - - - 📍 {chatShift.locationLabel !== '--' ? chatShift.locationLabel : 'Location TBD'} - - - 🕐 {formatTimeRange(chatShift.startTime, chatShift.endTime)} - -
+ const selectSortBy = (sortOption) => { + setSortBy(sortOption); + setShowSortModal(false); + }; - {/* Guard name */} - {chatShift.assignedGuard && ( -
- • {chatShift.assignedGuard?.name || 'Assigned Guard'} -
- )} + const formatDate = (dateString) => { + if (!dateString) return "--"; + const date = new Date(dateString); + if (isNaN(date)) return "--"; + return date.toLocaleDateString('en-US', { day: '2-digit', month: 'short', year: 'numeric' }); + }; - {/* Messages */} -
- {loadingMessages ? ( -
-

Loading messages...

-
- ) : messages.length === 0 ? ( -
-
- - - -
-

- No messages yet -

-

- Send a message to start the conversation -

-
- ) : ( - messages.map((msg, i) => { - const currentUserEmail = localStorage.getItem('userEmail'); - const isOwn = msg.isOwn || msg.sender?.email === currentUserEmail; - return ( -
- {!isOwn && ( - - {msg.senderName || msg.sender?.email || 'Guard'} - - )} -
- {msg.content} -
- {msg.timestamp && ( - - {new Date(msg.timestamp).toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - })} - - )} -
- ); - }) - )} -
-
+ const formatTimeRange = (start, end) => { + if (!start || !end) return "--"; + const [sh, sm] = start.split(":").map(Number); + const [eh, em] = end.split(":").map(Number); + if ([sh, sm, eh, em].some((n) => Number.isNaN(n))) return "--"; + return `${String(sh).padStart(2, '0')}:${String(sm).padStart(2, '0')} - ${String(eh).padStart(2, '0')}:${String(em).padStart(2, '0')}`; + }; - {/* Input */} -
-
- setNewMessage(e.target.value)} - onKeyDown={handleChatKeyDown} - placeholder="Type a message..." - style={chatInputStyle} - /> - -
-

Messages are visible to all parties on this shift

-
-
- )} -
- ); -}; +
+ + + + +
+ + {loading &&

Loading shifts...

} + {error &&

{error}

} + {!loading && !error && currentItems.length === 0 &&

No shifts found.

} +
+ {currentItems.map((shift) => { + const [datePart, timePart] = shift.dateTime?.split(' ') || [null, null]; + return ( +
+
+

{shift.title}

+
+
{shift.status}
+
{shift.payRate !== "--" ? `$${shift.payRate}` : '--'}
+
+
+
+
+ Location + {shift.locationLabel} +
+
+
+ Date + {formatDate(datePart)} +
+
+ Time + {formatTimeRange(shift.startTime, shift.endTime)} +
+
+ +
+
+ ); + })} +
+ {!loading && !error && totalPages > 1 && ( + + )} + {showSortModal && ( + + )} + {selectedShift && detailForm && ( +
+
e.stopPropagation()}> +
+
+

Secure Shift

+

{isEditing ? 'Edit Shift' : 'Shift Schedule View'}

+

Review and update shift fields. All fields are required.

+
+ +
-// Chat Icon SVG -const ChatIcon = () => ( - - - -); + {feedback &&
{feedback}
} -// Applicants Panel -const ApplicantsPanel = ({ shift, applicantAction, onApprove, onReject }) => { - const applicants = shift.applicants || []; +
+
+ + + {formErrors.title && {formErrors.title}} +
+
+ + + {formErrors.date && {formErrors.date}} +
+
+ + + {formErrors.startTime && {formErrors.startTime}} +
+
+ + + {formErrors.endTime && {formErrors.endTime}} +
+
+ + +
+
+ + + {formErrors.payRate && {formErrors.payRate}} +
+
+ + +
+
+ + +
+
- if (applicants.length === 0) { - return ( -
-
👥
-

No applicants yet

-

- Guards who apply for this shift will appear here. -

-
+
+ +
+ +
+ + + {selectedShift.createdBy ? ( +
    +
  • {selectedShift.createdBy.name}
  • +
+) : ( +

No guards assigned

+)} + +
+ + +
+
+ )} +
); - } - - return ( -
-
- {applicants.map((applicant) => { - const gid = applicant._id || applicant.id; - const action = applicantAction[gid]; - const isApproved = action === 'approved'; - const isRejected = action === 'rejected'; - - return ( -
-
- {(applicant.name || applicant.email || 'G').charAt(0).toUpperCase()} -
-
-

{applicant.name || 'Unknown Guard'}

-

{applicant.email || '--'}

- {applicant.licenseType && ( - {applicant.licenseType} - )} -
-
- {isApproved ? ( - ✓ Approved - ) : isRejected ? ( - ✗ Rejected - ) : ( - <> - - - - )} -
-
- ); - })} -
-
- ); }; -// Sub-components const SummaryCard = ({ label, number, icon, bg }) => ( -
-
-

{label}

-

{number}

-
-
- {label} +
+
+

{label}

+

{number}

+
+
{label}
-
); -const FilterSortSection = ({ - Filter, - selectedFilter, - onFilterChange, - sortBy, - setShowSortModal, -}) => ( -
-
- Filter - Filter by: -
- {Object.values(Filter).map((f) => ( - - ))} -
-
-
- Sort - Sort by: - +const FilterSortSection = ({ Filter, selectedFilter, setSelectedFilter, sortBy, setShowSortModal }) => ( +
+
+ Filter + Filter by: +
+ {Object.values(Filter).map(f => ( + + ))} +
+
+
+ Sort + Sort by: + +
-
); -const Pagination = ({ - totalPages, - currentPage, - goPrevPage, - goNextPage, - goToPage, - getPaginationNumbers, -}) => ( -
- - {getPaginationNumbers().map((page, index) => ( - - ))} - -
+const Pagination = ({ totalPages, currentPage, goPrevPage, goNextPage, goToPage, getPaginationNumbers }) => ( +
+ + {getPaginationNumbers().map((page, index) => ( + + ))} + +
); const SortModal = ({ Sort, sortBy, selectSortBy, setShowSortModal }) => ( -
setShowSortModal(false)}> -
e.stopPropagation()}> -
-

Sort by

- -
-
- {Object.values(Sort).map((option) => ( - - ))} -
+
setShowSortModal(false)}> +
e.stopPropagation()}> +
+

Sort by

+ +
+
+ {Object.values(Sort).map(option => ( + + ))} +
+
-
); export default ManageShift; -// Styles + + +// Status tag styles const getStatusTagStyle = (status) => ({ - padding: '4px 12px', - borderRadius: '16px', - fontSize: '12px', - fontWeight: '600', - display: 'inline-block', - color: - status === 'Completed' - ? '#2E7D32' - : status === 'In Progress' - ? '#7B1FA2' - : status === 'Pending' - ? '#F57C00' - : status === 'Open' - ? '#1565C0' - : '#757575', - backgroundColor: - status === 'Completed' - ? '#EAFAE7' - : status === 'In Progress' - ? '#F6EFFF' - : status === 'Pending' - ? '#FBFAE2' - : status === 'Open' - ? '#E3F2FD' - : '#F5F5F5', + padding: '4px 12px', + borderRadius: '16px', + fontSize: '12px', + fontWeight: '600', + display: 'inline-block', + color: + status === "Completed" ? "#2E7D32" : + status === "In Progress" ? "#7B1FA2" : + status === "Pending" ? "#F57C00" : + status === "Open" ? "#1565C0" : + "#757575", + backgroundColor: + status === "Completed" ? "#EAFAE7" : + status === "In Progress" ? "#F6EFFF" : + status === "Pending" ? "#FBFAE2" : + status === "Open" ? "#E3F2FD" : + "#F5F5F5", }); +// Container styles const containerStyle = { - padding: '40px', - minHeight: '100vh', - maxWidth: '1200px', - margin: '0 auto', + padding: '40px', + minHeight: '100vh', + maxWidth: '1200px', + margin: '0 auto', }; + const headerStyle = { - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: '24px', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: '24px', +}; + +const titleStyle = { + fontSize: '28px', + fontWeight: '700', + color: '#1a1a1a', + margin: '0', }; -const titleStyle = { fontSize: '28px', fontWeight: '700', color: '#1a1a1a', margin: '0' }; + const addButtonStyle = { - backgroundColor: '#274b93', - color: 'white', - border: 'none', - borderRadius: '12px', - padding: '10px 16px', - fontSize: '14px', - fontWeight: '600', - cursor: 'pointer', - display: 'flex', - alignItems: 'center', - gap: '8px', - boxShadow: '0 2px 4px rgba(39, 75, 147, 0.2)', + backgroundColor: '#274b93', + color: 'white', + border: 'none', + borderRadius: '12px', + padding: '10px 16px', + fontSize: '14px', + fontWeight: '600', + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + gap: '8px', + boxShadow: '0 2px 4px rgba(39, 75, 147, 0.2)', }; + +// Summary cards styles const summaryGridStyle = { - display: 'grid', - gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', - gap: '16px', - marginBottom: '24px', + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', + gap: '16px', + marginBottom: '24px', }; + const summaryCardStyle = { - borderRadius: '12px', - padding: '20px 30px', - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', + borderRadius: '12px', + padding: '20px 30px', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', }; + const summaryLabelStyle = { - margin: '0 0 8px 0', - fontSize: '16px', - color: '#1E1E1E', - fontWeight: '400', + margin: '0 0 8px 0', + fontSize: '16px', + color: '#1E1E1E', + fontWeight: '400', +}; + +const summaryNumberStyle = { + margin: '0', + fontSize: '24px', + fontWeight: '700', + color: '#1E1E1E', +}; + +const bigIconStyle = { + width: '24px', + height: '24px', +}; + +const smallIconStyle = { + width: '20px', + height: '20px', }; -const summaryNumberStyle = { margin: '0', fontSize: '24px', fontWeight: '700', color: '#1E1E1E' }; -const bigIconStyle = { width: '24px', height: '24px' }; -const smallIconStyle = { width: '20px', height: '20px' }; + +// Filter section styles const filterSectionStyle = { - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: '24px', - flexWrap: 'wrap', - gap: '16px', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: '24px', + flexWrap: 'wrap', + gap: '16px', +}; + +const filterGroupStyle = { + display: 'flex', + alignItems: 'center', + gap: '12px', +}; + +const sortGroupStyle = { + display: 'flex', + alignItems: 'center', + gap: '12px', +}; + +const filterLabelStyle = { + fontSize: '14px', + fontWeight: '400', + color: '#1E1E1E', }; -const filterGroupStyle = { display: 'flex', alignItems: 'center', gap: '12px' }; -const sortGroupStyle = { display: 'flex', alignItems: 'center', gap: '12px' }; -const filterLabelStyle = { fontSize: '14px', fontWeight: '400', color: '#1E1E1E' }; -const filterButtonsStyle = { display: 'flex', gap: '8px' }; + +const filterButtonsStyle = { + display: 'flex', + gap: '8px', +}; + const filterButtonStyle = { - backgroundColor: 'white', - border: '1px solid #e0e0e0', - borderRadius: '12px', - padding: '8px 16px', - fontSize: '14px', - color: '#666', - cursor: 'pointer', - fontWeight: '500', + backgroundColor: 'white', + border: '1px solid #e0e0e0', + borderRadius: '12px', + padding: '8px 16px', + fontSize: '14px', + color: '#666', + cursor: 'pointer', + fontWeight: '500', }; + const activeFilterButtonStyle = { - ...filterButtonStyle, - backgroundColor: '#274b93', - color: 'white', - border: '1px solid #274b93', + ...filterButtonStyle, + backgroundColor: '#274b93', + color: 'white', + border: '1px solid #274b93', }; + const sortButtonStyle = { - backgroundColor: 'white', - border: '1px solid #e0e0e0', - borderRadius: '12px', - padding: '8px 16px', - fontSize: '14px', - color: '#666', - cursor: 'pointer', - display: 'flex', - alignItems: 'center', - gap: '4px', + backgroundColor: 'white', + border: '1px solid #e0e0e0', + borderRadius: '12px', + padding: '8px 16px', + fontSize: '14px', + color: '#666', + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + gap: '4px', }; + +// Grid and card styles const gridStyle = { - display: 'grid', - gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', - gap: '20px', - marginBottom: '32px', + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', + gap: '20px', + marginBottom: '32px', }; + const cardStyle = { - backgroundColor: 'white', - borderRadius: '12px', - padding: '20px', - boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)', - display: 'flex', - flexDirection: 'column', - justifyContent: 'space-between', - gap: '20px', + backgroundColor: 'white', + borderRadius: '12px', + padding: '20px', + boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)', + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + gap: '20px', }; + const cardHeaderStyle = { - display: 'flex', - justifyContent: 'space-between', - alignItems: 'flex-start', - marginTop: '12px', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginTop: '12px', }; + const cardTitleStyle = { - margin: '0 0 4px 0', - fontSize: '18px', - fontWeight: '600', - color: '#1E1E1E', + margin: '0 0 4px 0', + fontSize: '18px', + fontWeight: '600', + color: '#1E1E1E', }; -const priceStyle = { fontSize: '16px', fontWeight: '600', color: '#2E7D32' }; -const cardDetailsStyle = { display: 'flex', flexDirection: 'column', gap: '8px' }; -const detailRowStyle = { display: 'flex', alignItems: 'center', gap: '8px' }; -const detailTextStyle = { fontSize: '14px', color: '#1E1E1E', fontWeight: '400' }; -const cardActionsRowStyle = { display: 'flex', gap: '8px', marginTop: '8px', alignItems: 'center' }; -const viewDetailsButtonStyle = { - flex: 1, - backgroundColor: '#274b93', - color: 'white', - border: 'none', - borderRadius: '12px', - padding: '12px', - fontSize: '14px', - fontWeight: '600', - cursor: 'pointer', + +const priceStyle = { + fontSize: '16px', + fontWeight: '600', + color: '#2E7D32', }; -const chatIconButtonStyle = { - width: '44px', - height: '44px', - backgroundColor: '#f3f4f6', - border: '1px solid #e5e7eb', - borderRadius: '12px', - cursor: 'pointer', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - color: '#374151', - flexShrink: 0, + +const cardDetailsStyle = { + display: 'flex', + flexDirection: 'column', + gap: '8px', }; -const applicantBadgeStyle = { - display: 'flex', - alignItems: 'center', - gap: '6px', - fontSize: '12px', - color: '#274b93', - fontWeight: 500, - background: '#EFF4FF', - borderRadius: '8px', - padding: '4px 10px', + +const detailRowStyle = { + display: 'flex', + alignItems: 'center', + gap: '8px', }; -const applicantDotStyle = { - width: '6px', - height: '6px', - borderRadius: '50%', - background: '#274b93', - display: 'inline-block', + +const detailTextStyle = { + fontSize: '14px', + color: '#1E1E1E', + fontWeight: '400', }; + +const viewDetailsButtonStyle = { + backgroundColor: '#274b93', + color: 'white', + border: 'none', + borderRadius: '12px', + padding: '12px', + fontSize: '14px', + fontWeight: '600', + cursor: 'pointer', + marginTop: '8px', +}; + +// Pagination styles const paginationStyle = { - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - gap: '8px', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + gap: '8px', }; + const paginationButtonStyle = { - width: '32px', - height: '32px', - backgroundColor: 'white', - border: 'none', - borderRadius: '16px', - fontSize: '14px', - fontWeight: '500', - color: '#1E1E1E', - cursor: 'pointer', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', + width: '32px', + height: '32px', + backgroundColor: 'white', + border: 'none', + borderRadius: '16px', + fontSize: '14px', + fontWeight: '500', + color: '#1E1E1E', + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', }; + const activePaginationButtonStyle = { - ...paginationButtonStyle, - backgroundColor: '#274b93', - color: 'white', - fontWeight: '600', + ...paginationButtonStyle, + backgroundColor: '#274b93', + color: 'white', + fontWeight: '600', +}; + +const disabledPaginationButtonStyle = { + ...paginationButtonStyle, + cursor: 'not-allowed', }; -const disabledPaginationButtonStyle = { ...paginationButtonStyle, cursor: 'not-allowed' }; + +// Modal styles const modalOverlayStyle = { - position: 'fixed', - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: 'rgba(0, 0, 0, 0.5)', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - zIndex: 1000, + position: 'fixed', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + zIndex: 1000, }; + const modalContentStyle = { - backgroundColor: 'white', - borderRadius: '12px', - padding: '0', - maxWidth: '400px', - width: '90%', - boxShadow: '0 4px 20px rgba(0, 0, 0, 0.15)', + backgroundColor: 'white', + borderRadius: '12px', + padding: '0', + maxWidth: '400px', + width: '90%', + boxShadow: '0 4px 20px rgba(0, 0, 0, 0.15)', }; + const modalHeaderStyle = { - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - padding: '20px 24px', - borderBottom: '1px solid #e0e0e0', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '20px 24px', + borderBottom: '1px solid #e0e0e0', }; -const modalTitleStyle = { margin: 0, fontSize: '18px', fontWeight: '600', color: '#1E1E1E' }; + +const modalTitleStyle = { + margin: 0, + fontSize: '18px', + fontWeight: '600', + color: '#1E1E1E', +}; + const closeButtonStyle = { - backgroundColor: 'transparent', - border: 'none', - fontSize: '24px', - color: '#666', - cursor: 'pointer', - padding: '0', - width: '24px', - height: '24px', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', + backgroundColor: 'transparent', + border: 'none', + fontSize: '24px', + color: '#666', + cursor: 'pointer', + padding: '0', + width: '24px', + height: '24px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', }; -const modalBodyStyle = { padding: '16px 0' }; + +const modalBodyStyle = { + padding: '16px 0', +}; + const sortOptionStyle = { - width: '100%', - backgroundColor: 'transparent', - border: 'none', - padding: '12px 24px', - fontSize: '16px', - color: '#1E1E1E', - cursor: 'pointer', - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - textAlign: 'left', + width: '100%', + backgroundColor: 'transparent', + border: 'none', + padding: '12px 24px', + fontSize: '16px', + color: '#1E1E1E', + cursor: 'pointer', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + textAlign: 'left', }; + const activeSortOptionStyle = { - ...sortOptionStyle, - backgroundColor: '#EFF4FF', - color: '#274b93', - fontWeight: '600', + ...sortOptionStyle, + backgroundColor: '#EFF4FF', + color: '#274b93', + fontWeight: '600', }; -const checkmarkStyle = { color: '#274b93', fontWeight: 'bold' }; + +const checkmarkStyle = { + color: '#274b93', + fontWeight: 'bold', +}; + +// Detail modal styles (aligned to create shift design) const detailModalOverlay = { - position: 'fixed', - inset: 0, - background: 'rgba(0,0,0,0.45)', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - zIndex: 1100, - padding: '20px', + position: 'fixed', + inset: 0, + background: 'rgba(0,0,0,0.45)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + zIndex: 1100, + padding: '20px', }; + const detailModalContent = { - background: '#fff', - borderRadius: '14px', - width: 'min(960px, 100%)', - padding: '28px 32px 32px', - boxShadow: '0 20px 60px rgba(0,0,0,0.18)', - fontFamily: 'Poppins, sans-serif', + background: '#fff', + borderRadius: '14px', + width: 'min(960px, 100%)', + padding: '28px 32px 32px', + boxShadow: '0 20px 60px rgba(0,0,0,0.18)', + fontFamily: 'Poppins, sans-serif', }; + const detailModalHeader = { - display: 'flex', - justifyContent: 'space-between', - gap: '16px', - alignItems: 'flex-start', - marginBottom: '12px', + display: 'flex', + justifyContent: 'space-between', + gap: '16px', + alignItems: 'flex-start', + marginBottom: '12px', }; + const detailModalOverline = { - margin: 0, - color: '#566074', - fontSize: '12px', - letterSpacing: '0.4px', - fontWeight: 600, + margin: 0, + color: '#566074', + fontSize: '12px', + letterSpacing: '0.4px', + fontWeight: 600, +}; + +const detailModalTitle = { + margin: '4px 0', + fontSize: '22px', + fontWeight: 700, + color: '#1d1f2e', +}; + +const detailModalSubtitle = { + margin: 0, + color: '#6b7280', + fontSize: '14px', }; -const detailModalTitle = { margin: '4px 0', fontSize: '22px', fontWeight: 700, color: '#1d1f2e' }; -const detailModalSubtitle = { margin: 0, color: '#6b7280', fontSize: '14px' }; + const modalCloseButton = { - background: '#f3f4f6', - border: '1px solid #e5e7eb', - borderRadius: '10px', - width: '36px', - height: '36px', - fontSize: '22px', - cursor: 'pointer', - color: '#374151', + background: '#f3f4f6', + border: '1px solid #e5e7eb', + borderRadius: '10px', + width: '36px', + height: '36px', + fontSize: '22px', + cursor: 'pointer', + color: '#374151', }; + const detailGrid = { - display: 'grid', - gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', - gap: '16px', - marginTop: '16px', + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', + gap: '16px', + marginTop: '16px', +}; + +const detailField = { + display: 'flex', + flexDirection: 'column', + gap: '6px', }; -const detailField = { display: 'flex', flexDirection: 'column', gap: '6px' }; -const detailLabel = { fontSize: '13px', color: '#374151', fontWeight: 600 }; + +const detailLabel = { + fontSize: '13px', + color: '#374151', + fontWeight: 600, +}; + const inputStyle = { - width: '100%', - padding: '12px 14px', - borderRadius: '10px', - border: '1px solid #d1d5db', - background: '#f3f4f6', - fontSize: '14px', - color: '#111827', - outline: 'none', + width: '100%', + padding: '12px 14px', + borderRadius: '10px', + border: '1px solid #d1d5db', + background: '#f3f4f6', + fontSize: '14px', + color: '#111827', + outline: 'none', }; + const detailActions = { - marginTop: '20px', - display: 'flex', - gap: '12px', - justifyContent: 'flex-end', + marginTop: '20px', + display: 'flex', + gap: '12px', + justifyContent: 'flex-end', }; + const primaryButton = { - backgroundColor: '#274b93', - color: 'white', - border: 'none', - borderRadius: '20px', - padding: '12px 24px', - fontSize: '14px', - fontWeight: 600, - cursor: 'pointer', + backgroundColor: '#274b93', + color: 'white', + border: 'none', + borderRadius: '20px', + padding: '12px 24px', + fontSize: '14px', + fontWeight: 600, + cursor: 'pointer', }; + const secondaryButton = { - backgroundColor: 'white', - color: '#d14343', - border: '1px solid #d14343', - borderRadius: '20px', - padding: '12px 20px', - fontSize: '14px', - fontWeight: 600, - cursor: 'pointer', + backgroundColor: 'white', + color: '#d14343', + border: '1px solid #d14343', + borderRadius: '20px', + padding: '12px 20px', + fontSize: '14px', + fontWeight: 600, + cursor: 'pointer', }; + const feedbackStyle = { - marginTop: '8px', - marginBottom: '8px', - padding: '10px 12px', - borderRadius: '10px', - fontSize: '13px', -}; -const feedbackSuccessStyle = { - ...feedbackStyle, - backgroundColor: '#edf7ed', - color: '#1b5e20', - border: '1px solid #c8e6c9', -}; -const feedbackErrorStyle = { - ...feedbackStyle, - backgroundColor: '#ffebee', - color: '#c62828', - border: '1px solid #ffcdd2', -}; -const inlineError = { color: '#d14343', fontSize: '12px', marginTop: '2px' }; - -// Tab styles -const tabBarStyle = { - display: 'flex', - gap: '4px', - borderBottom: '2px solid #f3f4f6', - marginBottom: '8px', -}; -const tabStyle = { - padding: '10px 20px', - background: 'none', - border: 'none', - fontSize: '14px', - fontWeight: 500, - color: '#9ca3af', - cursor: 'pointer', - borderBottomWidth: '2px', - borderBottomStyle: 'solid', - borderBottomColor: 'transparent', - marginBottom: '-2px', - display: 'flex', - alignItems: 'center', - gap: '6px', -}; -const activeTabStyle = { - ...tabStyle, - color: '#274b93', - borderBottomColor: '#274b93', - fontWeight: 700, -}; -const tabBadgeStyle = { - backgroundColor: '#274b93', - color: 'white', - borderRadius: '10px', - padding: '1px 7px', - fontSize: '11px', - fontWeight: 700, + marginTop: '8px', + marginBottom: '8px', + padding: '10px 12px', + borderRadius: '10px', + backgroundColor: '#f8fafc', + color: '#0f172a', + border: '1px solid #e2e8f0', + fontSize: '13px', }; -// Applicant panel styles -const applicantsPanelStyle = { marginTop: '8px' }; -const applicantsListStyle = { - display: 'flex', - flexDirection: 'column', - gap: '12px', - marginTop: '12px', -}; -const applicantCardStyle = { - display: 'flex', - alignItems: 'center', - gap: '14px', - padding: '14px 16px', - borderRadius: '12px', - border: '1px solid #e5e7eb', - background: '#fff', -}; -const approvedCardStyle = { borderColor: '#bbf7d0', background: '#f0fdf4' }; -const avatarStyle = { - width: '40px', - height: '40px', - borderRadius: '50%', - background: 'linear-gradient(135deg, #274b93, #4a72d4)', - color: 'white', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - fontWeight: 700, - fontSize: '16px', - flexShrink: 0, -}; -const applicantNameStyle = { - margin: '0 0 2px', - fontSize: '14px', - fontWeight: 600, - color: '#111827', -}; -const applicantEmailStyle = { margin: 0, fontSize: '12px', color: '#6b7280' }; -const licenseBadgeStyle = { - display: 'inline-block', - marginTop: '4px', - padding: '2px 8px', - borderRadius: '8px', - background: '#EFF4FF', - color: '#274b93', - fontSize: '11px', - fontWeight: 600, -}; -const applicantActionsStyle = { display: 'flex', gap: '8px', flexShrink: 0 }; -const approveButtonStyle = { - padding: '7px 16px', - borderRadius: '20px', - border: 'none', - background: '#274b93', - color: 'white', - fontSize: '13px', - fontWeight: 600, - cursor: 'pointer', -}; -const rejectButtonStyle = { - padding: '7px 14px', - borderRadius: '20px', - border: '1px solid #d14343', - background: 'white', - color: '#d14343', - fontSize: '13px', - fontWeight: 600, - cursor: 'pointer', -}; -const approvedPillStyle = { - padding: '6px 14px', - borderRadius: '20px', - background: '#dcfce7', - color: '#16a34a', - fontSize: '13px', - fontWeight: 600, -}; -const rejectedPillStyle = { - padding: '6px 14px', - borderRadius: '20px', - background: '#fee2e2', - color: '#dc2626', - fontSize: '13px', - fontWeight: 600, -}; -const emptyApplicantsStyle = { textAlign: 'center', padding: '40px 20px', color: '#9ca3af' }; -const emptyIconStyle = { fontSize: '40px', marginBottom: '8px' }; - -// Chat modal styles -const chatModalOverlay = { - position: 'fixed', - inset: 0, - background: 'rgba(0,0,0,0.5)', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - zIndex: 1200, - padding: '20px', -}; -const chatModalContainer = { - background: '#fff', - borderRadius: '16px', - width: '100%', - maxWidth: '420px', - maxHeight: '90vh', - display: 'flex', - flexDirection: 'column', - boxShadow: '0 24px 64px rgba(0,0,0,0.25)', - overflow: 'hidden', -}; -const chatModalHeaderStyle = { - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - padding: '18px 20px 14px', - background: '#1a2f6e', -}; -const chatModalHeaderLeft = { display: 'flex', alignItems: 'center', gap: '10px' }; -const chatLogoStyle = { - width: '32px', - height: '32px', - borderRadius: '8px', - background: 'rgba(255,255,255,0.15)', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', -}; -const chatModalOverlineStyle = { - margin: 0, - fontSize: '10px', - color: 'rgba(255,255,255,0.6)', - fontWeight: 600, - letterSpacing: '0.8px', -}; -const chatModalTitleStyle = { margin: 0, fontSize: '16px', fontWeight: 700, color: 'white' }; -const chatCloseButtonStyle = { - background: 'rgba(255,255,255,0.15)', - border: 'none', - color: 'white', - width: '30px', - height: '30px', - borderRadius: '8px', - fontSize: '20px', - cursor: 'pointer', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', -}; -const chatShiftInfoRowStyle = { - display: 'flex', - gap: '6px', - padding: '10px 20px 14px', - flexWrap: 'wrap', - background: '#1a2f6e', -}; -const chatPillStyle = { - display: 'flex', - alignItems: 'center', - gap: '4px', - background: 'rgba(255,255,255,0.15)', - borderRadius: '20px', - padding: '4px 10px', - fontSize: '11px', - color: 'rgba(255,255,255,0.9)', - fontWeight: 500, -}; -const chatPillDotStyle = { - width: '6px', - height: '6px', - borderRadius: '50%', - background: '#4ade80', - flexShrink: 0, -}; -const chatGuardNameStyle = { - padding: '10px 20px', - fontSize: '12px', - color: '#6b7280', - borderBottom: '1px solid #f3f4f6', - background: '#fff', -}; -const chatMessagesAreaStyle = { - flex: 1, - overflowY: 'auto', - padding: '16px 20px', - display: 'flex', - flexDirection: 'column', - minHeight: '240px', - maxHeight: '340px', - background: '#fff', -}; -const chatEmptyStyle = { - flex: 1, - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', - textAlign: 'center', - padding: '40px 20px', -}; -const chatSenderNameStyle = { - fontSize: '11px', - color: '#9ca3af', - marginBottom: '4px', - paddingLeft: '2px', -}; -const chatBubbleOtherStyle = { - background: '#f3f4f6', - borderRadius: '12px 12px 12px 2px', - padding: '10px 14px', - fontSize: '13px', - color: '#111827', - maxWidth: '80%', - wordBreak: 'break-word', -}; -const chatBubbleOwnStyle = { - background: '#1a2f6e', - borderRadius: '12px 12px 2px 12px', - padding: '10px 14px', - fontSize: '13px', - color: 'white', - maxWidth: '80%', - wordBreak: 'break-word', -}; -const chatTimestampStyle = { - fontSize: '10px', - color: '#9ca3af', - marginTop: '3px', - paddingLeft: '2px', -}; -const chatInputAreaStyle = { - padding: '12px 16px 14px', - borderTop: '1px solid #f3f4f6', - background: '#fff', -}; -const chatInputRowStyle = { - display: 'flex', - gap: '8px', - alignItems: 'center', - background: '#f3f4f6', - borderRadius: '12px', - padding: '6px 6px 6px 14px', -}; -const chatInputStyle = { - flex: 1, - background: 'none', - border: 'none', - outline: 'none', - fontSize: '13px', - color: '#111827', -}; -const chatSendButtonStyle = { - width: '34px', - height: '34px', - borderRadius: '8px', - border: 'none', - background: '#1a2f6e', - color: 'white', - cursor: 'pointer', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - flexShrink: 0, -}; -const chatFooterNoteStyle = { - margin: '8px 0 0', - fontSize: '11px', - color: '#9ca3af', - textAlign: 'center', +const inlineError = { + color: '#d14343', + fontSize: '12px', + marginTop: '2px', }; From 270a9ea19eaf4f9028ad06a36ad3b993f8cfdf09 Mon Sep 17 00:00:00 2001 From: Evangeline-d Date: Sat, 9 May 2026 23:39:55 +1000 Subject: [PATCH 4/4] Fix CreateShift filename casing --- .../employer-panel/src/pages/CreateShift.css | 438 ------------------ 1 file changed, 438 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 08a19015e..000000000 --- a/app-frontend/employer-panel/src/pages/CreateShift.css +++ /dev/null @@ -1,438 +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 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%; - } -}