Saltar al contenido principal

Restaurant Expo Screen Improvements

Overview

This document outlines the improvements made to the Restaurant Expo Screen (RestaurantExpoPage.tsx) to address performance issues, add missing features, and improve user experience.

Date

February 20, 2026

Problems Addressed

1. Performance Issues

N+1 Query Pattern

Problem: The component was making individual API calls for each order's completion status, resulting in 100+ API calls when displaying 100 orders.

Solution:

  • Added backend batch endpoint: GET /orders/station-completion/batch?orderIds=id1,id2,id3
  • Created getOrderStationCompletionBatch() service method
  • Updated frontend to use batch fetching via getRestaurantOrderStationCompletionBatch()
  • Reduced 100 API calls to 1 batch call

WebSocket Refresh Loop

Problem: When any print job updated, the component refreshed completion status for ALL orders.

Solution:

  • Modified WebSocket handler to only refresh the affected order when orderId is available
  • Falls back to batch refresh only when order ID is unknown
  • Significantly reduced unnecessary API calls

2. Missing Features

Order Timer Display

Added:

  • Real-time timer showing elapsed time since order was created
  • Color-coded timer:
    • Green: < 10 minutes
    • Amber: 10-20 minutes
    • Red: > 20 minutes
  • Auto-updates every second

Sound/Visual Alerts

Added:

  • Audio notification when orders become complete
  • Tracks completed orders to prevent duplicate sounds
  • Non-intrusive sound with 30% volume
  • Graceful fallback if audio not supported

Error Handling with Retry Logic

Added:

  • retryWithBackoff() utility function
  • Exponential backoff: 1s, 2s, 4s delays
  • Up to 3 retry attempts for failed API calls
  • Applied to both order loading and completion fetching
  • User-friendly error toasts with translated messages

3. Code Quality Improvements

Better Error Messages

  • Added toast notifications for load failures
  • Translated error messages (English & Spanish)
  • Errors no longer silently logged to console

Internationalization

  • Added missing translation key: restaurant.expo.loadError
  • English: "Failed to load orders"
  • Spanish: "Error al cargar órdenes"

Technical Implementation

Backend Changes

apps/backend/src/restaurant/interfaces/orders.controller.ts

@Get("station-completion/batch")
async getStationCompletionBatch(@Query("orderIds") orderIds: string) {
if (!orderIds) {
throw new BadRequestException("orderIds query parameter is required");
}

const orderIdArray = orderIds.split(",").filter((id) => id.trim());
if (orderIdArray.length === 0) {
return {};
}

const [error, response] = await errorFirstWrapAsync(
this.ordersService.getOrderStationCompletionBatch(orderIdArray),
);

if (error) throw Errors.from({ logger: this.logger, cause: error });
return response;
}

apps/backend/src/restaurant/application/orders.service.ts

async getOrderStationCompletionBatch(orderIds: string[]): Promise<
Record<string, OrderStationCompletion>
> {
const result: Record<string, OrderStationCompletion> = {};

await Promise.all(
orderIds.map(async (orderId) => {
try {
const completion = await this.getOrderStationCompletion(orderId);
result[orderId] = completion;
} catch (error) {
console.error(`Failed to get completion for order ${orderId}:`, error);
}
}),
);

return result;
}

Frontend Changes

apps/frontend-pwa/src/services/restaurantOrdersService.ts

export async function getRestaurantOrderStationCompletionBatch(
token: string,
orderIds: string[],
): Promise<Record<string, OrderStationCompletion>> {
if (orderIds.length === 0) return {};
const url = `${ORDERS_BASE_URL}/station-completion/batch?orderIds=${orderIds.join(",")}`;
return api<Record<string, OrderStationCompletion>>(url, token);
}

apps/frontend-pwa/src/components/forms/restaurant/RestaurantExpoPage.tsx

Key Changes:

  1. Retry Utility:
async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000,
): Promise<T> {
let lastError: Error | unknown;

for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (attempt < maxRetries) {
const delay = baseDelay * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}

throw lastError;
}
  1. Sound Alert:
function playCompletionSound() {
try {
const audio = new Audio("data:audio/wav;base64,...");
audio.volume = 0.3;
audio.play().catch((err) => console.log("Could not play sound:", err));
} catch (err) {
console.log("Sound not supported:", err);
}
}
  1. Batch Completion Loading:
