Skip to main content
Contextual menus adapt the product catalog based on the customer’s table type and special events. A bar table shows only drinks, a dining table shows all categories, and a birthday table adds an automatic discount with animated banner.

How it works

The menu filtering logic is centralized in the useMenuLogic hook (src/lib/modules/menu/useMenuLogic.ts):
export function useMenuLogic() {
  const { tableId } = useLocalSearchParams<{ tableId?: string }>();
  const currentTable = useTableStore((s) => s.currentTable);
  const setBirthdayMode = useCartStore((s) => s.setBirthdayMode);

  // Filter products based on the table's menuType
  const products: Product[] =
    currentTable?.menuType === 'DRINKS_ONLY'
      ? PRODUCTS.filter((p) => p.category === 'DRINK')
      : PRODUCTS;

  // Group by category for section rendering
  const sections = [
    { key: 'FOOD', title: 'Platos', data: products.filter((p) => p.category === 'FOOD') },
    { key: 'SNACK', title: 'Snacks', data: products.filter((p) => p.category === 'SNACK') },
    { key: 'DRINK', title: 'Bebidas', data: products.filter((p) => p.category === 'DRINK') },
    { key: 'DESSERT', title: 'Postres', data: products.filter((p) => p.category === 'DESSERT') },
  ].filter((s) => s.data.length > 0);

  return { table: currentTable, sections, isBirthdayMode, discount };
}
Empty sections are automatically filtered out with .filter((s) => s.data.length > 0). This ensures bar tables don’t show empty “Food” or “Desserts” sections.

Full menu

Tables with menuType: 'FULL' display all product categories:
1

Product fetch

All products are loaded from PRODUCTS array (src/lib/core/mockData.ts)
2

Category grouping

Products are grouped into four sections:
  • Food: Main dishes (category: FOOD)
  • Snacks: Appetizers and sides (category: SNACK)
  • Drinks: Beverages (category: DRINK)
  • Desserts: Sweets (category: DESSERT)
3

Rendering

The menu component renders a SectionList with category headers
Example table:
{
  id: 'TABLE_HALL_05',
  displayName: 'Salon 05',
  menuType: 'FULL',
  status: 'FREE',
  specialEvent: 'NONE'
}

Drinks only

Bar tables with menuType: 'DRINKS_ONLY' show only beverages:
src/lib/modules/menu/useMenuLogic.ts
const products: Product[] =
  currentTable?.menuType === 'DRINKS_ONLY'
    ? PRODUCTS.filter((p) => p.category === 'DRINK')
    : PRODUCTS;
This filtering happens before category grouping, so the sections array only contains the “Bebidas” section. Example table:
{
  id: 'TABLE_BAR_01',
  displayName: 'Barra 01',
  menuType: 'DRINKS_ONLY',
  status: 'FREE',
  specialEvent: 'NONE'
}
Customers at drinks-only tables cannot add food to their cart. The products simply don’t appear in the menu.

Special events

Birthday mode

Tables with specialEvent: 'BIRTHDAY' trigger special behavior:
The discount is activated as soon as the table is loaded (src/lib/modules/menu/useMenuLogic.ts:22):
useEffect(() => {
  if (!currentTable) return;
  if (currentTable.specialEvent === 'BIRTHDAY') {
    setBirthdayMode(true, currentTable.discount ?? 0);
  } else {
    setBirthdayMode(false, 0);
  }
}, [currentTable?.id]);
Default discount: 15% (0.15)
Example table:
{
  id: 'TABLE_BDAY_99',
  displayName: 'Mesa Especial',
  menuType: 'FULL',
  status: 'FREE',
  specialEvent: 'BIRTHDAY',
  discount: 0.15,
  animation: 'cake'
}

Product structure

Products are defined with the Product type (src/lib/core/types.ts:23):
export interface Product {
  id: string;
  name: string;
  price: number;
  category: ProductCategory;
  image: string;
  description?: string;
}

