Skip to main content
Version: 1.0.0

Orders

Author: Carmine Antonio Bonavoglia Creation Date: 29/10/2025
Last Reviewer: Carmine Antonio Bonavoglia Last Updated: 31/10/2025

Overview

OrderPage.js is the primary component for displaying, managing, and orchestrating user orders in ARShades Studio. It handles two main workflows:

  1. order_3d_asset — Users request 3D asset creation/processing. Workflow: request → Admin quotes → Client accepts/rejects → production.
  2. add_assets_to_catalogue — Users bulk-request adding assets to catalogues via CSV. Workflow: upload → validation → processing → completion.

The page provides:

  • Infinite-scroll table of orders with filtering by role
  • Real-time status/ticket updates via Firestore listeners
  • Multiple modal workflows (bulk order, quote upload/response, ticketing, deletion)
  • Fine-grained RBAC: Super Admin, Admin, Member, Modellista, ModellerSupervisor
  • Session-based data transfer (e.g., from BulkAddModal to BulkOrderModal)

File location: /src/pages/OrderPage.js


Architecture & Component Hierarchy

OrderPage (page)

├─ HeaderComponent
│ └─ PrimaryButton "Order 3D Assets" (visible for Admin, Member; hidden for Modellista, ModellerSupervisor)

├─ InfiniteScroll (dataLength, hasMore, onScroll → loadMoreOrders)
│ └─ Table
│ ├─ Columns: Timestamp | Profile | Batch | Type | Status | Tickets | Actions
│ ├─ RowKey: order.id
│ └─ Renderers: formatTimestamp, getUserFullName, getStatusColour, getStatusText, etc.

└─ Modals
├─ BulkOrderModal (3-step stepper: upload → review → additional-info)
├─ OrderDetailsModal (full order details & edits)
├─ UploadQuoteModal (Admin: upload PDF quote + metadata)
├─ QuoteResponseModal (Client: accept/reject quote)
├─ ReviseQuoteModal (Admin: revise after rejection)
├─ TicketsModal (view & create support tickets per order)
└─ DeleteOrderModal (confirmation + deletion reason)

State Management Deep-Dive

Core Declarative State

// Orders & pagination
const [orders, setOrders] = useState([]); // Loaded order list
const [loading, setLoading] = useState(true); // Initial page load
const [loadingMore, setLoadingMore] = useState(false); // Infinite scroll load
const [lastVisibleDoc, setLastVisibleDoc] = useState(null); // Firestore cursor
const [hasMore, setHasMore] = useState(true); // Pagination sentinel

// User context
const [profileEmail, setProfileEmail] = useState(null); // Fetched from secure storage
const [clientId, setClientId] = useState(null); // Client identifier (or ref_cliente fallback)
const [currentUserRole, setCurrentUserRole] = useState(null); // 'Admin', 'Cliente', 'Modellista', 'ModellerSupervisor'
const [userProfiles, setUserProfiles] = useState({}); // Map { email → { firstName, lastName } }

// BulkOrderModal states
const [bulkOrderModalVisible, setBulkOrderModalVisible] = useState(false);
const [bulkOrderInitialData, setBulkOrderInitialData] = useState(null); // From sessionStorage (BulkAddModal)
const [bulkOrderInitialStep, setBulkOrderInitialStep] = useState(0); // 0 = start, 1 = prefilled data
const [manualModalOpen, setManualModalOpen] = useState(false); // Differentiate manual vs auto-open
const [resetBulkOrderStepper, setResetBulkOrderStepper] = useState(false); // Signal to stepper to reset

// Modal visibility & selection states
const [orderDetailsModalVisible, setOrderDetailsModalVisible] = useState(false);
const [selectedOrder, setSelectedOrder] = useState(null);

const [uploadQuoteModalVisible, setUploadQuoteModalVisible] = useState(false);
const [selectedOrderForQuote, setSelectedOrderForQuote] = useState(null);

