Appearance
Menu Categories
Overview
Categories organize menu items into logical groups. They determine how items are displayed and navigated in Online Ordering and Kiosk.
Key Purpose: Organize menu items for easy customer navigation.
Purpose
This page lets you create, edit, and organize menu categories that group items into sections customers browse on online ordering and kiosk channels.
Key Concepts
- Category: A named grouping of menu items with an optional description, an optional parent category, and an optional image. Items reference a category through their own
category_id. - Parent Category: A category can optionally be nested under a parent category via
parent_id, giving one level of nesting (subcategories). Only root (top-level) categories can be chosen as parents. - Multi-language Details: Category name and description are stored in a
detailsmap keyed by language, withdefaultas the primary language entry. - Advance-Order Rule: A category carries two optional fields,
lead_time_days(how many days ahead a customer must order) andavailable_pickup_days(which weekdays pickup is allowed). Items inherit them; an item that sets its own value overrides the inherited one. Both default to "no restriction" —0days and an empty weekday list. (Verified:app/RawModels/Category.phplines 64-65 and 75-84;Item::applyCategoryPickupRuleapp/RawModels/Item.phplines 338-349.)
Actions
Create Category
Add a new category with a name, optional description, optional parent category, and an optional image. The category is saved and synced to all active third-party integrations (Square, MplusKassa, etc.).
Edit Category
Update a category's name, description, parent category, or image. After saving, the cache for every item in the category is cleared so changes are reflected immediately, and the change is synced to active integrations.
Delete Category
Remove a category permanently. Deletion is blocked if the category still contains items -- all items must be reassigned or removed first.
Location
- Backoffice Route:
/menus/categories - Backend Controller:
app/Http/Controllers/Api/BackOffice/CategoryController.php - Page:
src/pages/menus/categories/index.vue→ View:src/views/reporting-categories/ReportingCategories.vue - Form:
src/components/forms/items/CategoryForm.vue
Fields
Category Name
| Property | Value |
|---|---|
| Field ID | details.name |
| Label | Category Name |
| Type | Text |
| Required | Yes |
| Validation | Required string (no max length enforced) |
Description: Name of the category shown to customers as a menu section header or navigation tab.
Best Practices:
- Keep names short (1-3 words)
- Use familiar terms customers expect
- Be consistent with industry standards
Customer Impact:
- Online Ordering: Tab/section header in menu
- Kiosk: Large category buttons
Examples:
- "Starters"
- "Main Courses"
- "Pizzas"
- "Burgers"
- "Drinks"
- "Desserts"
- "Sides"
- "Kids Menu"
Category Description
| Property | Value |
|---|---|
| Field ID | details.description |
| Label | Description |
| Type | Textarea |
| Required | No |
| Validation | Optional string (no max length enforced) |
Description: Optional description shown below category name.
Customer Impact:
- Online Ordering: Shown as subtitle under category name
- Kiosk: May be displayed on category selection screen
Examples:
- "Start your meal with our delicious appetizers"
- "Hand-stretched, wood-fired pizzas"
- "Freshly made desserts"
Category Image
| Property | Value |
|---|---|
| Field ID | image |
| Label | Category Image |
| Type | Image Upload |
| Required | No |
| Validation | JPG, PNG, SVG, WebP; max 10MB |
Description: Image representing the category. Used in visual menu layouts.
Customer Impact:
- Online Ordering: Category thumbnail (if visual menu enabled)
- Kiosk: Large category image on selection screen
Best Practices:
- Use appetizing food photos
- Show representative items from category
- Consistent style across categories
Parent Category
| Property | Value |
|---|---|
| Field ID | parent_id |
| Label | Parent Category |
| Type | Select |
| Required | No |
Description: Optional parent category to nest this category underneath, creating a subcategory. The dropdown lists only root (top-level) categories and excludes the category being edited, so the hierarchy is limited to one level of nesting and a category can never be its own parent.
Business Logic:
- No parent selected → Root (top-level) category
- Parent selected → Subcategory nested under that parent
Order Days in Advance (lead time)
| Property | Value |
|---|---|
| Field ID | lead_time_days |
| Label | Order days in advance |
| Type | Number (suffix "days") |
| Required | No |
| Default | 0 |
| Validation | nullable|integer|min:0|max:365 |
Description: How many days ahead a customer must order the items in this category. 0 means same-day ordering is allowed — i.e. no restriction. Items in this category inherit this value; an item that sets its own lead time overrides it.
(Verified: form field upvendo-backoffice src/components/forms/items/CategoryForm.vue lines 132-147; store default src/store/modules/category.ts line 14, coerced on load line 90; validation upvendo-backend app/Http/Requests/BackOffice/Category/StoreCategoryRequest.php line 50.)
Available Pickup Days
| Property | Value |
|---|---|
| Field ID | available_pickup_days |
| Label | Available pickup days |
| Type | Multi-select (weekday names, chips) |
| Required | No |
| Default | [] (empty = every day) |
| Validation | nullable|array; each entry must be one of Carbon::getDays() — Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday |
Description: The weekdays on which items in this category may be picked up. Leaving it empty allows every day. The values are full English weekday names, not codes or indices — the back-office option list is built from pickupWeekdayOptions() specifically to stay aligned with the backend's Carbon::getDays(). Items in this category inherit this list; an item that sets its own pickup days overrides it.
(Verified: form field CategoryForm.vue lines 148-161, options via weekdayOptions line 27 → pickupWeekdayOptions() in src/utils/dateUtils.ts lines 76-79; store default src/store/modules/category.ts line 15, coerced on load line 91; validation StoreCategoryRequest.php lines 51-52.)
Advance-Order Inheritance
The advance-order rule is defined on a category and inherited by its items, so you can set it once per category rather than on every item.
- The item wins where it sets a value; the category fills in only what the item leaves unset. The two fields are independent — an item may define its own pickup days and still inherit the category's lead time. (Verified:
Item::applyCategoryPickupRule, upvendo-backendapp/RawModels/Item.phplines 338-349.) - A stored
0/ empty list reads as "inherit", not as "no restriction". Because an item's0lead time and empty pickup-day list cannot be told apart from "never set", an item cannot currently opt out of its category's rule back down to same-day ordering. (Verified: the same method; the limitation is called out in its docblock.) - Inheritance walks the whole ancestor chain, nearest first. A subcategory that sets nothing inherits from its parent, and so on to the root. The first ancestor that defines a non-zero lead time supplies it; likewise the first that defines a non-empty pickup-day list. (Verified:
CategoryRepository::getEffectivePickupRuleMaplines 500-552.) - The fold is in-memory and never persisted. Nothing is written onto the item document; the rule is resolved at menu-build and order-validation time. (Verified:
applyCategoryPickupRuledocblock; call sitesapp/RawModels/Menu/MenuLoadContext.phplines 162, 252 and 330-356, andapp/Repositories/ItemRepository.phplines 147-188.) - Editing a category refreshes its whole subtree. Because the rule cascades, saving a category busts the item caches and reloads the menus for that category and every descendant — nothing on the items themselves changes, which is exactly why the push has to be explicit. (Verified:
CategoryService::refreshSubtreeMenus,app/Services/BackOffice/CategoryService.phplines 144-147 and 165+.) - The API enforces it at order submission. When an order is submitted, the backend checks every item against its effective rule (its own, or its category's where unset) and rejects the order with a 422 and ":item is not available for the selected pickup date." naming the first offending item. (Verified:
ItemRepository::firstUnavailableForPickuplines 147-188 andItem::isAvailableForPickuplines 362-372, called fromOnlineOrderingOrchestrator::assertItemsAvailableForPickupline 1642 andKioskOrchestratorline 1057.)
NOTE
How the storefront and kiosk display an item that fails the rule is a channel concern and is not verified here. This section covers the back-office fields, the inheritance resolution, and the server-side enforcement only.
Customer Impact
Online Ordering
- Navigation: Categories as tabs or sidebar sections
- Scrolling: Scroll to category section
- Filtering: Click category to filter items
- Visual: Category images (if enabled)
Kiosk
- Home Screen: Large category buttons
- Selection: Tap category to see items
- Navigation: Back to categories button
- Visual: Category images prominent
Receipt
- Items grouped by category (optional)
- Category name may appear as section header
Relations
Depends On
- Parent Category: A subcategory references a parent category via
parent_id(one level of nesting).
Affects
- Menu Items: Items reference a category through their own
category_id. - Order Capacity: Category-specific limits reference categories.
- Reports: Sales by category.
Related Features
Business Rules
- A category cannot be deleted if it still contains items; the system returns a 400 error with "Delete failed since the category is currently being used."
- Category images are processed through media upload and stored as content entities; the resulting
content_idis saved on the category record. - When a category is updated, the item cache for every item in that category is cleared to ensure ordering channels display the latest data. Because the advance-order rule cascades, the refresh covers the category's whole subtree, not just its direct items. (Verified:
CategoryService::refreshSubtreeMenus.) - A category's advance-order rule (
lead_time_days,available_pickup_days) is inherited by its items and by its descendant categories. An item that sets its own value overrides the inherited one, per field. A stored0/ empty list on an item means "inherit", not "no restriction". (Verified:Item::applyCategoryPickupRule,CategoryRepository::getEffectivePickupRuleMap.) lead_time_daysis validated asnullable|integer|min:0|max:365;available_pickup_daysmust be an array of full English weekday names fromCarbon::getDays(). The same two rules apply on the item validator. (Verified:StoreCategoryRequest.phplines 50-52,StoreItemRequest.phplines 146-148.)- Category creation, update, and deletion are synced to all active third-party integrations (Square, MplusKassa, etc.) using the category model type and the corresponding event constant.
- Categories synced one-way from a POS are protected: Kassanet-synced categories cannot be modified or deleted locally, and read-only inbound POS systems (e.g. ShopCaisse) cannot create or delete categories from the back office.
- The
detailsfield merges existing language translations with the new update, preserving translations for languages not included in the current request.
FAQs
How do I create a subcategory? When creating or editing a category, choose a Parent Category to nest it underneath. Only root (top-level) categories can be selected as parents, so the hierarchy is limited to one level of nesting.
Is there a limit to how many categories I can create? There is no hard limit in the system, but keeping the number manageable is recommended for a good customer browsing experience.
What happens if I try to delete a category that still has items? Deletion is blocked. You must reassign or remove every item first, otherwise the system returns "Delete failed since the category is currently being used."
"What fields can I actually set when creating a category?" A category has six editable fields on the create/edit form: Category Name (required), Description (optional), Parent Category (optional), Order days in advance (
lead_time_days, optional), Available pickup days (available_pickup_days, optional), and an image. There's no menu picker, availability schedule, or tax-rate field on the form itself. Visibility is handled separately — the category list has per-row "Mark Unavailable" and "Hide" toggles (astatusof Active / Unavailable / Hidden that cascades to subcategories), not a field on the form. (Verified:src/components/forms/items/CategoryForm.vue;StoreCategoryRequest::rules().)"How do I make a whole category order-ahead only (e.g. cakes need 2 days' notice)?" Set Order days in advance on the category. Every item in it inherits that lead time, so you don't have to edit each item. If some items in the category need a different lead time, set it on those items — an item's own value overrides the category's.
"How do I restrict a category to certain pickup days?" Use Available pickup days on the category and pick the weekdays. Leaving it empty means every day. Items inherit the list; an item with its own pickup days overrides it.
"I set a lead time on a category but one item should still be same-day — how?" This is currently not possible. An item's stored
0lead time is indistinguishable from "not set", so it reads as "inherit" rather than as an opt-out. The workaround is to move that item to a category without a lead time. (Verified: theapplyCategoryPickupRuledocblock records this as a known limitation onapp/RawModels/Item.phplines 338-349.)"Does a subcategory inherit its parent's advance-order rule?" Yes. Resolution walks up the ancestor chain nearest-first, so a subcategory that sets nothing takes the first ancestor that does. Each of the two fields resolves independently. (Verified:
CategoryRepository::getEffectivePickupRuleMap.)"How do I create a subcategory / nest categories?" When creating or editing a category, choose a Parent Category to nest it underneath. The list then shows the hierarchy as an expandable tree, and only root (top-level) categories can be selected as parents.
"Why can't I pick a parent category that is itself a subcategory?" The parent dropdown only lists root categories and excludes the category you're editing, so the hierarchy is limited to one level of nesting and a category can never be its own parent.
"What image formats and size are allowed for a category image?" Category images accept JPG, PNG, SVG, and WebP up to 10 MB.
"Why does a category show a higher item count than the items directly inside it?" Each category in the tree shows both its direct item count and an aggregate count that adds up the items in all of its subcategories, so a parent reflects everything beneath it.
"Why can't I delete a category?" Deletion is blocked while the category still has items assigned. Reassign or remove every item first, otherwise you get "Delete failed since the category is currently being used."
"I added a translation but my edit didn't wipe the other languages — is that expected?" Yes. Category name and description are stored per language, and saving merges your update with existing translations, so languages you didn't touch are preserved.
"Why is the Name (or Parent) field greyed out on some categories?" Categories synced from a POS are locked: Kassanet (Hendrickx / Vanhoutte) categories lock Name and Parent, and MplusKassa categories lock Name and Description, because those fields are owned by the POS and would be overwritten on the next sync. (Lightspeed K-Series has no category field-locking.)
"I edited a category but nothing changed for customers — why?" After saving, the system clears the cache for every item in that category so ordering channels pick up the change. If you still don't see it, confirm the save succeeded and that the items belong to that category.
"Do my categories get pushed to my connected POS / Square?" Yes. Creating, updating, or deleting a category triggers a sync to all active integrations (Square, MplusKassa, etc.); read-only inbound POS systems like Kassanet and ShopCaisse are protected and can't be created/edited/deleted from the back office.
"I can't find the Create Category button — why?" It only appears if you have the create-category permission and the location's POS allows catalog creation; a read-only inbound POS hides it.
Troubleshooting
Problem: Category not showing in menu
Causes:
- Category has no items
- All items in category unavailable
Solutions:
- Add items to category
- Enable item availability
Problem: Items in wrong category
Causes:
- Item assigned to wrong category
- Category names confusing
Solutions:
- Edit item → Change category
- Rename categories for clarity
Problem: Edited category but nothing changed for customers
Causes:
- Item cache not yet reflected
- Items not actually assigned to this category
Solutions:
- Confirm the save succeeded (the system clears the cache for every item in the category on save)
- Verify the items belong to that category
Examples
Single Category (create payload)
The store/update endpoint accepts a details object (with name and optional description), an optional parent_id, an optional image, and the optional advance-order pair lead_time_days / available_pickup_days:
json
{
"details": {
"name": "Starters",
"description": "Start your meal with our delicious appetizers"
},
"parent_id": null
}Category with an advance-order rule (create payload)
Items in this category must be ordered at least 2 days ahead and can only be picked up on Friday or Saturday, unless the item overrides one of the two:
json
{
"details": {
"name": "Celebration Cakes",
"description": "Made to order"
},
"parent_id": null,
"lead_time_days": 2,
"available_pickup_days": ["Friday", "Saturday"]
}Standard Restaurant Categories
json
{
"categories": [
{ "details": { "name": "Starters" } },
{ "details": { "name": "Salads" } },
{ "details": { "name": "Main Courses" } },
{ "details": { "name": "Sides" } },
{ "details": { "name": "Desserts" } },
{ "details": { "name": "Drinks" } }
]
}Pizza Restaurant
json
{
"categories": [
{ "details": { "name": "Classic Pizzas", "description": "Our traditional favorites" } },
{ "details": { "name": "Specialty Pizzas", "description": "Chef's special creations" } },
{ "details": { "name": "Build Your Own", "description": "Create your perfect pizza" } },
{ "details": { "name": "Pasta" } },
{ "details": { "name": "Salads" } },
{ "details": { "name": "Sides & Extras" } },
{ "details": { "name": "Desserts" } },
{ "details": { "name": "Drinks" } }
]
}Subcategories (one level of nesting)
A child category references its parent via parent_id:
json
{
"categories": [
{ "details": { "name": "Drinks" } },
{ "details": { "name": "Hot Drinks" }, "parent_id": "<drinks_category_id>" },
{ "details": { "name": "Cold Drinks" }, "parent_id": "<drinks_category_id>" }
]
}