export type ProductCategory = 'FOOD' | 'DRINK' | 'SNACK' | 'DESSERT';
Example product:
{
  id: 'PROD_001',
  name: 'Burger Clásica',
  price: 12.50,
  category: 'FOOD',
  image: require('@/assets/products/burger.jpg'),
  description: 'Carne Angus, queso cheddar, lechuga, tomate'
}

State management

The menu system uses three stores:

Table store

src/stores/useTableStore.ts
const currentTable = useTableStore((s) => s.currentTable);
Stores the active TableData object with:
  • id: QR code string
  • displayName: Human-readable table name
  • menuType: 'FULL' | 'DRINKS_ONLY'
  • specialEvent: 'NONE' | 'BIRTHDAY'
  • discount: Optional discount percentage (0.15 = 15%)

Cart store

src/stores/useCartStore.ts
interface CartState {
  items: CartItem[];
  isBirthdayMode: boolean;
  discount: number;
  total: number;
  addItem: (product: Product) => void;
  removeItem: (productId: string) => void;
}

Adding items to cart

src/stores/useCartStore.ts
addItem: (product: Product) => {
  const { items, discount } = get();
  const existing = items.find((i) => i.product.id === product.id);
  const updated = existing
    ? items.map((i) =>
        i.product.id === product.id
          ? { ...i, quantity: i.quantity + 1 }
          : i
      )
    : [...items, { product, quantity: 1 }];

  set({ items: updated, total: calcTotal(updated, discount) });
}
The cart automatically recalculates the total whenever items are added or removed, respecting the current discount if birthday mode is active.
The menu screen uses a SectionList to render categorized products:
<SectionList
  sections={sections}
  keyExtractor={(item) => item.id}
  renderItem={({ item }) => <ProductCard product={item} />}
  renderSectionHeader={({ section: { title } }) => (
    <Text style={styles.sectionHeader}>{title}</Text>
  )}
  ListHeaderComponent={
    isBirthdayMode ? <BirthdayBanner discount={discount} /> : null
  }
/>

Delivery mode menus

In delivery mode, the menu always shows the full catalog regardless of table type, since there is no table (src/components/location/ContextSwitcher.tsx:92):
if (distanceMeters > Config.restaurant.geofenceRadiusMeters) {
  setAppMode('DELIVERY');
  setServiceType('DELIVERY');
  // No table is set, so menuType filtering doesn't apply
}
The delivery catalog component loads all products directly:
const allProducts = PRODUCTS; // No filtering

Edge cases

If the user opens a deep link directly to the menu without scanning a QR code, the hook hydrates the table from route params (src/lib/modules/menu/useMenuLogic.ts:15):
const { tableId } = useLocalSearchParams<{ tableId?: string }>();

useEffect(() => {
  if (tableId && (!currentTable || currentTable.id !== tableId)) {
    setTable(tableId);
  }
}, [tableId]);

Missing table

If currentTable is null, the menu defaults to showing all products (full menu):
const products: Product[] =
  currentTable?.menuType === 'DRINKS_ONLY'
    ? PRODUCTS.filter((p) => p.category === 'DRINK')
    : PRODUCTS;
The ?. optional chaining ensures the code doesn’t crash.

Birthday mode cleanup

Birthday mode is cleared when the session ends (src/stores/useCartStore.ts:66):
resetCart: () => {
  set({
    items: [],
    total: 0,
    isBirthdayMode: false,
    discount: 0,
  });
}

Testing different menu types

1

Scan a bar table QR code

Use QR code TABLE_BAR_01 to test drinks-only menu
2

Verify only drinks appear

The menu should show only the “Bebidas” section
3

Scan a dining table QR code

Use QR code TABLE_HALL_05 to test full menu
4

Verify all categories appear

The menu should show Food, Snacks, Drinks, and Desserts
5

Scan a birthday table QR code

Use QR code TABLE_BDAY_99 to test birthday mode
6

Verify discount and banner

The menu should show:
  • Animated birthday banner at the top
  • 15% discount label
  • Reduced total in cart

Table Mode

QR scanning and table session initialization

QR Scanner

Camera-based QR code scanning

Payments

Checkout with discount calculation

Delivery Mode

Delivery orders (always full menu)