const [quoteResponseModalVisible, setQuoteResponseModalVisible] =
useState(false);
const [selectedOrderForQuoteResponse, setSelectedOrderForQuoteResponse] =
useState(null);

const [reviseQuoteModalVisible, setReviseQuoteModalVisible] = useState(false);
const [selectedOrderForRevise, setSelectedOrderForRevise] = useState(null);

const [ticketsModalVisible, setTicketsModalVisible] = useState(false);
const [selectedOrderForTickets, setSelectedOrderForTickets] = useState(null);

const [deleteOrderModalVisible, setDeleteOrderModalVisible] = useState(false);
const [selectedOrderForDelete, setSelectedOrderForDelete] = useState(null);

Ref-based State (Transient Control)

const lastErrorTimeRef = useRef(0);
const ERROR_DEBOUNCE_TIME = 2000; // ms

// useCallback to debounce error messages (avoid message spam)
const showErrorMessage = useCallback(
(msg = "Error loading orders. Please try again.") => {
const currentTime = Date.now();
if (currentTime - lastErrorTimeRef.current > ERROR_DEBOUNCE_TIME) {
messageApi.error(msg);
lastErrorTimeRef.current = currentTime;
}
},
[messageApi]
);

Lifecycle Effects

1. Load User Data (mount)

useEffect(() => {
const loadUserData = async () => {
const storedEmail = await getSecureItem("email");
const storedRole = await getSecureItem("role");
let storedClientId = await getSecureItem("clientId");

if (!storedClientId) {
const refCliente = await getSecureItem("ref_cliente");
storedClientId = refCliente || null;
}

const profiles = await getUserProfiles();

setProfileEmail(storedEmail);
setCurrentUserRole(storedRole);
setClientId(storedClientId);
setUserProfiles(profiles);
};

loadUserData();
}, []);

Purpose: Fetch user identity from secure storage (email, role, client ID) and pre-load user profile mapping.


2. Check for SessionStorage Bulk Order Data

useEffect(() => {
const checkBulkOrderData = () => {
const shouldOpenModal = sessionStorage.getItem("openBulkOrderModal");
const bulkOrderData = sessionStorage.getItem("bulkOrderData");

if (shouldOpenModal === "true" && bulkOrderData) {
const parsedData = JSON.parse(bulkOrderData);
setBulkOrderInitialData(parsedData);
setBulkOrderInitialStep(1); // Skip to step 2 (review)
setBulkOrderModalVisible(true);

sessionStorage.removeItem("openBulkOrderModal");
sessionStorage.removeItem("bulkOrderData");
}
};

if (clientId && !manualModalOpen) {
checkBulkOrderData();
}
}, [clientId, manualModalOpen]);

Purpose: Support deep-linking from BulkAddModal. If bulk data was prepared elsewhere and stored in sessionStorage, auto-open BulkOrderModal at step 2 (review) with the data pre-filled.


3. Load Orders on Context Change

useEffect(() => {
loadOrders();
}, [profileEmail, currentUserRole, clientId]);

Purpose: Fetch fresh orders whenever user identity changes.


4. Handle Navigation-based Ticket Modal Open

useEffect(() => {
if (
location.state?.openTicket &&
location.state?.orderId &&
!loading &&
orders.length > 0
) {
const orderId = location.state.orderId;
const order = orders.find((o) => o.id === orderId);

if (order) {
setSelectedOrderForTickets(order);
setTicketsModalVisible(true);
window.history.replaceState({}, document.title); // Clear state
}
}
}, [location.state, orders, loading]);

Purpose: Support opening tickets modal via URL state (e.g., from notification click).


5. Real-time Firestore Listeners

