Notifications Page
Author: Carmine Antonio Bonavoglia Creation Date: 29/10/2025
Last Reviewer: Carmine Antonio Bonavoglia Last Updated: 31/10/2025
Overview
The Notifications page is a central hub for displaying all notifications across different sections of the application:
- Variants: Validation status, decline notifications, assignments
- Orders: Quotation requests, quote uploads, quote responses
- Tickets: Ticket creation, status changes, new messages
- Teams: Member invitations, role changes, member removals
Key characteristics:
- Multi-source aggregation — Combines notifications from 4 distinct systems
- Smart grouping — Automatically categorizes by type with collapsible sections
- Firestore enrichment — Supplements notifications with brand names and profile names
- Read status tracking — Marks individual and bulk notifications as read via Redux
- Smart navigation — Clicking notifications routes to relevant detail pages
- Collapsible groups — Users control which notification types are expanded
Location: /src/frameValidation/pages/Notifications.js
Architecture Overview
Notifications Page
├─ Redux Layer
│ ├─ fetchNotifications(email) → loads from Firestore
│ ├─ markNotificationAsReadThunk(id) → marks single notification
│ ├─ markAllNotificationsAsReadThunk(notifications) → batch mark all
│ └─ State: notifications[], loading
│
├─ Data Processing Pipeline
│ ├─ Load notifications from Redux
│ ├─ Enrich order notifications with Firestore data
│ ├─ Group notifications by type
│ ├─ Initialize collapse state
│ ├─ Fetch brand names (async parallel)
│ └─ Fetch profile names (async parallel)
│
├─ State Management
│ ├─ groupedNotifications {type → notifications[]}
│ ├─ collapsedGroups {type → boolean}
│ ├─ brandNames {brandId → name}
│ ├─ profileNames {email → fullName}
│ ├─ markingAsRead boolean
│ └─ HTML refs for group elements
│
├─ UI Components
│ ├─ Header (with Back button and action buttons)
│ ├─ Unread summary section (dynamic layout)
│ ├─ Grouped notification cards
│ │ ├─ Group header (expandable)
│ │ ├─ Notification list
│ │ └─ Type-specific renderers
│ └─ Empty state (no notifications)
│
└─ Navigation Flows
├─ Variants → /framevalidation/{brandId}/{productId}/{variantId}
├─ Orders → /orderpage
├─ Tickets → /orderpage (with state: openTicket, ticketId)
├─ Teams → /profile
└─ Catalogues → /my-catalogues/{catalogId}
State Management
Redux Integration
const dispatch = useDispatch();
const notifications = useSelector((state) => state.notifications.notifications); // Array of all notifications
const loading = useSelector((state) => state.notifications.loading); // Loading state
Redux Actions Used:
fetchNotifications(email)— Fetch all notifications for user from FirestoremarkNotificationAsReadThunk(id)— Mark single notification as readmarkAllNotificationsAsReadThunk(notifications)— Mark all notifications as read (batch)
Local Component State
const [groupedNotifications, setGroupedNotifications] = useState({}); // {type → [notifications]}
const [collapsedGroups, setCollapsedGroups] = useState({}); // {type → true/false}
const [brandNames, setBrandNames] = useState({}); // {brandId → "Brand Name"}
const [profileNames, setProfileNames] = useState({}); // {email → "First Last"}
const [markingAsRead, setMarkingAsRead] = useState(false); // UI loading state
HTML Refs
const groupRefs = React.useRef({}); // {type → HTMLElement} for scroll targets
Notification Data Flow
Step 1: Load Notifications
Trigger: Component mount or user clicks "Refresh" button
useEffect(() => {
if (email) {
dispatch(fetchNotifications(email));
}
}, [email, dispatch]);
Result: Redux populates notifications[] array
Step 2: Enrich Order Notifications
Purpose: Fill missing order details by querying Firestore
const enrichOrderNotifications = async (notifications) => {
return Promise.all(
notifications.map(async (notification) => {
// Skip non-order notifications
if (!notification.type?.startsWith("Order - ")) {
return notification;
}
// Fetch order doc if has orderId
try {
if (notification.orderId) {
const orderDoc = await getDoc(
doc(db, "CatalogOrders", notification.orderId)
);
if (orderDoc.exists()) {
const orderData = orderDoc.data();
return {
...notification,
orderType: notification.orderType || orderData.type,
orderStatus:
notification.orderStatus || orderData.status,
quoteUploadedAt:
notification.quoteUploadedAt ||
orderData.quoteUploadedAt,
quoteResponseDate:
notification.quoteResponseDate ||
orderData.quoteResponseDate,
};
}
}
} catch (error) {
console.error("Error enriching order notification:", error);
}
return notification;
})
);
};
Step 3: Group Notifications
Purpose: Organize notifications by type with intelligent consolidation
const groupNotificationsByType = (notifications) => {
const grouped = {};
notifications.forEach((notification) => {
let type = notification.type || "Unknown";
// Consolidate variations under main categories
if (type.startsWith("Order - ")) {
type = "Orders";
} else if (type.startsWith("Ticket - ")) {
type = "Tickets";
} else if (type.startsWith("Teams - ")) {
type = "Teams";
}
if (!grouped[type]) {
grouped[type] = [];
}
grouped[type].push(notification);
});
return grouped;
};
Example grouping:
{
"Orders": [{Order - Quote Sent}, {Order - Quote Accepted}, ...],
"Tickets": [{Ticket - Created}, {Ticket - New Message}, ...],
"Teams": [{Teams - Member Added}, ...],
"Variant validated": [{variant notification}],
"Variant declined": [{variant notification}],
"Variant assigned": [{variant notification}]
}
Step 4: Fetch Brand Names (Parallel)
Purpose: Enrich variant notifications with human-readable brand names
const fetchBrandNames = async (brandIds) => {
const uniqueBrandIds = [...new Set(brandIds)]; // Remove duplicates
const brandNamesMap = {};
const brandPromises = uniqueBrandIds.map(async (brandId) => {
if (!brandId) return null;
try {
const brandDoc = await getDoc(doc(db, "MainBrands", brandId));
if (brandDoc.exists()) {
const brandData = brandDoc.data();
return {
id: brandId,
name: brandData.nameBrand || brandId, // Fallback to ID
};
}
return { id: brandId, name: brandId };
} catch (error) {
console.error(`Error fetching brand ${brandId}:`, error);
return { id: brandId, name: brandId };
}
});
const brandResults = await Promise.all(brandPromises);
// Build map: brandId → brand name
brandResults.forEach((result) => {
if (result) {
brandNamesMap[result.id] = result.name;
}
});
setBrandNames(brandNamesMap);
};
Triggered when:
// Inside useEffect that processes notifications
const brandIds = enrichedNotifications
.filter((notification) => notification.brandId)
.map((notification) => notification.brandId);
if (brandIds.length > 0) {
fetchBrandNames(brandIds);
}
Step 5: Fetch Profile Names (Parallel)
Purpose: Enrich notifications with full names instead of email addresses
const fetchProfileNames = async (emails) => {
const uniqueEmails = [...new Set(emails)];
const profileNamesMap = {};
const profilePromises = uniqueEmails.map(async (email) => {
if (!email) return null;
try {
const profileDoc = await getDoc(doc(db, "Profiles", email));
if (profileDoc.exists()) {
const profileData = profileDoc.data();
const firstName = profileData.firstName || "";
const lastName = profileData.lastName || "";
const fullName = `${firstName} ${lastName}`.trim();
return {
email: email,
name: fullName || email, // Fallback to email
};
}
return { email: email, name: email };
} catch (error) {
console.error(`Error fetching profile ${email}:`, error);
return { email: email, name: email };
}
});
const profileResults = await Promise.all(profilePromises);
// Build map: email → full name
profileResults.forEach((result) => {
if (result) {
profileNamesMap[result.email] = result.name;
}
});
setProfileNames(profileNamesMap);
};
Triggered when:
// Extract all emails from notifications
const emails = [];
enrichedNotifications.forEach((notification) => {
if (notification.sentBy && !emails.includes(notification.sentBy)) {
emails.push(notification.sentBy);
}
if (notification.sentTo && !emails.includes(notification.sentTo)) {
emails.push(notification.sentTo);
}
});
if (emails.length > 0) {
fetchProfileNames(emails);
}
Notification Types & Rendering
Type 1: Orders
Type prefix: "Order - "
Subtypes:
"Order - Add 3d assets to catalog""Order - Quote Sent""Order - Quote Accepted""Order - Quote Rejected"- etc.
Style Info:
case "Orders":
return {
color: "#fa8c16", // Orange
icon: 'O',
title: 'Orders'
};
Rendered Details:
Order by: [Sender Full Name]
Object: [Order Status] (e.g., "Quote Sent")
Order Id: [Order ID]
Order Name: [Order Name]
Quote Sent: [Date/Time if available]
Quote Response: [Date/Time if available]
>> Click to view order details
Navigation: Clicking the notification navigates to /orderpage
Type 2: Tickets
Type prefix: "Ticket - "
Subtypes:
"Ticket - Created""Ticket - Status Changed""Ticket - New Message"
Style Info:
case "Tickets":
return {
color: "#13c2c2", // Cyan
icon: 'T',
title: 'Tickets'
};
Message rendering:
const getTicketMessage = (notification) => {
const orderId = notification.orderId || "N/A";
const subject = notification.ticketObject || "the ticket";
switch (notification.type) {
case "Ticket - Created":
return (
<>
A ticket has been opened for order{" "}
<strong>{orderId}</strong>
</>
);
case "Ticket - Status Changed":
if (
notification.newStatus === "closed" ||
notification.newStatus === "Closed"
) {
return (
<>
The ticket for order <strong>{orderId}</strong> has been
closed
</>
);
}
return (
<>
The ticket status for order <strong>{orderId}</strong> has
been updated
</>
);
case "Ticket - New Message":
return (
<>
You have received a reply on ticket "
<strong>{subject}</strong>" for order{" "}
<strong>{orderId}</strong>
</>
);
default:
return (
<>
Ticket update for order <strong>{orderId}</strong>
</>
);
}
};
Navigation: Clicking the notification navigates to:
navigate("/orderpage", {
state: {
openTicket: true,
ticketId: notification.ticketId,
orderId: notification.orderId,
},
});
Type 3: Teams
Type prefix: "Teams - "
Subtypes:
"Teams - Member Added""Teams - Member Removed""Teams - Role Changed"- etc.
Style Info:
case "Teams":
return {
color: "#722ed1", // Purple
icon: 'M',
title: 'Teams'
};
Message rendering:
const getTeamMessage = (notification) => {
let message = notification.message || "Team update";
// Replace sender email with full name if available
if (notification.sentBy && profileNames[notification.sentBy]) {
message = message.replace(
notification.sentBy,
profileNames[notification.sentBy]
);
}
return <div style={{ fontSize: "13px" }}>{message}</div>;
};
Rendered Details:
[Custom message with sender's full name]
Team ID: [Team ID]
>> Click to view your teams
Navigation: Clicking the notification navigates to /profile
Type 4: Variant Validated
No prefix — Exact type: "Variant validated"
Style Info:
case "Variant validated":
return {
color: statusTokens.success, // Green (#52c41a)
icon: 'V',
title: 'Variant Validated'
};
Direction:
notificationDirection === 'sent'— User sent variant for validation- Otherwise — User received variant validation
Rendered Details:
[SENT] Sent to: [Profile Full Name]
OR
[RECEIVED] From: [Profile Full Name]
SKU Model: [SKU]
Brand: [Brand Name]
Status: [New Status if available]
Message: [Custom message if available]
---
Variant awaiting your approval
Navigation: Clicking routes to:
navigate(
`/framevalidation/${notification.brandId}/${notification.productId}/${notification.variantId}`
);
Type 5: Variant Declined
No prefix — Exact type: "Variant declined"
Style Info:
case "Variant declined":
return {
color: statusTokens.error, // Red (#ff4d4f)
icon: 'X',
title: 'Variant Declined'
};
Special feature: Shows "View details" button for decline notes
const viewDeclineDetails = async (e, notification) => {
e.stopPropagation();
const noteId = notification.declineRef || notification.declineDescription;
if (!noteId) {
message.error("No decline notes available");
return;
}
// Uses NoteViewer component to display decline reasons
const noteViewerComponent = (
<NoteViewer
list_notes={[noteId]}
record={{
glassesName: notification.nameVariant || "",
skuModel: notification.skuModel || "",
}}
/>
);
// Render and trigger modal
const noteViewerElement = document.createElement("div");
document.body.appendChild(noteViewerElement);
const ReactDOM = await import("react-dom");
ReactDOM.render(noteViewerComponent, noteViewerElement);
const button = noteViewerElement.querySelector("button");
if (button)
button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
};
Rendered Details:
[SENT] Sent to: [Profile Name]
OR
[RECEIVED] From: [Profile Name]
SKU Model: [SKU]
Brand: [Brand Name]
Notes: [View details link]
---
The variant has been rejected
Type 6: Variant Assigned
No prefix — Exact type: "Variant assigned"
Style Info:
case "Variant assigned":
return {
color: "#1890ff", // Blue
icon: 'A',
title: 'Variant Assigned'
};
Rendered Details:
[SENT] Sent to: [Profile Name]
OR
[RECEIVED] From: [Profile Name]
SKU Model: [SKU]
Brand: [Brand Name]
Message: [Custom message if available]
---
The variant has been assigned
UI Components & Layouts
Header Section
┌─────────────────────────────────────────────────────┐
│ [Back Icon] Notifications [Mark all as read] [Refresh] │
└─────────────────────────────────────────────────────┘
Back button: Uses navigate(-1) to return to previous page
Mark all as read button:
- Triggers
markAllAsRead()which calls Redux thunk - Disabled if: loading | no notifications | all already read
- Shows loading spinner during operation
Refresh button:
- Calls
dispatch(fetchNotifications(email)) - Shows loading spinner during fetch
Unread Summary Section
Single unread type:
┌──────────────────────────────────────────────────────┐
│ You have 3 unread Variants [V 3 Variants] │
└──────────────────────────────────────────────────────┘
Multiple unread types:
┌──────────────────────────────────────────────────────┐
│ You have 8 unread notifications │
│ [V 2 Validated] [X 1 Declined] │
└──────────────────────────────────────────────────────┘
Clicking badges: Scrolls to that notification group
Grouped Notification Cards
Card per notification type:
┌─────────────────────────────────────────────┐
│ [V] Variants 3 unread [▼ Collapse] │ ← Header (clickable)
├─────────────────────────────────────────────┤
│ ┃ Glasses Model A [NEW] 29/10/2025 14:30 │ ← Unread indicator (left bar)
│ │ From: John Smith [RECEIVED] │
│ │ SKU: ABC-123 │
│ │ Brand: Brand X │
│ │ Variant awaiting your approval │
│ ├─────────────────────────────────────────────┤
│ ┃ Glasses Model B [NEW] 29/10/2025 13:45 │
│ │ Sent to: Jane Doe [SENT] │
│ │ SKU: DEF-456 │
│ │ Brand: Brand Y │
│ │ Message: Check this variant │
│ ├─────────────────────────────────────────────┤
│ │ Glasses Model C 29/10/2025 12:00 │ ← Read (lighter bg)
│ │ From: Admin [RECEIVED] │
│ │ SKU: GHI-789 │
│ │ Brand: Brand Z │
│ └─────────────────────────────────────────────┘
Header styling:
- Background color with 20% opacity
- Type icon with full opacity color
- Group title with color
- Unread count badge
- Expand/collapse chevron
Item styling per read status:
- Unread: Bold text, colored background, left border bar
- Read: Normal text, transparent background, no left bar
Empty State
┌──────────────────────────────────────┐
│ │
│ No notifications │
│ │
└──────────────────────────────────────┘
Interaction Flows
Flow 1: Click Notification Item
User clicks notification item
↓
Call handleNotificationClick(notification)
├─ If not read:
│ └─ markNotificationAsReadThunk(id)
│ └─ Update Redux + local state
│
└─ Navigate based on type:
├─ Teams → /profile
├─ Order - Add 3d assets → /my-catalogues/{catalogId}
├─ Order - * → /orderpage
├─ Ticket - * → /orderpage with state
└─ Variant * → /framevalidation/{brandId}/{productId}/{variantId}
Flow 2: Mark All as Read
User clicks "Mark all as read" button
↓
markAllAsRead()
├─ Get unread count
├─ Validate (if 0 unread, show "already read" message)
├─ Call markAllNotificationsAsReadThunk(notifications)
│ └─ Updates Redux state (all read: true)
├─ Update local groupedNotifications state
│ └─ Set all items: read = true
└─ Show success message with count
Flow 3: Expand/Collapse Group
User clicks group header
↓
toggleGroupCollapse(groupType)
├─ Flip collapsedGroups[groupType]
├─ Update UI (list items become visible/hidden)
└─ No API calls
Flow 4: Scroll to Group (from summary)
User clicks unread count badge in summary
↓
scrollToNotificationGroup(groupType)
├─ Expand group: collapsedGroups[groupType] = false
├─ Wait 100ms for render
└─ Scroll: groupRefs.current[groupType].scrollIntoView({ smooth })
Flow 5: View Decline Details
User clicks "View details" on declined variant
↓
viewDeclineDetails(e, notification)
├─ stopPropagation (prevent parent click)
├─ Get noteId from declineRef or declineDescription
├─ Create NoteViewer component with note ID
├─ Render to DOM
└─ Programmatically trigger modal open
Key Utility Functions
formatTimestamp(timestamp)
Purpose: Convert various timestamp formats to readable string
const formatTimestamp = (timestamp) => {
if (!timestamp) return "N/A";
let date;
if (timestamp.__time__) {
date = new Date(timestamp.__time__);
} else if (timestamp.seconds) {
// Firestore Timestamp
date = new Date(timestamp.seconds * 1000);
} else if (timestamp instanceof Date) {
date = timestamp;
} else {
date = new Date(timestamp);
}
return date.toLocaleString("en-GB", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
};
Handled formats:
timestamp.__time__(custom JS object)timestamp.seconds(Firestore Timestamp)- JavaScript
Dateobject - String or numeric ISO/Unix timestamp
Output: "29/10/2025, 14:30"
formatNotificationDate(date)
Purpose: Format date for notification items (shorter format)
const formatNotificationDate = (date) => {
if (!date) return "";
const dateObj = new Date(date);
return dateObj.toLocaleString("en-GB", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
};
Output: "29/10/2025, 14:30"
getStatusText(status)
Purpose: Convert order status codes to readable text
const getStatusText = (status) => {
switch (status) {
case "pending_analysis":
return "Pending Analysis";
case "analysis_complete":
return "Analysis Complete";
case "processing_addition":
return "Processing Addition";
case "working_3d_assets":
return "Batch Created";
case "freezing_time":
return "Working - Freezing Time";
case "quote_requested":
return "Quote Requested";
case "quote_sent":
return "Quote Sent";
case "quote_accepted":
return "Quote Accepted";
case "quote_rejected":
return "Quote Rejected";
case "completed":
return "Batch Created";
case "deleted":
return "Order Cancelled";
case "error":
return "Error";
default:
return status;
}
};
Used for: Displaying order status in notification details
getNotificationStyleInfo(type)
Purpose: Return color, icon, and title for notification type
const getNotificationStyleInfo = (type) => {
switch (type) {
case "Variant validated":
return {
color: statusTokens.success,
icon: "V",
title: "Variant Validated",
};
case "Variant declined":
return {
color: statusTokens.error,
icon: "X",
title: "Variant Declined",
};
case "Variant assigned":
return { color: "#1890ff", icon: "A", title: "Variant Assigned" };
case "Orders":
return { color: "#fa8c16", icon: "O", title: "Orders" };
case "Tickets":
return { color: "#13c2c2", icon: "T", title: "Tickets" };
case "Teams":
return { color: "#722ed1", icon: "M", title: "Teams" };
default:
return {
color: statusTokens.info,
icon: "N",
title: "Notification",
};
}
};
Used for: Group header styling and badges
countUnreadByType(notificationsArray)
Purpose: Count unread notifications in array
const countUnreadByType = (notificationsArray) => {
return notificationsArray.filter((n) => !n.read).length;
};
Notification Data Structure
Variant Notification
{
id: string, // Unique ID
type: "Variant validated" | "Variant declined" | "Variant assigned",
date: Date | timestamp,
read: boolean,
notificationDirection: "sent" | "received",
// Variant info
brandId: string,
productId: string,
variantId: string,
nameVariant: string,
skuModel: string,
// Sender/Receiver
sentBy: string, // Email of sender
sentTo: string, // Email of receiver
// For declined variants
declineRef: string, // Reference to decline note (Firestore doc ID)
declineDescription: string, // Fallback decline text
// Custom message
message: string,
newStatus: string
}
Order Notification
{
id: string,
type: "Order - " + [subtype],
date: Date | timestamp,
read: boolean,
// Order info
orderId: string,
orderName: string,
orderType: string, // e.g., "order_3d_asset", "add_assets_to_catalogue"
orderStatus: string, // e.g., "quote_sent", "quote_accepted"
// Enriched from Firestore
quoteUploadedAt: Date,
quoteResponseDate: Date,
// Sender/Receiver
sentBy: string,
sentTo: string,
// For catalogue notifications
catalogId: string,
nameCatalog: string
}
Ticket Notification
{
id: string,
type: "Ticket - " + [subtype],
date: Date | timestamp,
read: boolean,
// Ticket info
ticketId: string,
ticketObject: string, // Subject/title
orderId: string,
newStatus: string, // For status change notifications
// Sender/Receiver
sentBy: string,
sentTo: string
}
Team Notification
{
id: string,
type: "Teams - " + [subtype],
date: Date | timestamp,
read: boolean,
// Team info
teamId: string,
teamName: string,
message: string, // Descriptive message
// Sender/Receiver
sentBy: string, // Email
sentTo: string // Email
}
Performance Considerations
Parallel Data Fetching
Pattern: Use Promise.all() for independent Firestore queries
// Fetch all brands in parallel
const brandPromises = uniqueBrandIds.map((id) =>
getDoc(doc(db, "MainBrands", id))
);
const brandResults = await Promise.all(brandPromises);
// Fetch all profiles in parallel
const profilePromises = uniqueEmails.map((email) =>
getDoc(doc(db, "Profiles", email))
);
const profileResults = await Promise.all(profilePromises);
Benefits:
- Reduces total fetch time (parallel vs sequential)
- Single failure doesn't block other fetches (try/catch per request)
- Fallback to ID/email if fetch fails
Deduplication
Brand IDs:
const uniqueBrandIds = [...new Set(brandIds)]; // Remove duplicates
Emails:
const uniqueEmails = [...new Set(emails)]; // Remove duplicates
Benefits: Avoid redundant Firestore queries
Memoization via useState
Brand names map:
const [brandNames, setBrandNames] = useState({}); // Cached for component lifetime
Profile names map:
const [profileNames, setProfileNames] = useState({}); // Cached for component lifetime
Benefits: Avoid re-fetching after initial load (unless component unmounts)
Integration Points & Dependencies
Redux Actions (from notifications/actions)
import {
fetchNotifications, // Fetch all notifications for user
markNotificationAsReadThunk, // Mark single as read
markAllNotificationsAsReadThunk, // Mark all as read (batch)
} from "../../redux/notifications/actions";
Firestore Collections
import { db } from '../../data/base';
// Collections accessed:
- CatalogOrders (for order enrichment)
- MainBrands (for brand names)
- Profiles (for user full names)
Utilities
import { secureGetItem } from "../../data/utils"; // Get email from localStorage
// Usage:
const email = secureGetItem("email");
Components
import EmptyCard from "../../components/EmptyCard"; // Empty state
import HeaderComponent from "../../components/Components/Headers/HeaderComponent"; // Header
import NoteViewer from "../modal/NoteViewerModal"; // Decline note modal
Ant Design Components
Card, List, Avatar, Typography, Spin, Button, message, Row, Col, Space, Badge;
CaretDownOutlined, CaretUpOutlined;
NavigateBeforeIcon(Material - UI);
Error Handling & Edge Cases
Edge Case 1: Missing Firestore Data
Scenario: Brand document or profile document doesn't exist
Handling:
if (brandDoc.exists()) {
// Use nameBrand
} else {
// Fallback: use brandId as display name
return { id: brandId, name: brandId };
}
Impact: Notification shows ID instead of name, still readable
Edge Case 2: Failed Firestore Query
Scenario: Network error or permission denied on Firestore read
Handling:
try {
// Query Firestore
} catch (error) {
console.error(`Error fetching brand ${brandId}:`, error);
// Fallback: use ID
return { id: brandId, name: brandId };
}
Impact: Continues processing, no UI break
Edge Case 3: Empty Notifications Array
Scenario: User has no notifications
Handling:
if (Object.keys(groupedNotifications).length === 0) {
return <EmptyCard description="No notifications" />;
}
Impact: Shows friendly empty state
Edge Case 4: All Notifications Already Read
Scenario: User clicks "Mark all as read" when all read
Handling:
const unreadNotifications = notifications.filter(
(notification) => !notification.read
);
if (unreadNotifications.length === 0) {
message.info("All notifications are already read");
setMarkingAsRead(false);
return;
}
Impact: Shows info message, button disabled in render
Edge Case 5: No Decline Notes Available
Scenario: Declined variant has no declineRef or declineDescription
Handling:
const noteId = notification.declineRef || notification.declineDescription;
if (!noteId) {
message.error("No decline notes available");
return;
}
Impact: Shows error, no modal opened
Edge Case 6: Missing Navigation Context
Scenario: User clicks variant notification but no brandId/productId/variantId
Current behavior: Doesn't navigate (missing required params)
Recommendation: Add fallback route or show warning
Edge Case 7: Email Domain Substitution in Team Messages
Scenario: Team message contains sender's email address
Solution:
if (notification.sentBy && profileNames[notification.sentBy]) {
message = message.replace(
notification.sentBy,
profileNames[notification.sentBy]
);
}
Example:
- Original: "john.doe@company.com was added to team"
- Rendered: "John Doe was added to team"
Testing Considerations
Unit Tests Needed
groupNotificationsByType()— Group consolidation logicinitializeCollapsedGroups()— Collapse initializationcountUnreadByType()— Unread countingformatTimestamp()— Timestamp formatting with multiple input typesformatNotificationDate()— Date formattinggetStatusText()— Status code translationgetNotificationStyleInfo()— Style info lookup
Integration Tests Needed
- Fetch notifications on mount
- Enrich order notifications with Firestore data
- Fetch brand names and profile names in parallel
- Mark single notification as read
- Mark all notifications as read
- Group notifications after fetch
- Collapse/expand groups
- Scroll to notification group
- Navigate to correct page based on notification type
E2E Tests Needed
- Load Notifications page → all notifications display
- Click notification → marked as read + navigate
- Click "Mark all as read" → all become read
- Click unread summary badge → scroll to group
- Click "View details" on decline → modal shows
- Refresh button → re-fetches notifications
Performance Metrics
Typical data volumes:
- Notifications per user: 10-100 per session
- Brand queries: 1-20 parallel (with dedup)
- Profile queries: 5-30 parallel (with dedup)
- Total Firestore reads: 30-60 operations
Optimization opportunities:
- Implement pagination (show first 20, load more on scroll)
- Cache brand/profile names in localStorage
- Use Firestore batch reads instead of individual queries
- Implement virtual scrolling for large lists
- Debounce rapid expand/collapse toggles
Related Files & Architecture
Notifications component:
/src/frameValidation/pages/Notifications.js
Redux state:
/src/redux/notifications/(reducer, actions, thunks)
Related pages:
/src/frameValidation/pages/OrderPage.js(order navigation)/src/frameValidation/pages/MyDataConsumption.js/src/components/Profile/TeamsSection.js(teams navigation)/src/pages/MyCatalogues.js(catalogue navigation)
Firestore collections:
CatalogOrdersMainBrandsProfiles