{ "name": "categories", "type": "base", "system": false, "listRule": "@request.auth.id != ''", "viewRule": "@request.auth.id != ''", "createRule": "@request.auth.id != ''", "updateRule": "@request.auth.id != ''", "deleteRule": "@request.auth.id != ''", "fields": [ { "name": "name", "type": "text", "required": true, "min": 1, "max": 100, "pattern": "" }, { "name": "slug", "type": "text", "required": true, "min": 1, "max": 100, "pattern": "^[a-z0-9_-]+$" }, { "name": "match_keywords", "type": "text", "required": false, "max": 500, "pattern": "", "help": "Comma-separated keywords scanned against item description + SKU for category assignment. Lowercased before match." }, { "name": "match_supplier_tags", "type": "text", "required": false, "max": 500, "pattern": "", "help": "Comma-separated supplier supply_tags that map to this category. Used when item has no catalogue match. Lowercased before match." }, { "name": "match_brands", "type": "text", "required": false, "max": 200, "pattern": "", "help": "Comma-separated price_list.brand values that auto-assign to this category. Highest-priority signal — checked first." }, { "name": "color", "type": "text", "required": false, "max": 9, "pattern": "", "help": "Hex colour for the tab. E.g. #F2AF58" }, { "name": "display_order", "type": "number", "required": false, "min": 0, "noDecimal": true, "help": "Lower numbers render first. UNKNOWN is conventionally 9999 so it sinks to the right." }, { "name": "is_active", "type": "bool", "required": false, "help": "Inactive categories are hidden from the sprint tab bar but existing quote_item rows still point at them." } ], "indexes": [ "CREATE UNIQUE INDEX `idx_categories_slug` ON `categories` (`slug`)", "CREATE INDEX `idx_categories_display_order` ON `categories` (`display_order`)" ] }