useEffect(() => {
if (orders.length === 0) return;

console.log(
"👂 Setting up real-time listeners for",
orders.length,
"orders"
);

const orderIds = orders.map((o) => o.id);
const unsubscribes = orderIds.map((orderId) => {
const orderRef = doc(db, "CatalogOrders", orderId);
return onSnapshot(
orderRef,
(docSnapshot) => {
if (docSnapshot.exists()) {
const updatedOrderData = docSnapshot.data();

// Merge updated data into the list
setOrders((prevOrders) =>
prevOrders.map((o) =>
o.id === orderId ? { ...o, ...updatedOrderData } : o
)
);
}
},
(error) => console.error("Error in order listener:", orderId, error)
);
});

return () => {
console.log("🔇 Removing real-time listeners for orders");
unsubscribes.forEach((unsub) => unsub());
};
}, [orders.length]);

Purpose: Subscribe to per-order updates. When ticket count, status, or other fields change in Firestore, the UI updates in real-time without full page refresh.

Performance note: Creating 100+ listeners at scale can be expensive. Consider aggregated listeners for large order lists.


Data Loading Functions

loadOrders()

const loadOrders = async () => {
if (!profileEmail) return;

try {
setLoading(true);
setOrders([]);
setLastVisibleDoc(null);
setHasMore(true);

// Use role-based fetch if role available
let result;
if (currentUserRole) {
result = await getCatalogOrdersByRole(
profileEmail,
currentUserRole,
clientId,
PAGE_SIZE
);
} else {
const fetchedOrders = await getCatalogOrdersByProfile(profileEmail);
result = {
orders: fetchedOrders,
lastVisible: null,
hasMore: false,
};
}

let filteredOrders = result.orders;

// Role-based filtering
if (currentUserRole !== "Admin") {
// Non-admins don't see deleted orders
filteredOrders = filteredOrders.filter(
(o) => o.status !== "deleted"
);
}

if (currentUserRole === "Client" || currentUserRole === "Cliente") {
// Clients only see orders from 'working_3d_assets' onwards
const allowedStatuses = [
"working_3d_assets",
"freezing_time",
"quote_requested",
"quote_sent",
"quote_accepted",
"quote_rejected",
"completed",
];
filteredOrders = filteredOrders.filter((o) =>
allowedStatuses.includes(o.status)
);
}

// Add unique key to each order
const ordersWithKeys = filteredOrders.map((o, idx) => ({
...o,
key: o.id || `order-${idx}`,
}));

setOrders(ordersWithKeys);
setLastVisibleDoc(result.lastVisible);
setHasMore(result.hasMore);
} catch (error) {
console.error("Error loading orders:", error);
showErrorMessage();
} finally {
setLoading(false);
}
};

Filtering logic:

  • Super Admin: sees all orders globally
  • Admin / Member: see orders only from working_3d_assets status onwards (hide internal processing states)
  • Other roles: follow role-specific rules

loadMoreOrders() (Infinite Scroll)

const loadMoreOrders = useCallback(async () => {
if (loading || loadingMore || !hasMore || !profileEmail) return;

try {
setLoadingMore(true);

let result;
if (currentUserRole) {
result = await getCatalogOrdersByRole(
profileEmail, currentUserRole, clientId, PAGE_SIZE, lastVisibleDoc
);
} else {
setLoadingMore(false);
return; // No pagination support for non-role-based
}

let filteredOrders = result.orders;

// Apply same filtering as loadOrders()
if (currentUserRole !== 'Admin') {
filteredOrders = filteredOrders.filter(o => o.status !== 'deleted');
}

if (currentUserRole === 'Client' || currentUserRole === 'Cliente') {
const allowedStatuses = [...];
filteredOrders = filteredOrders.filter(o => allowedStatuses.includes(o.status));
}

if (filteredOrders && filteredOrders.length > 0) {
const ordersWithKeys = filteredOrders.map((o, idx) => ({
...o,
key: o.id || `order-${orders.length + idx}`
}));

setOrders(prevOrders => [...prevOrders, ...ordersWithKeys]);
setLastVisibleDoc(result.lastVisible);
setHasMore(result.hasMore);
} else {
setHasMore(false);
}
} catch (error) {
console.error('Error loading more orders:', error);
showErrorMessage('Failed to load more orders.');
setHasMore(false);
} finally {
setLoadingMore(false);
}
}, [loading, loadingMore, hasMore, profileEmail, currentUserRole, clientId, lastVisibleDoc, orders.length, showErrorMessage]);

