diff --git a/app-frontend/employer-panel/src/components/Header.js b/app-frontend/employer-panel/src/components/Header.js
index ec2dad5ed..93db7b4a2 100644
--- a/app-frontend/employer-panel/src/components/Header.js
+++ b/app-frontend/employer-panel/src/components/Header.js
@@ -2,6 +2,7 @@ import React from 'react';
import { Link, useNavigate } from 'react-router-dom';
import CompanyLogo from './company_logo.svg';
import ProfilePicPlaceHolder from './ProfilePicPlaceHolder.svg';
+import NotificationsPopup from '../pages/NotificationsPopup';
export default function Header() {
const navigate = useNavigate();
@@ -55,6 +56,9 @@ export default function Header() {
Email
)}
+ {/* Notifications */}
+
+
{/* Avatar */}
navigate("/company-profile")} style={{ cursor: "pointer" }}>

diff --git a/app-frontend/employer-panel/src/components/NotificationIcon.svg b/app-frontend/employer-panel/src/components/NotificationIcon.svg
new file mode 100644
index 000000000..24849a48a
--- /dev/null
+++ b/app-frontend/employer-panel/src/components/NotificationIcon.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/app-frontend/employer-panel/src/pages/NotificationsPopup.js b/app-frontend/employer-panel/src/pages/NotificationsPopup.js
new file mode 100644
index 000000000..5d647fde1
--- /dev/null
+++ b/app-frontend/employer-panel/src/pages/NotificationsPopup.js
@@ -0,0 +1,208 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { useNavigate } from 'react-router-dom';
+import NotificationIcon from '../components/NotificationIcon.svg';
+import http from '../lib/http';
+
+const POLL_INTERVAL_MS = 30_000;
+
+export default function NotificationsPopup() {
+ const navigate = useNavigate();
+ const [notifications, setNotifications] = useState([]);
+ const [showPopup, setShowPopup] = useState(false);
+ const popupRef = useRef(null);
+
+ // Tracks applicant IDs we've already seen per shift,
+ // so we only notify on genuinely new applications
+ const prevApplicantsRef = useRef({});
+
+ // ------------------------------------------------------------------
+ // Polling — diffs current applicants against what we last saw
+ // ------------------------------------------------------------------
+ const fetchAndDiffShifts = async () => {
+ try {
+ const response = await http.get('/shifts?withApplicantsOnly=true');
+ const shifts = (response.data.items || []).filter(
+ s => s.status === 'open' || s.status === 'applied'
+ );
+
+ const newNotifications = [];
+
+ shifts.forEach((shift) => {
+ const applicants = shift.applicants || [];
+ const prevIds = prevApplicantsRef.current[shift._id] || [];
+
+ applicants
+ .filter(a => !prevIds.includes(a._id))
+ .forEach((applicant) => {
+ newNotifications.push({
+ id: `${shift._id}-${applicant._id}`,
+ shiftId: shift._id,
+ shiftTitle: shift.title,
+ shiftDate: shift.date,
+ shiftStartTime: shift.startTime,
+ shiftEndTime: shift.endTime,
+ guardName: applicant.name,
+ isRead: false,
+ receivedAt: new Date().toISOString(),
+ });
+ });
+
+ prevApplicantsRef.current[shift._id] = applicants.map(a => a._id);
+ });
+
+ if (newNotifications.length > 0) {
+ setNotifications(prev => [...newNotifications, ...prev]);
+ }
+ } catch (err) {
+ console.error('NotificationsPopup: failed to fetch shifts', err);
+ }
+ };
+
+ useEffect(() => {
+ fetchAndDiffShifts();
+ const interval = setInterval(fetchAndDiffShifts, POLL_INTERVAL_MS);
+ return () => clearInterval(interval);
+ }, []);
+
+ // Close popup when clicking outside
+ useEffect(() => {
+ const handleClickOutside = (e) => {
+ if (popupRef.current && !popupRef.current.contains(e.target)) {
+ setShowPopup(false);
+ }
+ };
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ // ------------------------------------------------------------------
+ // Handlers
+ // ------------------------------------------------------------------
+ const unreadCount = notifications.filter(n => !n.isRead).length;
+
+ const markAllAsRead = () =>
+ setNotifications(prev => prev.map(n => ({ ...n, isRead: true })));
+
+ const handleBellClick = () => {
+ setShowPopup(prev => !prev);
+ // Mark as read when user opens the panel
+ if (!showPopup && unreadCount > 0) markAllAsRead();
+ };
+
+ const handleNotificationClick = () => {
+ navigate('/manage-shift');
+ setShowPopup(false);
+ };
+
+ // ------------------------------------------------------------------
+ // Styles (inline to preserve positioning behaviour with parent header)
+ // ------------------------------------------------------------------
+ const popupStyle = {
+ position: 'absolute',
+ top: '55px',
+ right: '0px',
+ backgroundColor: 'white',
+ borderRadius: '10px',
+ boxShadow: '0 4px 20px rgba(0,0,0,0.2)',
+ width: '320px',
+ maxHeight: '400px',
+ overflowY: 'auto',
+ zIndex: 1000,
+ color: '#333',
+ };
+
+ const notificationItemStyle = (isRead) => ({
+ padding: '12px 16px',
+ borderBottom: '1px solid #f0f0f0',
+ backgroundColor: isRead ? 'white' : '#eef2ff',
+ cursor: 'pointer',
+ transition: 'background-color 0.2s',
+ });
+
+ // ------------------------------------------------------------------
+ // Render
+ // ------------------------------------------------------------------
+ return (
+
+
+
+

+
+ {unreadCount > 0 && (
+
+ {unreadCount > 99 ? '99+' : unreadCount}
+
+ )}
+
+
+ {showPopup && (
+
+
+
+ Notifications
+ {unreadCount > 0 && (
+
+ Mark all as read
+
+ )}
+
+
+ {notifications.length === 0 ? (
+
+ No new applications yet
+
+ ) : (
+ notifications.map((n) => (
+
+
+ New application — {n.shiftTitle}
+
+
+ 👤 {n.guardName}
+
+
+ 📅 {new Date(n.shiftDate).toLocaleDateString('en-AU', {
+ weekday: 'short', day: 'numeric',
+ month: 'short', year: 'numeric',
+ })}
+
+
+ 🕐 {n.shiftStartTime} – {n.shiftEndTime}
+
+
+ ))
+ )}
+
+ )}
+
+ );
+}
\ No newline at end of file