Skip to main content
Version: 1.0.0

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:

  1. fetchNotifications(email) — Fetch all notifications for user from Firestore
  2. markNotificationAsReadThunk(id) — Mark single notification as read
  3. markAllNotificationsAsReadThunk(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:

  1. timestamp.__time__ (custom JS object)
  2. timestamp.seconds (Firestore Timestamp)
  3. JavaScript Date object
  4. 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:


Testing Considerations

Unit Tests Needed

  • groupNotificationsByType() — Group consolidation logic
  • initializeCollapsedGroups() — Collapse initialization
  • countUnreadByType() — Unread counting
  • formatTimestamp() — Timestamp formatting with multiple input types
  • formatNotificationDate() — Date formatting
  • getStatusText() — Status code translation
  • getNotificationStyleInfo() — 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:

  1. Implement pagination (show first 20, load more on scroll)
  2. Cache brand/profile names in localStorage
  3. Use Firestore batch reads instead of individual queries
  4. Implement virtual scrolling for large lists
  5. Debounce rapid expand/collapse toggles

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:

  • CatalogOrders
  • MainBrands
  • Profiles