Table Column Definitions

const columns = [
{
title: "Date/Time",
dataIndex: "timestamp",
key: "timestamp",
width: "20%",
render: (timestamp) => formatTimestamp(timestamp),
sorter: (a, b) =>
(a.timestamp?.seconds || 0) - (b.timestamp?.seconds || 0),
defaultSortOrder: "descend",
},
{
title: "Requested by",
dataIndex: "profile",
key: "profile",
width: "20%",
render: (email) => (
<div style={{ fontWeight: "500" }}>{getUserFullName(email)}</div>
),
sorter: (a, b) =>
getUserFullName(a.profile)
.toLowerCase()
.localeCompare(getUserFullName(b.profile).toLowerCase()),
},
{
title: "Batch",
dataIndex: "batch",
key: "batch",
width: "15%",
render: (batch) => batch || "-",
},
{
title: "Type",
dataIndex: "type",
key: "type",
width: "15%",
render: (type) => (
<Tag color={type === "order_3d_asset" ? "purple" : "blue"}>
{getTypeText(type)}
</Tag>
),
},
{
title: "Status",
dataIndex: "status",
key: "status",
width: "15%",
render: (status) => (
<Tag color={getStatusColour(status)}>
{getStatusText(status, currentUserRole)}
</Tag>
),
},
{
title: "Tickets",
key: "tickets",
width: "10%",
align: "center",
render: (_, record) => {
const ticketCount = record.list_tickets_refs?.length || 0;
if (ticketCount === 0) return <span>-</span>;

return (
<Badge count={ticketCount}>
<Button
type="text"
icon={<ChatBubbleOutlineIcon />}
onClick={() => {
setSelectedOrderForTickets(record);
setTicketsModalVisible(true);
}}
/>
</Badge>
);
},
},
{
title: "Actions",
key: "actions",
width: "10%",
align: "center",
render: (_, record) => {
const menuItems = getActionMenuItems(record);

return (
<Dropdown
menu={{
items: menuItems,
onClick: ({ key }) => handleActionClick(key, record),
}}
trigger={["click"]}
>
<Button type="text" icon={<MoreOutlined />} />
</Dropdown>
);
},
},
];

Helper Functions for UI

Timestamp Formatting

const formatTimestamp = (timestamp) => {
if (!timestamp) return "N/A";

let date;
if (timestamp.__time__) {
date = new Date(timestamp.__time__);
} else if (timestamp.seconds) {
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",
});
};

User Profile Resolution

const getUserFullName = (email) => {
if (!email || !userProfiles[email]) {
return email || "Unknown";
}

const profile = userProfiles[email];
return `${profile.firstName} ${profile.lastName}`.trim() || email;
};

Status Color Mapping

const getStatusColour = (status) => {
const colorMap = {
pending_analysis: "processing",
analysis_complete: "success",
processing_addition: "warning",
working_3d_assets: "gold",
freezing_time: "orange",
quote_requested: "purple",
quote_sent: "cyan",
quote_accepted: "green",
quote_rejected: "red",
completed: "green",
deleted: "default",
error: "error",
};
return colorMap[status] || "default";
};

Status Text (Role-aware)

const getStatusText = (status, userRole = null) => {
const textMap = {
quote_sent:
userRole === "Client" || userRole === "Cliente"
? "Quote Received"
: "Quote Sent",
pending_analysis: "Pending Analysis",
analysis_complete: "Analysis Complete",
processing_addition: "Processing Addition",
working_3d_assets: "Batch Created",
freezing_time: "Working - Freezing Time",
quote_requested: "Quote Requested",
quote_accepted: "Quote Accepted",
quote_rejected: "Quote Rejected",
completed: "Batch Created",
deleted: "Order Cancelled",
error: "Error",
};
return textMap[status] || status;
};

