@fayz-ai/plugin-menu 0.9.0-next.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,429 +0,0 @@
1
- import { createFayzClient, type FayzClientOptions, type FayzTableFilter } from '@fayz-ai/sdk'
2
- import type {
3
- CreateMenuCategoryInput,
4
- CreateMenuItemInput,
5
- MenuCategory,
6
- MenuItem,
7
- MenuItemQuery,
8
- MenuItemStatus,
9
- MenuSummary,
10
- ModifierGroup,
11
- PaginatedResult,
12
- UpdateMenuItemInput,
13
- } from '../types'
14
- import type { MenuDataProvider } from './types'
15
-
16
- export interface FayzMenuProviderOptions extends FayzClientOptions {
17
- projectId?: string
18
- schema?: string
19
- runtime?: boolean
20
- categoriesTable?: string
21
- productsTable?: string
22
- categoryKind?: string
23
- }
24
-
25
- interface CategoryRow {
26
- id: string
27
- tenant_id?: string | null
28
- kind?: string | null
29
- name: string
30
- slug?: string | null
31
- parent_id?: string | null
32
- icon?: string | null
33
- color?: string | null
34
- sort_order?: number | null
35
- is_active?: boolean | null
36
- metadata?: Record<string, unknown> | null
37
- created_at?: string | null
38
- updated_at?: string | null
39
- }
40
-
41
- interface ProductRow {
42
- id: string
43
- tenant_id?: string | null
44
- category_id?: string | null
45
- name: string
46
- description?: string | null
47
- sku?: string | null
48
- price?: number | string | null
49
- cost?: number | string | null
50
- currency?: string | null
51
- image_url?: string | null
52
- status?: string | null
53
- is_active?: boolean | null
54
- tags?: string[] | null
55
- metadata?: Record<string, unknown> | null
56
- created_at?: string | null
57
- updated_at?: string | null
58
- }
59
-
60
- const DEFAULT_CATEGORY_KIND = 'menu_category'
61
- const MENU_METADATA_KEY = 'fayzMenu'
62
-
63
- function nowIso(): string {
64
- return new Date().toISOString()
65
- }
66
-
67
- function slugify(value: string): string {
68
- return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
69
- }
70
-
71
- function numberValue(value: number | string | null | undefined): number {
72
- if (typeof value === 'number') return value
73
- if (typeof value === 'string') return Number(value) || 0
74
- return 0
75
- }
76
-
77
- function menuMetadata(row: ProductRow): Record<string, unknown> {
78
- const raw = row.metadata?.[MENU_METADATA_KEY]
79
- return raw && typeof raw === 'object' && !Array.isArray(raw) ? raw as Record<string, unknown> : {}
80
- }
81
-
82
- function rowToCategory(row: CategoryRow, itemCount = 0): MenuCategory {
83
- return {
84
- id: row.id,
85
- name: row.name,
86
- parentId: row.parent_id ?? undefined,
87
- icon: row.icon ?? undefined,
88
- color: row.color ?? undefined,
89
- sortOrder: row.sort_order ?? 0,
90
- isActive: row.is_active ?? true,
91
- itemCount,
92
- tenantId: row.tenant_id ?? 'runtime-tenant',
93
- createdAt: row.created_at ?? nowIso(),
94
- updatedAt: row.updated_at ?? row.created_at ?? nowIso(),
95
- }
96
- }
97
-
98
- function rowToItem(row: ProductRow, categoryName?: string): MenuItem {
99
- const metadata = menuMetadata(row)
100
- const status = (metadata.status ?? row.status ?? 'available') as MenuItemStatus
101
- return {
102
- id: row.id,
103
- name: row.name,
104
- description: row.description ?? undefined,
105
- price: numberValue(row.price),
106
- cost: numberValue(row.cost),
107
- currency: row.currency ?? 'BRL',
108
- sku: row.sku ?? undefined,
109
- imageUrl: row.image_url ?? undefined,
110
- categoryId: row.category_id ?? undefined,
111
- categoryName,
112
- status,
113
- isActive: row.is_active ?? true,
114
- prepTimeMinutes: typeof metadata.prepTimeMinutes === 'number' ? metadata.prepTimeMinutes : undefined,
115
- allergens: Array.isArray(metadata.allergens) ? metadata.allergens as string[] : undefined,
116
- dietaryTags: Array.isArray(metadata.dietaryTags) ? metadata.dietaryTags as string[] : row.tags ?? undefined,
117
- availableForPos: typeof metadata.availableForPos === 'boolean' ? metadata.availableForPos : true,
118
- availableForDelivery: typeof metadata.availableForDelivery === 'boolean' ? metadata.availableForDelivery : true,
119
- deliveryPrice: typeof metadata.deliveryPrice === 'number' ? metadata.deliveryPrice : undefined,
120
- sortOrder: typeof metadata.sortOrder === 'number' ? metadata.sortOrder : 0,
121
- isFeatured: typeof metadata.isFeatured === 'boolean' ? metadata.isFeatured : false,
122
- modifierGroupIds: Array.isArray(metadata.modifierGroupIds) ? metadata.modifierGroupIds as string[] : undefined,
123
- metadata: row.metadata ?? {},
124
- tenantId: row.tenant_id ?? 'runtime-tenant',
125
- createdAt: row.created_at ?? nowIso(),
126
- updatedAt: row.updated_at ?? row.created_at ?? nowIso(),
127
- }
128
- }
129
-
130
- function categoryPayload(input: CreateMenuCategoryInput, kind: string): Record<string, unknown> {
131
- return {
132
- kind,
133
- name: input.name,
134
- slug: slugify(input.name),
135
- parent_id: input.parentId ?? null,
136
- icon: input.icon ?? null,
137
- color: input.color ?? null,
138
- sort_order: input.sortOrder ?? 0,
139
- is_active: true,
140
- }
141
- }
142
-
143
- function itemMetadata(input: Partial<CreateMenuItemInput>, existing?: Record<string, unknown>): Record<string, unknown> {
144
- return {
145
- ...(existing ?? {}),
146
- [MENU_METADATA_KEY]: {
147
- ...((existing?.[MENU_METADATA_KEY] && typeof existing[MENU_METADATA_KEY] === 'object') ? existing[MENU_METADATA_KEY] as Record<string, unknown> : {}),
148
- status: input.status,
149
- prepTimeMinutes: input.prepTimeMinutes,
150
- allergens: input.allergens,
151
- dietaryTags: input.dietaryTags,
152
- availableForPos: input.availableForPos ?? true,
153
- availableForDelivery: input.availableForDelivery ?? true,
154
- deliveryPrice: input.deliveryPrice,
155
- isFeatured: input.isFeatured ?? false,
156
- modifierGroupIds: input.modifierGroupIds,
157
- },
158
- }
159
- }
160
-
161
- function itemCreatePayload(input: CreateMenuItemInput): Record<string, unknown> {
162
- return {
163
- name: input.name,
164
- description: input.description ?? null,
165
- price: input.price,
166
- cost: input.cost ?? null,
167
- currency: 'BRL',
168
- image_url: input.imageUrl ?? null,
169
- category_id: input.categoryId ?? null,
170
- status: input.status === 'hidden' ? 'archived' : 'active',
171
- is_active: input.status !== 'hidden',
172
- tags: input.dietaryTags ?? [],
173
- metadata: itemMetadata(input),
174
- }
175
- }
176
-
177
- function itemUpdatePayload(input: UpdateMenuItemInput, existing?: MenuItem): Record<string, unknown> {
178
- const payload: Record<string, unknown> = {}
179
- if (input.name !== undefined) payload.name = input.name
180
- if (input.description !== undefined) payload.description = input.description
181
- if (input.price !== undefined) payload.price = input.price
182
- if (input.cost !== undefined) payload.cost = input.cost
183
- if (input.imageUrl !== undefined) payload.image_url = input.imageUrl
184
- if (input.categoryId !== undefined) payload.category_id = input.categoryId
185
- if (input.status !== undefined) {
186
- payload.status = input.status === 'hidden' ? 'archived' : 'active'
187
- payload.is_active = input.status !== 'hidden'
188
- }
189
- if (input.dietaryTags !== undefined) payload.tags = input.dietaryTags
190
- payload.metadata = itemMetadata(input, existing?.metadata)
191
- return payload
192
- }
193
-
194
- function filtersFromQuery(query: MenuItemQuery): FayzTableFilter[] {
195
- const filters: FayzTableFilter[] = [{ column: 'is_active', operator: 'eq', value: true }]
196
- if (query.categoryId) filters.push({ column: 'category_id', operator: 'eq', value: query.categoryId })
197
- return filters
198
- }
199
-
200
- function matchesClientQuery(item: MenuItem, query: MenuItemQuery): boolean {
201
- if (query.status) {
202
- const statuses = Array.isArray(query.status) ? query.status : [query.status]
203
- if (!statuses.includes(item.status)) return false
204
- }
205
- if (query.availableForPos !== undefined && item.availableForPos !== query.availableForPos) return false
206
- if (query.availableForDelivery !== undefined && item.availableForDelivery !== query.availableForDelivery) return false
207
- if (query.isFeatured !== undefined && item.isFeatured !== query.isFeatured) return false
208
- if (query.search) {
209
- const search = query.search.toLowerCase()
210
- return item.name.toLowerCase().includes(search)
211
- || (item.description?.toLowerCase().includes(search) ?? false)
212
- }
213
- return true
214
- }
215
-
216
- function paginate<T>(items: T[], page?: number, pageSize?: number): PaginatedResult<T> {
217
- const currentPage = page ?? 1
218
- const limit = pageSize ?? 50
219
- const start = (currentPage - 1) * limit
220
- return { data: items.slice(start, start + limit), total: items.length }
221
- }
222
-
223
- export function createFayzMenuProvider(options: FayzMenuProviderOptions = {}): MenuDataProvider {
224
- const client = createFayzClient(options)
225
- const projectId = options.projectId
226
- const schema = options.schema
227
- const runtime = options.runtime
228
- const categoriesTable = options.categoriesTable ?? 'categories'
229
- const productsTable = options.productsTable ?? 'products'
230
- const categoryKind = options.categoryKind ?? DEFAULT_CATEGORY_KIND
231
-
232
- const tableOptions = { projectId, schema, runtime }
233
-
234
- async function getCategoriesRaw(): Promise<CategoryRow[]> {
235
- const response = await client.data.listRows<CategoryRow>({
236
- ...tableOptions,
237
- table: categoriesTable,
238
- filters: [
239
- { column: 'kind', operator: 'eq', value: categoryKind },
240
- { column: 'is_active', operator: 'eq', value: true },
241
- ],
242
- sortColumn: 'sort_order',
243
- sortDirection: 'asc',
244
- limit: 500,
245
- })
246
- return response.rows
247
- }
248
-
249
- async function getItemsRaw(): Promise<ProductRow[]> {
250
- const response = await client.data.listRows<ProductRow>({
251
- ...tableOptions,
252
- table: productsTable,
253
- filters: [{ column: 'is_active', operator: 'eq', value: true }],
254
- sortColumn: 'name',
255
- sortDirection: 'asc',
256
- limit: 1000,
257
- })
258
- return response.rows.filter((row) => {
259
- const metadata = menuMetadata(row)
260
- return metadata.kind === 'menu_item' || metadata.status != null || row.category_id != null
261
- })
262
- }
263
-
264
- async function getCategoryMap() {
265
- const categories = await getCategoriesRaw()
266
- return new Map(categories.map((category) => [category.id, category]))
267
- }
268
-
269
- async function getItems(): Promise<MenuItem[]> {
270
- const [rows, categoryMap] = await Promise.all([getItemsRaw(), getCategoryMap()])
271
- return rows.map((row) => rowToItem(row, row.category_id ? categoryMap.get(row.category_id)?.name : undefined))
272
- .sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name))
273
- }
274
-
275
- return {
276
- async getCategories() {
277
- const [categories, items] = await Promise.all([getCategoriesRaw(), getItemsRaw()])
278
- const counts = new Map<string, number>()
279
- for (const item of items) {
280
- if (item.category_id) counts.set(item.category_id, (counts.get(item.category_id) ?? 0) + 1)
281
- }
282
- return categories.map((category) => rowToCategory(category, counts.get(category.id) ?? 0))
283
- },
284
-
285
- async getCategoryById(id: string) {
286
- const response = await client.data.listRows<CategoryRow>({
287
- ...tableOptions,
288
- table: categoriesTable,
289
- filters: [{ column: 'id', operator: 'eq', value: id }],
290
- limit: 1,
291
- })
292
- return response.rows[0] ? rowToCategory(response.rows[0]) : null
293
- },
294
-
295
- async createCategory(input: CreateMenuCategoryInput) {
296
- const row = await client.data.createRow<CategoryRow>({
297
- ...tableOptions,
298
- table: categoriesTable,
299
- row: categoryPayload(input, categoryKind),
300
- })
301
- return rowToCategory(row)
302
- },
303
-
304
- async updateCategory(id: string, data: Partial<MenuCategory>) {
305
- const row = await client.data.updateRow<CategoryRow>({
306
- ...tableOptions,
307
- table: categoriesTable,
308
- primaryKeys: { id },
309
- row: {
310
- name: data.name,
311
- parent_id: data.parentId,
312
- icon: data.icon,
313
- color: data.color,
314
- sort_order: data.sortOrder,
315
- is_active: data.isActive,
316
- },
317
- })
318
- return rowToCategory(row)
319
- },
320
-
321
- async deleteCategory(id: string) {
322
- await client.data.updateRow<CategoryRow>({
323
- ...tableOptions,
324
- table: categoriesTable,
325
- primaryKeys: { id },
326
- row: { is_active: false },
327
- })
328
- },
329
-
330
- async reorderCategories(ids: string[]) {
331
- await Promise.all(ids.map((id, sortOrder) => this.updateCategory(id, { sortOrder })))
332
- },
333
-
334
- async getMenuItems(query: MenuItemQuery) {
335
- const items = (await getItems()).filter((item) => matchesClientQuery(item, query))
336
- return paginate(items, query.page, query.pageSize)
337
- },
338
-
339
- async getMenuItemById(id: string) {
340
- const response = await client.data.listRows<ProductRow>({
341
- ...tableOptions,
342
- table: productsTable,
343
- filters: [{ column: 'id', operator: 'eq', value: id }],
344
- limit: 1,
345
- })
346
- if (!response.rows[0]) return null
347
- const categoryMap = await getCategoryMap()
348
- const row = response.rows[0]
349
- return rowToItem(row, row.category_id ? categoryMap.get(row.category_id)?.name : undefined)
350
- },
351
-
352
- async createMenuItem(input: CreateMenuItemInput) {
353
- const row = await client.data.createRow<ProductRow>({
354
- ...tableOptions,
355
- table: productsTable,
356
- row: itemCreatePayload(input),
357
- })
358
- const categoryMap = await getCategoryMap()
359
- return rowToItem(row, row.category_id ? categoryMap.get(row.category_id)?.name : undefined)
360
- },
361
-
362
- async updateMenuItem(id: string, data: UpdateMenuItemInput) {
363
- const existing = await this.getMenuItemById(id)
364
- const row = await client.data.updateRow<ProductRow>({
365
- ...tableOptions,
366
- table: productsTable,
367
- primaryKeys: { id },
368
- row: itemUpdatePayload(data, existing ?? undefined),
369
- })
370
- const categoryMap = await getCategoryMap()
371
- return rowToItem(row, row.category_id ? categoryMap.get(row.category_id)?.name : undefined)
372
- },
373
-
374
- async deleteMenuItem(id: string) {
375
- await client.data.updateRow<ProductRow>({
376
- ...tableOptions,
377
- table: productsTable,
378
- primaryKeys: { id },
379
- row: { is_active: false },
380
- })
381
- },
382
-
383
- async toggleAvailability(id: string, field: 'status' | 'availableForPos' | 'availableForDelivery', value: unknown) {
384
- return this.updateMenuItem(id, { [field]: value } as UpdateMenuItemInput)
385
- },
386
-
387
- async reorderItems(categoryId: string, ids: string[]) {
388
- await Promise.all(ids.map(async (id, sortOrder) => {
389
- const existing = await this.getMenuItemById(id)
390
- await client.data.updateRow<ProductRow>({
391
- ...tableOptions,
392
- table: productsTable,
393
- primaryKeys: { id },
394
- row: { metadata: itemMetadata({ categoryId }, { ...existing?.metadata, [MENU_METADATA_KEY]: { ...existing?.metadata?.[MENU_METADATA_KEY] as Record<string, unknown>, sortOrder } }) },
395
- })
396
- }))
397
- },
398
-
399
- async getModifierGroups(): Promise<ModifierGroup[]> {
400
- return []
401
- },
402
-
403
- async createModifierGroup(): Promise<ModifierGroup> {
404
- throw new Error('Menu modifier groups require a dedicated Fayz table/broker contract.')
405
- },
406
-
407
- async updateModifierGroup(): Promise<ModifierGroup> {
408
- throw new Error('Menu modifier groups require a dedicated Fayz table/broker contract.')
409
- },
410
-
411
- async deleteModifierGroup(): Promise<void> {
412
- throw new Error('Menu modifier groups require a dedicated Fayz table/broker contract.')
413
- },
414
-
415
- async getSummary(): Promise<MenuSummary> {
416
- const [items, categories] = await Promise.all([getItems(), this.getCategories()])
417
- const prices = items.filter((item) => item.price > 0).map((item) => item.price)
418
- return {
419
- totalItems: items.length,
420
- availableItems: items.filter((item) => item.status === 'available').length,
421
- soldOutItems: items.filter((item) => item.status === 'sold_out').length,
422
- hiddenItems: items.filter((item) => item.status === 'hidden').length,
423
- categoryCount: categories.length,
424
- averagePrice: prices.length ? prices.reduce((sum, price) => sum + price, 0) / prices.length : 0,
425
- featuredCount: items.filter((item) => item.isFeatured).length,
426
- }
427
- },
428
- }
429
- }
package/src/data/types.ts DELETED
@@ -1,33 +0,0 @@
1
- import type {
2
- MenuCategory, MenuItem, ModifierGroup,
3
- CreateMenuItemInput, UpdateMenuItemInput, CreateMenuCategoryInput,
4
- MenuItemQuery, PaginatedResult, MenuSummary,
5
- } from '../types'
6
-
7
- export interface MenuDataProvider {
8
- // Categories
9
- getCategories(): Promise<MenuCategory[]>
10
- getCategoryById(id: string): Promise<MenuCategory | null>
11
- createCategory(input: CreateMenuCategoryInput): Promise<MenuCategory>
12
- updateCategory(id: string, data: Partial<MenuCategory>): Promise<MenuCategory>
13
- deleteCategory(id: string): Promise<void>
14
- reorderCategories(ids: string[]): Promise<void>
15
-
16
- // Menu Items
17
- getMenuItems(query: MenuItemQuery): Promise<PaginatedResult<MenuItem>>
18
- getMenuItemById(id: string): Promise<MenuItem | null>
19
- createMenuItem(input: CreateMenuItemInput): Promise<MenuItem>
20
- updateMenuItem(id: string, data: UpdateMenuItemInput): Promise<MenuItem>
21
- deleteMenuItem(id: string): Promise<void>
22
- toggleAvailability(id: string, field: 'status' | 'availableForPos' | 'availableForDelivery', value: any): Promise<MenuItem>
23
- reorderItems(categoryId: string, ids: string[]): Promise<void>
24
-
25
- // Modifier Groups
26
- getModifierGroups(): Promise<ModifierGroup[]>
27
- createModifierGroup(input: Partial<ModifierGroup>): Promise<ModifierGroup>
28
- updateModifierGroup(id: string, data: Partial<ModifierGroup>): Promise<ModifierGroup>
29
- deleteModifierGroup(id: string): Promise<void>
30
-
31
- // Summary
32
- getSummary(): Promise<MenuSummary>
33
- }
package/src/index.ts DELETED
@@ -1,191 +0,0 @@
1
- import React from 'react'
2
- import type { PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core'
3
- import { registerTranslations } from '@fayz-ai/core'
4
- import type { EntityLookup } from '@fayz-ai/saas'
5
- import type { MenuDataProvider } from './data/types'
6
- import { createMockMenuProvider } from './data/mock'
7
- import { createMenuStore } from './store'
8
- import { menuRegistries } from './registries'
9
- import { menuLocales } from './locales'
10
- import { PluginSettingsPanel } from '@fayz-ai/saas'
11
-
12
- // Lazy import page to avoid circular dependency issues
13
- const MenuPage = React.lazy(() => import('./MenuPage').then((m) => ({ default: m.MenuPage })))
14
-
15
- // ---------------------------------------------------------------------------
16
- // Labels
17
- // ---------------------------------------------------------------------------
18
-
19
- export interface MenuPluginLabels {
20
- pageTitle: string
21
- menuManager: string
22
- categories: string
23
- newItem: string
24
- }
25
-
26
- const DEFAULT_LABELS: MenuPluginLabels = {
27
- pageTitle: 'Menu',
28
- menuManager: 'Menu Manager',
29
- categories: 'Categories',
30
- newItem: 'New Item',
31
- }
32
-
33
- // ---------------------------------------------------------------------------
34
- // Currency
35
- // ---------------------------------------------------------------------------
36
-
37
- const DEFAULT_CURRENCY = { code: 'BRL', locale: 'pt-BR', symbol: 'R$' }
38
-
39
- // ---------------------------------------------------------------------------
40
- // Options
41
- // ---------------------------------------------------------------------------
42
-
43
- export interface MenuPluginOptions {
44
- modules?: {
45
- modifiers?: boolean
46
- deliveryPricing?: boolean
47
- }
48
- labels?: Partial<MenuPluginLabels>
49
- currency?: { code?: string; locale?: string; symbol?: string }
50
- navPosition?: number
51
- navSection?: 'main' | 'secondary' | 'settings'
52
- scope?: PluginScope
53
- verticalId?: VerticalId
54
- dataProvider?: MenuDataProvider
55
- menuItemLookup?: EntityLookup
56
- }
57
-
58
- // ---------------------------------------------------------------------------
59
- // Config resolver
60
- // ---------------------------------------------------------------------------
61
-
62
- function resolveConfig(options?: MenuPluginOptions) {
63
- return {
64
- modules: {
65
- modifiers: options?.modules?.modifiers !== false,
66
- deliveryPricing: options?.modules?.deliveryPricing === true,
67
- },
68
- labels: { ...DEFAULT_LABELS, ...options?.labels },
69
- currency: { ...DEFAULT_CURRENCY, ...options?.currency },
70
- }
71
- }
72
-
73
- // ---------------------------------------------------------------------------
74
- // Factory
75
- // ---------------------------------------------------------------------------
76
-
77
- export function createMenuPlugin(options?: MenuPluginOptions): PluginManifest {
78
- const config = resolveConfig(options)
79
- registerTranslations(menuLocales)
80
- const provider = options?.dataProvider ?? createMockMenuProvider()
81
- const store = createMenuStore(provider)
82
-
83
- const PageComponent: React.FC<any> = () =>
84
- React.createElement(React.Suspense, { fallback: null },
85
- React.createElement(MenuPage, { config, provider, store, registries: menuRegistries })
86
- )
87
-
88
- return {
89
- id: 'menu',
90
- name: config.labels.pageTitle,
91
- icon: 'UtensilsCrossed',
92
- version: '1.0.0',
93
- scope: options?.scope ?? 'vertical',
94
- verticalId: options?.verticalId,
95
- // Digital menu serves both internal ops (saas) and online ordering
96
- // (ecommerce) — genuinely dual-surface, low confidence, revisit if a
97
- // real host disagrees.
98
- scaffolds: ['saas', 'ecommerce'],
99
- defaultEnabled: true,
100
- dependencies: [],
101
- declaredLimits: [
102
- { key: 'menu_items', label: 'Menu items', table: 'menu_items' },
103
- ],
104
- navigation: [
105
- {
106
- section: options?.navSection ?? 'main',
107
- position: options?.navPosition ?? 5,
108
- label: config.labels.pageTitle,
109
- route: '/menu',
110
- icon: 'UtensilsCrossed',
111
- permission: { feature: 'menu', action: 'read' as const },
112
- },
113
- ],
114
- routes: [
115
- {
116
- path: '/menu',
117
- component: PageComponent,
118
- permission: { feature: 'menu', action: 'read' as const },
119
- },
120
- ],
121
- widgets: [],
122
- aiTools: [
123
- {
124
- id: 'menu.list-items',
125
- name: 'listMenuItems',
126
- description: 'Lists menu items, optionally filtered by category or status.',
127
- icon: 'UtensilsCrossed',
128
- mode: 'read' as const,
129
- category: 'Menu',
130
- parameters: {
131
- type: 'object' as const,
132
- properties: {
133
- category: { type: 'string' as const, description: 'Category name to filter by' },
134
- status: { type: 'string' as const, enum: ['available', 'sold_out', 'hidden'] },
135
- },
136
- },
137
- suggestions: [
138
- { label: 'What items are on the menu?' },
139
- { label: 'Which items are sold out right now?' },
140
- ],
141
- },
142
- {
143
- id: 'menu.toggle-availability',
144
- name: 'toggleMenuItemAvailability',
145
- description: 'Marks a menu item as available or sold out.',
146
- icon: 'ToggleLeft',
147
- mode: 'persist' as const,
148
- category: 'Menu',
149
- parameters: {
150
- type: 'object' as const,
151
- properties: {
152
- itemName: { type: 'string' as const, description: 'Menu item name' },
153
- status: { type: 'string' as const, enum: ['available', 'sold_out'] },
154
- },
155
- required: ['itemName', 'status'],
156
- },
157
- permission: { feature: 'menu', action: 'edit' as const },
158
- },
159
- ],
160
- registries: menuRegistries,
161
- settings: [
162
- {
163
- id: 'menu',
164
- label: config.labels.pageTitle,
165
- icon: 'UtensilsCrossed',
166
- component: (() => {
167
- const MenuSettingsTab: React.ComponentType<unknown> = () =>
168
- React.createElement(PluginSettingsPanel, {
169
- title: 'Menu Settings',
170
- subtitle: 'Categories, allergens, and modifiers',
171
- registries: menuRegistries,
172
- routeBase: '/settings/menu',
173
- })
174
- MenuSettingsTab.displayName = 'MenuSettingsTab'
175
- return MenuSettingsTab
176
- })(),
177
- order: 20,
178
- permission: { feature: 'menu', action: 'read' as const },
179
- },
180
- ],
181
- locales: menuLocales,
182
- }
183
- }
184
-
185
- export type { MenuDataProvider } from './data/types'
186
- export { createFayzMenuProvider } from './data/platform'
187
- export type { FayzMenuProviderOptions } from './data/platform'
188
- export type { ResolvedMenuConfig } from './MenuContext'
189
- // Domain types — public so any host can implement a MenuDataProvider (e.g. an
190
- // app-local Supabase provider during plugin incubation).
191
- export type * from './types'