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:
order_3d_asset— Users request 3D asset creation/processing. Workflow: request → Admin quotes → Client accepts/rejects → production.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_assetsstatus 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:
| Role | View Details | Upload Quote | Respond to Quote | Revise Quote | New Ticket | Delete 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}`);
}
};
Modal Workflows
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_HTTPasynchronously to process variants - Stepper reset:
resetBulkOrderStepperprop 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:
- Admin uploads PDF quote document (max 10MB)
- Enters quote name (e.g., "Quote-2025-10-29")
- Selects quote date
- Optionally adds notes
- Confirmation modal appears
- 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:
- Client sees quote summary and details
- Clicks "Accept" or "Reject"
- If reject, optionally adds reason/notes
- On accept:
- Status →
quote_accepted - Background call to
callPopulateVariantFromSkuApi()to populate variants
- Status →
- If reject:
- Status →
quote_rejected - Admin notified to revise or handle
- Status →
5. ReviseQuoteModal (Admin response to rejection)
Trigger: "Revise Quote" action (Admin, order_3d_asset, quote_rejected status)
Workflow:
- Admin reviews rejection reason
- Uploads new PDF quote
- Status →
quote_sentagain - 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:
- Confirmation modal asks for deletion reason
- On confirm:
deleteCatalogOrder(orderId, reason)called - Order status →
deleted - 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 emailgetCatalogOrdersByRole(email, role, clientId, pageSize, cursor)— fetch with role-based filtering & paginationgetUserProfiles()— pre-load user profile mapcreateAssetOrder(clientId)— create asset order in DBupdateAssetOrderWithOrderableItems(orderId, items)— save processed items to orderupdateAssetOrderForQuote(orderId, orderData)— finalize order with additional info & statusuploadQuoteAndUpdateOrder(orderId, file, name, date, notes)— upload PDF & transition statusacceptQuote(orderId)— mark quote as accepted, trigger variant populationrejectQuote(orderId, reason)— reject quote, store reasoncallPopulateVariantFromSkuApi(orderId)— background variant fetchingdeleteCatalogOrder(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.
- Fields:
Error Handling & Edge Cases
Error Scenarios
-
Network timeout during file upload:
handleFileProcessed()catches error and shows message- User can retry upload
-
Invalid CSV format:
UploadFileStepvalidates columns and rows- Errors reported per row
-
Duplicate batch name:
AdditionalInfoStepvalidates uniqueness- Error shown before submission
-
Real-time listener failure:
- Error logged but doesn't block page
- Stale data remains until refresh
-
SessionStorage stale data:
- Checked on modal open
- Cleared after use
-
Multiple modal opens:
- State properly isolated per modal
- Closing one doesn't affect others
Performance Considerations
- Pagination:
PAGE_SIZE = 20per 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
Related Files & Architecture
/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.js—getSecureItem()for secure storage access- Firestore:
CatalogOrderscollection