RBAC: Action Menu Generation

getActionMenuItems(record)

Determines which actions are available for a given order and user role.

const getActionMenuItems = (record) => {
const items = [];
const canDeleteStatuses = [
"pending_analysis",
"analysis_complete",
"processing_addition",
"working_3d_assets",
"freezing_time",
"quote_requested",
"quote_sent",
];
const canDelete = canDeleteStatuses.includes(record.status);

if (currentUserRole === "Admin") {
// Super Admin (Spaarkly staff)
items.push({ key: "view_details", label: "Order Details" });

if (
record.type === "order_3d_asset" &&
record.status === "quote_requested"
) {
items.push({ key: "upload_quote", label: "Upload Quote" });
}

if (
record.type === "order_3d_asset" &&
record.status === "quote_rejected"
) {
items.push({ key: "revise_quote", label: "Revise Quote" });
}

items.push({ key: "new_ticket", label: "Tickets" });

if (canDelete) {
items.push({
key: "delete_order",
label: "Delete Order",
danger: true,
});
}
} else if (currentUserRole === "Client" || currentUserRole === "Cliente") {
// Client user
items.push({ key: "view_details", label: "Order Details" });

if (
record.type === "order_3d_asset" &&
record.status === "quote_sent"
) {
items.push({ key: "respond_to_quote", label: "Respond to Quote" });
}

items.push({ key: "new_ticket", label: "New Ticket" });

if (canDelete) {
items.push({
key: "delete_order",
label: "Delete Order",
danger: true,
});
}
} else {
// Other roles (Modellista, ModellerSupervisor)
items.push({ key: "view_details", label: "Order Details" });
items.push({ key: "new_ticket", label: "New Ticket" });
}

return items;
};

Role permissions summary:

RoleView DetailsUpload QuoteRespond to QuoteRevise QuoteNew TicketDelete Order
Super Admin✅ (if quote_requested)✅ (if quote_rejected)✅ (if allowed status)
Admin✅ (if quote_requested)✅ (if quote_rejected)✅ (if allowed status)
Member✅ (if quote_sent)✅ (if allowed status)
Modellista
ModellerSupervisor

Action Handlers

handleActionClick(key, record)

const handleActionClick = (key, record) => {
switch (key) {
case "view_details":
setSelectedOrder(record);
setOrderDetailsModalVisible(true);
break;

case "upload_quote":
setSelectedOrderForQuote(record);
setUploadQuoteModalVisible(true);
break;

case "respond_to_quote":
setSelectedOrderForQuoteResponse(record);
setQuoteResponseModalVisible(true);
break;

case "revise_quote":
setSelectedOrderForRevise(record);
setReviseQuoteModalVisible(true);
break;

case "new_ticket":
setSelectedOrderForTickets(record);
setTicketsModalVisible(true);
break;

case "delete_order":
setSelectedOrderForDelete(record);
setDeleteOrderModalVisible(true);
break;

default:
messageApi.warning(`Unknown action: ${key}`);
}
};

1. BulkOrderModal (3D Asset Order)

Trigger: "Order 3D Assets" button (visible for non-restricted roles)

3-Step Stepper:

Step 0: Upload File
├─ Upload CSV/XLSX file (SKU, EAN, UPC codes)
└─ Click "Process File" → parses and validates each SKU

Step 1: Review & Select
├─ Table shows: SKU | EAN | Status (Orderable/In Progress/Error)
├─ Checkbox selection
└─ Click "Continue" to advance

Step 2: Additional Info
├─ Batch (required, must be unique)
├─ Order Name (optional)
└─ Reference Image (optional)
└─ Click "Request Quote" → creates asset order and sends to Admin for quote