const loadCompletionBatch = useCallback(
async (orderIds: string[]) => {
if (!token || orderIds.length === 0) return;

try {
const completions = await retryWithBackoff(() =>
getRestaurantOrderStationCompletionBatch(token, orderIds),
);
setCompletionByOrderId((prev) => ({
...prev,
...completions,
}));

// Check for newly completed orders and play sound
const newlyCompleted = Object.entries(completions).filter(
([orderId, completion]) =>
completion.isFullyComplete && !completedOrderIds.has(orderId),
);

if (newlyCompleted.length > 0 && soundEnabled) {
playCompletionSound();
setCompletedOrderIds((prev) => {
const next = new Set(prev);
for (const [orderId] of newlyCompleted) {
next.add(orderId);
}
return next;
});
}
} catch (err) {
console.error("Failed to load completion batch:", err);
}
},
[token, completedOrderIds, soundEnabled],
);
  1. Timer Display in ExpoOrderCard:
useEffect(() => {
const updateTimer = () => {
const sentAt = new Date(order.createdAt || Date.now());
const now = new Date();
const diffMs = now.getTime() - sentAt.getTime();
const minutes = Math.floor(diffMs / 60000);
const seconds = Math.floor((diffMs % 60000) / 1000);
setElapsedTime(`${minutes}:${seconds.toString().padStart(2, "0")}`);
};

updateTimer();
const interval = setInterval(updateTimer, 1000);
return () => clearInterval(interval);
}, [order.createdAt]);

const getTimerColor = () => {
const sentAt = new Date(order.createdAt || Date.now());
const now = new Date();
const minutes = Math.floor((now.getTime() - sentAt.getTime()) / 60000);

if (minutes < 10) return "text-green-600 dark:text-green-400";
if (minutes < 20) return "text-amber-600 dark:text-amber-400";
return "text-red-600 dark:text-red-400";
};
  1. Optimized WebSocket Handler:
useRestaurantWebSocket({
onOrderUpdated: (order: RestaurantOrder) => {
if (order.status !== OrderStatus.SENT_TO_KITCHEN) {
setOrders((prev) => prev.filter((o) => o.id !== order.id));
setCompletedOrderIds((prev) => {
const next = new Set(prev);
next.delete(order.id);
return next;
});
return;
}
setOrders((prev) => {
const existing = prev.find((o) => o.id === order.id);
if (existing) {
return prev.map((o) => (o.id === order.id ? order : o));
}
return [...prev, order];
});
// Refresh completion for this specific order
loadCompletion(order.id);
},
onPrintJobUpdated: (payload: {
printJob: { id: string; orderId?: string };
stationId: string;
}) => {
// Only refresh the affected order if we know which one it is
if (payload.printJob.orderId) {
loadCompletion(payload.printJob.orderId);
} else {
// Fallback: refresh all orders (but use batch)
const orderIds = orders.map((o) => o.id);
if (orderIds.length > 0) {
loadCompletionBatch(orderIds);
}
}
},
});

Performance Impact

Before

  • API Calls on Load: 1 (orders) + 100 (individual completions) = 101 calls
  • API Calls on Print Job Update: 100 calls (all orders refreshed)
  • Total for 10 print updates: ~1,000 API calls

After

  • API Calls on Load: 1 (orders) + 1 (batch completions) = 2 calls
  • API Calls on Print Job Update: 1 call (only affected order)
  • Total for 10 print updates: ~12 calls

Result: ~98% reduction in API calls

User Experience Improvements

  1. Faster Load Times: Batch fetching reduces initial load time significantly
  2. Real-time Feedback: Timer shows how long orders have been waiting
  3. Audio Alerts: Staff notified immediately when orders are ready
  4. Better Error Handling: Failed requests retry automatically
  5. Visual Clarity: Color-coded timers help prioritize urgent orders

Future Enhancements (Not Implemented)

  • Pagination UI for handling > 100 orders
  • Sorting/filtering options (by table, time, priority)
  • Configurable sound alerts (enable/disable, volume control)
  • Order detail modal showing items
  • Customizable timer thresholds
  • Offline support with service workers

Testing Recommendations

  1. Test with 100+ orders to verify batch performance
  2. Verify sound plays on order completion
  3. Test retry logic by simulating network failures
  4. Verify timer accuracy over extended periods
  5. Test WebSocket reconnection scenarios
  6. Verify translations in both English and Spanish

Breaking Changes

None. All changes are backward compatible.

Migration Notes

No migration required. The batch endpoint returns the same data structure as individual calls, just grouped by order ID.