Key features:

  • Session-based prefill: If sessionStorage.openBulkOrderModal === 'true', auto-open at step 2 with data
  • Firebase background function: After quote submission, calls REACT_APP_DOWNLOAD_VARIANT_JSON_HTTP asynchronously to process variants
  • Stepper reset: resetBulkOrderStepper prop triggers full state reset

State transitions in component:

const [currentStep, setCurrentStep] = useState(initialStep); // 0 or 1 (prefilled)
const [fileList, setFileList] = useState([]);
const [uploadedData, setUploadedData] = useState([]);
const [summary, setSummary] = useState({
total,
orderable,
error,
inProgress,
details,
});
const [assetOrderId, setAssetOrderId] = useState(null);
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const [selectedProducts, setSelectedProducts] = useState([]);
const [additionalInfo, setAdditionalInfo] = useState({ batch, name, image });

2. OrderDetailsModal

Trigger: "Order Details" action (all roles)

Displays:

  • Full order metadata (ID, timestamp, profile, type, status)
  • Order line items (for order_3d_asset, shows quote info if available)
  • Edit capability for Admin users

3. UploadQuoteModal (Admin → Client)

Trigger: "Upload Quote" action (Admin, order_3d_asset, quote_requested status)

Workflow:

  1. Admin uploads PDF quote document (max 10MB)
  2. Enters quote name (e.g., "Quote-2025-10-29")
  3. Selects quote date
  4. Optionally adds notes
  5. Confirmation modal appears
  6. On confirm: uploadQuoteAndUpdateOrder() called
    • PDF uploaded to storage
    • Order status → quote_sent
    • Client is notified

4. QuoteResponseModal (Client → Admin)

Trigger: "Respond to Quote" action (Client, order_3d_asset, quote_sent status)

Workflow:

  1. Client sees quote summary and details
  2. Clicks "Accept" or "Reject"
  3. If reject, optionally adds reason/notes
  4. On accept:
    • Status → quote_accepted
    • Background call to callPopulateVariantFromSkuApi() to populate variants
  5. If reject:
    • Status → quote_rejected
    • Admin notified to revise or handle

5. ReviseQuoteModal (Admin response to rejection)

Trigger: "Revise Quote" action (Admin, order_3d_asset, quote_rejected status)

Workflow:

  1. Admin reviews rejection reason
  2. Uploads new PDF quote
  3. Status → quote_sent again
  4. Client re-evaluates

6. TicketsModal (Support tickets per order)

Trigger: "Tickets" action or badge click

Features:

  • Lists existing tickets for the order
  • Shows ticket status, messages, timestamps
  • Allows creating new tickets (all roles)
  • Filters by ticket status (open, resolved, etc.)
  • Real-time updates via Firestore listeners

7. DeleteOrderModal (Confirmation + reason)

Trigger: "Delete Order" action (Admin/Client for allowed statuses)

Workflow:

  1. Confirmation modal asks for deletion reason
  2. On confirm: deleteCatalogOrder(orderId, reason) called
  3. Order status → deleted
  4. List refreshes, order hidden from non-Admin users

Quote Lifecycle Diagram

┌─────────────────────────────────────────────────────────────────┐
│ CLIENT initiates order_3d_asset │
│ Status: quote_requested │
└─────────────────┬───────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ ADMIN: UploadQuoteModal │
│ - Upload PDF quote │
│ - Enter quote name, date, notes │
│ Status: quote_requested → quote_sent │
└─────────────────┬───────────────────────────────────────────────┘


┌─────────────────┐
│ CLIENT notified │
└────────┬────────┘

┌────────┴────────┐
↓ ↓
┌─────────────┐ ┌──────────────┐
│ ACCEPT │ │ REJECT │
└────┬────────┘ └────┬─────────┘
│ │
↓ ↓
┌──────────────────┐ ┌────────────────────────┐
│ status: │ │ status: │
│ quote_accepted │ │ quote_rejected │
│ (variants added) │ │ (reason stored) │
└────┬─────────────┘ └────────┬───────────────┘
│ │
↓ ↓
[Processing] ┌─────────────────────────┐
│ ADMIN: ReviseQuoteModal │
│ Upload new PDF │
│ Status: quote_sent │
└───────┬─────────────────┘


[Loop back to CLIENT choice]

Order Status Lifecycle

For order_3d_asset:

quote_requested

(Admin uploads quote)

quote_sent
├→ Client accepts → quote_accepted → (processing starts)
└→ Client rejects → quote_rejected → (Admin revises)

(Admin uploads new quote)

quote_sent (loop back)

For add_assets_to_catalogue:

pending_analysis

analysis_complete

processing_addition

completed

Integration Points

APIs Used

  • getCatalogOrdersByProfile(email) — fetch orders by user email
  • getCatalogOrdersByRole(email, role, clientId, pageSize, cursor) — fetch with role-based filtering & pagination
  • getUserProfiles() — pre-load user profile map
  • createAssetOrder(clientId) — create asset order in DB
  • updateAssetOrderWithOrderableItems(orderId, items) — save processed items to order
  • updateAssetOrderForQuote(orderId, orderData) — finalize order with additional info & status
  • uploadQuoteAndUpdateOrder(orderId, file, name, date, notes) — upload PDF & transition status
  • acceptQuote(orderId) — mark quote as accepted, trigger variant population
  • rejectQuote(orderId, reason) — reject quote, store reason
  • callPopulateVariantFromSkuApi(orderId) — background variant fetching
  • deleteCatalogOrder(orderId, reason) — soft-delete order
  • Firebase onSnapshot() — real-time order updates

Firestore Collections

  • CatalogOrders — main orders collection
    • Fields: id, type, status, timestamp, profile, batch, list_tickets_refs, excelReportUrl, etc.

Error Handling & Edge Cases

Error Scenarios

  1. Network timeout during file upload:

    • handleFileProcessed() catches error and shows message
    • User can retry upload
  2. Invalid CSV format:

    • UploadFileStep validates columns and rows
    • Errors reported per row
  3. Duplicate batch name:

    • AdditionalInfoStep validates uniqueness
    • Error shown before submission
  4. Real-time listener failure:

    • Error logged but doesn't block page
    • Stale data remains until refresh
  5. SessionStorage stale data:

    • Checked on modal open
    • Cleared after use
  6. Multiple modal opens:

    • State properly isolated per modal
    • Closing one doesn't affect others

Performance Considerations

  • Pagination: PAGE_SIZE = 20 per load
  • Firestore listeners: One per order; consider aggregated listeners for 100+ orders
  • Debouncing: Error messages debounced to 2 seconds
  • Lazy loading: Infinite scroll fetches on scroll, not all at once

Testing Strategy

Unit Tests

// formatTimestamp
test("formatTimestamp converts Firestore timestamp", () => {
const timestamp = { seconds: 1672531200 };
expect(formatTimestamp(timestamp)).toMatch(/\d{2}\/\d{2}\/\d{4}/);
});

// getUserFullName
test("getUserFullName resolves email to full name", () => {
const profiles = {
"john@example.com": { firstName: "John", lastName: "Doe" },
};
expect(getUserFullName("john@example.com")).toBe("John Doe");
});

// getStatusColour
test("getStatusColour maps status to color", () => {
expect(getStatusColour("quote_requested")).toBe("purple");
expect(getStatusColour("quote_sent")).toBe("cyan");
});

// getActionMenuItems (RBAC)
test("Admin sees all actions for order_3d_asset in quote_requested", () => {
const record = { type: "order_3d_asset", status: "quote_requested" };
const items = getActionMenuItems(record);
expect(items.map((i) => i.key)).toContain("upload_quote");
});

test("Client cannot see upload_quote action", () => {
// Mock currentUserRole = 'Client'
const record = { type: "order_3d_asset", status: "quote_requested" };
const items = getActionMenuItems(record);
expect(items.map((i) => i.key)).not.toContain("upload_quote");
});

Integration Tests

// Full quote workflow
test("Quote lifecycle: upload → send → accept → populate", async () => {
// 1. Admin opens UploadQuoteModal
// 2. Uploads PDF, enters name & date
// 3. Confirm modal appears
// 4. Click "Send Quote"
// 5. Order status → quote_sent
// 6. Client sees TicketsModal option
// 7. Client opens QuoteResponseModal
// 8. Clicks "Accept"
// 9. Status → quote_accepted
// 10. Verify variant population API called
});

// Pagination
test("Infinite scroll loads next page on scroll", async () => {
// 1. Load first 20 orders
// 2. Scroll to bottom
// 3. loadMoreOrders() called
// 4. Next 20 orders appended to list
// 5. hasMore flag updated
});

// SessionStorage prefill
test("BulkOrderModal auto-opens with prefilled data from BulkAddModal", () => {
// 1. SessionStorage contains openBulkOrderModal=true & bulkOrderData
// 2. OrderPage mounts
// 3. BulkOrderModal visible & currentStep=1 (review)
// 4. uploadedData pre-populated
// 5. All orderable items pre-selected
});

E2E Tests

// Full order flow
test("Client bulk orders 3D assets and receives quote", async () => {
// 1. Navigate to /orders
// 2. Click "Order 3D Assets"
// 3. Upload CSV file
// 4. Process and review results
// 5. Enter batch name & order name
// 6. Request quote
// 7. Success modal appears
// 8. Verify order appears in list with status quote_requested
// 9. Admin logs in, uploads quote
// 10. Client sees status quote_sent
// 11. Client responds (accept/reject)
// 12. Verify status updates accordingly
});

Key Constants & Configuration

const PAGE_SIZE = 20; // Orders per page
const ERROR_DEBOUNCE_TIME = 2000; // ms between error messages

const CAN_DELETE_STATUSES = [
"pending_analysis",
"analysis_complete",
"processing_addition",
"working_3d_assets",
"freezing_time",
"quote_requested",
"quote_sent",
];

const CLIENT_ALLOWED_STATUSES = [
"working_3d_assets",
"freezing_time",
"quote_requested",
"quote_sent",
"quote_accepted",
"quote_rejected",
"completed",
];

const ORDER_TYPE_MAP = {
order_3d_asset: "Order Assets",
add_assets_to_catalogue: "Add Assets",
};

const STATUS_COLOUR_MAP = {
/* ... */
};
const STATUS_TEXT_MAP = {
/* ... */
};

Performance & Scaling Notes

  • Firestore listeners: Consider aggregated / sharded listeners for 1000+ orders
  • Pagination: Implement cursor-based pagination; avoid offset-based
  • Caching: Consider SWR / React Query for orders list caching
  • Lazy modals: Modal content could be lazy-loaded on open
  • Virtualization: For 1000+ rows, use react-window or similar
  • Debouncing: Already in place for error messages; add scroll debounce if needed

  • /src/pages/OrderPage.js — Main component (this file)
  • /src/components/ARSCatalog/BulkOrderModal.js — 3-step bulk order stepper
    • Steps: UploadFileStep, OrderReviewStep, AdditionalInfoStep
    • Manages asset order lifecycle: create → validate → finalize
  • /src/components/ARSCatalog/Modals/OrderDetailsModal.js — Full order detail view
  • /src/components/ARSCatalog/Modals/UploadQuoteModal.js — Admin quote upload
  • /src/components/ARSCatalog/Modals/QuoteResponseModal.js — Client quote response
  • /src/components/ARSCatalog/Modals/ReviseQuoteModal.js — Admin quote revision
  • /src/components/ARSCatalog/Modals/TicketsModal.js — Support ticket management
  • /src/components/ARSCatalog/Modals/DeleteOrderModal.js — Order deletion flow
  • /src/services/api/catalogOrdersApi.js — API client for order operations
  • /src/data/utils.jsgetSecureItem() for secure storage access
  • Firestore: CatalogOrders collection