@fayz-ai/plugin-menu 0.2.0

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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +49 -0
  3. package/dist/MenuContext.d.ts +30 -0
  4. package/dist/MenuContext.d.ts.map +1 -0
  5. package/dist/MenuPage-OABV6JYB.js +520 -0
  6. package/dist/MenuPage-OABV6JYB.js.map +1 -0
  7. package/dist/MenuPage-VMI2YAPY.cjs +522 -0
  8. package/dist/MenuPage-VMI2YAPY.cjs.map +1 -0
  9. package/dist/MenuPage.d.ts +13 -0
  10. package/dist/MenuPage.d.ts.map +1 -0
  11. package/dist/data/fayz.d.ts +12 -0
  12. package/dist/data/fayz.d.ts.map +1 -0
  13. package/dist/data/mock.d.ts +3 -0
  14. package/dist/data/mock.d.ts.map +1 -0
  15. package/dist/data/types.d.ts +22 -0
  16. package/dist/data/types.d.ts.map +1 -0
  17. package/dist/index.cjs +1053 -0
  18. package/dist/index.cjs.map +1 -0
  19. package/dist/index.d.ts +34 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +1046 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/locales/en.d.ts +2 -0
  24. package/dist/locales/en.d.ts.map +1 -0
  25. package/dist/locales/index.d.ts +2 -0
  26. package/dist/locales/index.d.ts.map +1 -0
  27. package/dist/locales/pt-BR.d.ts +2 -0
  28. package/dist/locales/pt-BR.d.ts.map +1 -0
  29. package/dist/registries.d.ts +3 -0
  30. package/dist/registries.d.ts.map +1 -0
  31. package/dist/store.d.ts +26 -0
  32. package/dist/store.d.ts.map +1 -0
  33. package/dist/types.d.ts +112 -0
  34. package/dist/types.d.ts.map +1 -0
  35. package/dist/views/MenuManagerView.d.ts +3 -0
  36. package/dist/views/MenuManagerView.d.ts.map +1 -0
  37. package/package.json +55 -0
  38. package/src/MenuContext.tsx +40 -0
  39. package/src/MenuPage.tsx +21 -0
  40. package/src/data/fayz.ts +429 -0
  41. package/src/data/mock.ts +289 -0
  42. package/src/data/types.ts +33 -0
  43. package/src/index.ts +184 -0
  44. package/src/locales/en.ts +64 -0
  45. package/src/locales/index.ts +4 -0
  46. package/src/locales/pt-BR.ts +64 -0
  47. package/src/registries.ts +84 -0
  48. package/src/store.ts +161 -0
  49. package/src/types.ts +145 -0
  50. package/src/views/MenuManagerView.tsx +688 -0
@@ -0,0 +1,289 @@
1
+ import type { MenuDataProvider } from './types'
2
+ import type {
3
+ MenuCategory, MenuItem, ModifierGroup,
4
+ CreateMenuItemInput, UpdateMenuItemInput, CreateMenuCategoryInput,
5
+ MenuItemQuery, PaginatedResult, MenuSummary,
6
+ } from '../types'
7
+
8
+ let uid = 0
9
+ function nextId(prefix: string) { return prefix + '-' + (++uid) }
10
+ function now(): string { return new Date().toISOString() }
11
+
12
+ function paginate<T>(items: T[], page?: number, pageSize?: number): PaginatedResult<T> {
13
+ const p = page ?? 1
14
+ const ps = pageSize ?? 50
15
+ const start = (p - 1) * ps
16
+ return { data: items.slice(start, start + ps), total: items.length }
17
+ }
18
+
19
+ function seedCategories(): MenuCategory[] {
20
+ const t = 'mock-tenant'
21
+ const ts = now()
22
+ return [
23
+ { id: nextId('cat'), name: 'Burgers', icon: '🍔', color: '#f59e0b', sortOrder: 0, isActive: true, itemCount: 3, tenantId: t, createdAt: ts, updatedAt: ts },
24
+ { id: nextId('cat'), name: 'Sides', icon: '🍟', color: '#22c55e', sortOrder: 1, isActive: true, itemCount: 3, tenantId: t, createdAt: ts, updatedAt: ts },
25
+ { id: nextId('cat'), name: 'Drinks', icon: '🥤', color: '#3b82f6', sortOrder: 2, isActive: true, itemCount: 4, tenantId: t, createdAt: ts, updatedAt: ts },
26
+ { id: nextId('cat'), name: 'Desserts', icon: '🍰', color: '#ec4899', sortOrder: 3, isActive: true, itemCount: 2, tenantId: t, createdAt: ts, updatedAt: ts },
27
+ { id: nextId('cat'), name: 'Combos', icon: '⭐', color: '#8b5cf6', sortOrder: 4, isActive: true, itemCount: 0, tenantId: t, createdAt: ts, updatedAt: ts },
28
+ ]
29
+ }
30
+
31
+ function seedItems(categories: MenuCategory[]): MenuItem[] {
32
+ const t = 'mock-tenant'
33
+ const ts = now()
34
+ const [burgers, sides, drinks, desserts] = categories.map((c) => c.id)
35
+
36
+ const item = (
37
+ name: string, price: number, categoryId: string, categoryName: string,
38
+ opts: Partial<MenuItem> = {},
39
+ ): MenuItem => ({
40
+ id: nextId('mi'),
41
+ name,
42
+ price,
43
+ currency: 'BRL',
44
+ categoryId,
45
+ categoryName,
46
+ status: 'available',
47
+ isActive: true,
48
+ availableForPos: true,
49
+ availableForDelivery: true,
50
+ sortOrder: 0,
51
+ isFeatured: false,
52
+ tenantId: t,
53
+ createdAt: ts,
54
+ updatedAt: ts,
55
+ ...opts,
56
+ })
57
+
58
+ let order = 0
59
+ return [
60
+ item('Classic Burger', 32, burgers, 'Burgers', { prepTimeMinutes: 12, sortOrder: order++, isFeatured: true }),
61
+ item('Cheese Burger', 36, burgers, 'Burgers', { prepTimeMinutes: 12, sortOrder: order++ }),
62
+ item('BBQ Bacon', 42, burgers, 'Burgers', { prepTimeMinutes: 15, sortOrder: order++, isFeatured: true }),
63
+ item('Fries', 15, sides, 'Sides', { prepTimeMinutes: 8, sortOrder: order++ }),
64
+ item('Onion Rings', 18, sides, 'Sides', { prepTimeMinutes: 8, sortOrder: order++ }),
65
+ item('Coleslaw', 12, sides, 'Sides', { prepTimeMinutes: 2, sortOrder: order++, availableForDelivery: false }),
66
+ item('Soda', 8, drinks, 'Drinks', { prepTimeMinutes: 1, sortOrder: order++ }),
67
+ item('Juice', 12, drinks, 'Drinks', { prepTimeMinutes: 3, sortOrder: order++ }),
68
+ item('Water', 5, drinks, 'Drinks', { prepTimeMinutes: 0, sortOrder: order++ }),
69
+ item('Milkshake', 16, drinks, 'Drinks', { prepTimeMinutes: 5, sortOrder: order++, isFeatured: true }),
70
+ item('Brownie', 14, desserts, 'Desserts', { prepTimeMinutes: 1, sortOrder: order++ }),
71
+ item('Ice Cream', 12, desserts, 'Desserts', { prepTimeMinutes: 2, sortOrder: order++, availableForDelivery: false }),
72
+ ]
73
+ }
74
+
75
+ function seedModifierGroups(): ModifierGroup[] {
76
+ const t = 'mock-tenant'
77
+ const ts = now()
78
+
79
+ const g1Id = nextId('mg')
80
+ const g2Id = nextId('mg')
81
+
82
+ return [
83
+ {
84
+ id: g1Id, name: 'Extra Toppings', minSelections: 0, maxSelections: 3, isRequired: false, sortOrder: 0, tenantId: t, createdAt: ts,
85
+ items: [
86
+ { id: nextId('mod'), groupId: g1Id, name: 'Extra Cheese', priceAdjustment: 5, isActive: true, sortOrder: 0 },
87
+ { id: nextId('mod'), groupId: g1Id, name: 'Bacon', priceAdjustment: 7, isActive: true, sortOrder: 1 },
88
+ { id: nextId('mod'), groupId: g1Id, name: 'Jalapeños', priceAdjustment: 4, isActive: true, sortOrder: 2 },
89
+ ],
90
+ },
91
+ {
92
+ id: g2Id, name: 'Drink Size', minSelections: 1, maxSelections: 1, isRequired: true, sortOrder: 1, tenantId: t, createdAt: ts,
93
+ items: [
94
+ { id: nextId('mod'), groupId: g2Id, name: 'Small', priceAdjustment: 0, isActive: true, sortOrder: 0 },
95
+ { id: nextId('mod'), groupId: g2Id, name: 'Medium', priceAdjustment: 3, isActive: true, sortOrder: 1 },
96
+ { id: nextId('mod'), groupId: g2Id, name: 'Large', priceAdjustment: 5, isActive: true, sortOrder: 2 },
97
+ ],
98
+ },
99
+ ]
100
+ }
101
+
102
+ export function createMockMenuProvider(): MenuDataProvider {
103
+ const tenantId = 'mock-tenant'
104
+ const categories = seedCategories()
105
+ const items = seedItems(categories)
106
+ const modifierGroups = seedModifierGroups()
107
+
108
+ return {
109
+ async getCategories(): Promise<MenuCategory[]> {
110
+ return [...categories].sort((a, b) => a.sortOrder - b.sortOrder)
111
+ },
112
+
113
+ async getCategoryById(id: string): Promise<MenuCategory | null> {
114
+ return categories.find((c) => c.id === id) ?? null
115
+ },
116
+
117
+ async createCategory(input: CreateMenuCategoryInput): Promise<MenuCategory> {
118
+ const cat: MenuCategory = {
119
+ id: nextId('cat'),
120
+ name: input.name,
121
+ parentId: input.parentId,
122
+ icon: input.icon,
123
+ color: input.color,
124
+ sortOrder: input.sortOrder ?? categories.length,
125
+ isActive: true,
126
+ itemCount: 0,
127
+ tenantId,
128
+ createdAt: now(),
129
+ updatedAt: now(),
130
+ }
131
+ categories.push(cat)
132
+ return cat
133
+ },
134
+
135
+ async updateCategory(id: string, data: Partial<MenuCategory>): Promise<MenuCategory> {
136
+ const cat = categories.find((c) => c.id === id)
137
+ if (!cat) throw new Error(`Category ${id} not found`)
138
+ Object.assign(cat, data, { updatedAt: now() })
139
+ return cat
140
+ },
141
+
142
+ async deleteCategory(id: string): Promise<void> {
143
+ const idx = categories.findIndex((c) => c.id === id)
144
+ if (idx >= 0) categories.splice(idx, 1)
145
+ },
146
+
147
+ async reorderCategories(ids: string[]): Promise<void> {
148
+ ids.forEach((id, i) => {
149
+ const cat = categories.find((c) => c.id === id)
150
+ if (cat) cat.sortOrder = i
151
+ })
152
+ },
153
+
154
+ async getMenuItems(query: MenuItemQuery): Promise<PaginatedResult<MenuItem>> {
155
+ let results = [...items]
156
+ if (query.categoryId) results = results.filter((i) => i.categoryId === query.categoryId)
157
+ if (query.status) {
158
+ const statuses = Array.isArray(query.status) ? query.status : [query.status]
159
+ results = results.filter((i) => statuses.includes(i.status))
160
+ }
161
+ if (query.availableForPos !== undefined) results = results.filter((i) => i.availableForPos === query.availableForPos)
162
+ if (query.availableForDelivery !== undefined) results = results.filter((i) => i.availableForDelivery === query.availableForDelivery)
163
+ if (query.isFeatured !== undefined) results = results.filter((i) => i.isFeatured === query.isFeatured)
164
+ if (query.search) {
165
+ const s = query.search.toLowerCase()
166
+ results = results.filter((i) => i.name.toLowerCase().includes(s) || i.description?.toLowerCase().includes(s))
167
+ }
168
+ results.sort((a, b) => a.sortOrder - b.sortOrder)
169
+ return paginate(results, query.page, query.pageSize)
170
+ },
171
+
172
+ async getMenuItemById(id: string): Promise<MenuItem | null> {
173
+ return items.find((i) => i.id === id) ?? null
174
+ },
175
+
176
+ async createMenuItem(input: CreateMenuItemInput): Promise<MenuItem> {
177
+ const cat = input.categoryId ? categories.find((c) => c.id === input.categoryId) : undefined
178
+ const item: MenuItem = {
179
+ id: nextId('mi'),
180
+ name: input.name,
181
+ description: input.description,
182
+ price: input.price,
183
+ cost: input.cost,
184
+ currency: 'BRL',
185
+ imageUrl: input.imageUrl,
186
+ categoryId: input.categoryId,
187
+ categoryName: cat?.name,
188
+ status: input.status ?? 'available',
189
+ isActive: true,
190
+ prepTimeMinutes: input.prepTimeMinutes,
191
+ allergens: input.allergens,
192
+ dietaryTags: input.dietaryTags,
193
+ availableForPos: input.availableForPos ?? true,
194
+ availableForDelivery: input.availableForDelivery ?? true,
195
+ deliveryPrice: input.deliveryPrice,
196
+ sortOrder: items.length,
197
+ isFeatured: input.isFeatured ?? false,
198
+ modifierGroupIds: input.modifierGroupIds,
199
+ tenantId,
200
+ createdAt: now(),
201
+ updatedAt: now(),
202
+ }
203
+ items.push(item)
204
+ if (cat) cat.itemCount = (cat.itemCount ?? 0) + 1
205
+ return item
206
+ },
207
+
208
+ async updateMenuItem(id: string, data: UpdateMenuItemInput): Promise<MenuItem> {
209
+ const item = items.find((i) => i.id === id)
210
+ if (!item) throw new Error(`MenuItem ${id} not found`)
211
+ Object.assign(item, data, { updatedAt: now() })
212
+ return item
213
+ },
214
+
215
+ async deleteMenuItem(id: string): Promise<void> {
216
+ const idx = items.findIndex((i) => i.id === id)
217
+ if (idx >= 0) {
218
+ const [removed] = items.splice(idx, 1)
219
+ if (removed.categoryId) {
220
+ const cat = categories.find((c) => c.id === removed.categoryId)
221
+ if (cat && cat.itemCount) cat.itemCount--
222
+ }
223
+ }
224
+ },
225
+
226
+ async toggleAvailability(id: string, field: 'status' | 'availableForPos' | 'availableForDelivery', value: any): Promise<MenuItem> {
227
+ const item = items.find((i) => i.id === id)
228
+ if (!item) throw new Error(`MenuItem ${id} not found`)
229
+ ;(item as any)[field] = value
230
+ item.updatedAt = now()
231
+ return item
232
+ },
233
+
234
+ async reorderItems(categoryId: string, ids: string[]): Promise<void> {
235
+ ids.forEach((id, i) => {
236
+ const item = items.find((it) => it.id === id && it.categoryId === categoryId)
237
+ if (item) item.sortOrder = i
238
+ })
239
+ },
240
+
241
+ async getModifierGroups(): Promise<ModifierGroup[]> {
242
+ return [...modifierGroups].sort((a, b) => a.sortOrder - b.sortOrder)
243
+ },
244
+
245
+ async createModifierGroup(input: Partial<ModifierGroup>): Promise<ModifierGroup> {
246
+ const group: ModifierGroup = {
247
+ id: nextId('mg'),
248
+ name: input.name ?? '',
249
+ minSelections: input.minSelections ?? 0,
250
+ maxSelections: input.maxSelections ?? 1,
251
+ isRequired: input.isRequired ?? false,
252
+ sortOrder: input.sortOrder ?? modifierGroups.length,
253
+ items: input.items ?? [],
254
+ tenantId,
255
+ createdAt: now(),
256
+ }
257
+ modifierGroups.push(group)
258
+ return group
259
+ },
260
+
261
+ async updateModifierGroup(id: string, data: Partial<ModifierGroup>): Promise<ModifierGroup> {
262
+ const group = modifierGroups.find((g) => g.id === id)
263
+ if (!group) throw new Error(`ModifierGroup ${id} not found`)
264
+ Object.assign(group, data)
265
+ return group
266
+ },
267
+
268
+ async deleteModifierGroup(id: string): Promise<void> {
269
+ const idx = modifierGroups.findIndex((g) => g.id === id)
270
+ if (idx >= 0) modifierGroups.splice(idx, 1)
271
+ },
272
+
273
+ async getSummary(): Promise<MenuSummary> {
274
+ const available = items.filter((i) => i.status === 'available')
275
+ const soldOut = items.filter((i) => i.status === 'sold_out')
276
+ const hidden = items.filter((i) => i.status === 'hidden')
277
+ const prices = items.filter((i) => i.price > 0).map((i) => i.price)
278
+ return {
279
+ totalItems: items.length,
280
+ availableItems: available.length,
281
+ soldOutItems: soldOut.length,
282
+ hiddenItems: hidden.length,
283
+ categoryCount: categories.length,
284
+ averagePrice: prices.length ? prices.reduce((a, b) => a + b, 0) / prices.length : 0,
285
+ featuredCount: items.filter((i) => i.isFeatured).length,
286
+ }
287
+ },
288
+ }
289
+ }
@@ -0,0 +1,33 @@
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 ADDED
@@ -0,0 +1,184 @@
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
+ defaultEnabled: true,
96
+ dependencies: [],
97
+ navigation: [
98
+ {
99
+ section: options?.navSection ?? 'main',
100
+ position: options?.navPosition ?? 5,
101
+ label: config.labels.pageTitle,
102
+ route: '/menu',
103
+ icon: 'UtensilsCrossed',
104
+ permission: { feature: 'menu', action: 'read' as const },
105
+ },
106
+ ],
107
+ routes: [
108
+ {
109
+ path: '/menu',
110
+ component: PageComponent,
111
+ permission: { feature: 'menu', action: 'read' as const },
112
+ },
113
+ ],
114
+ widgets: [],
115
+ aiTools: [
116
+ {
117
+ id: 'menu.list-items',
118
+ name: 'listMenuItems',
119
+ description: 'Lists menu items, optionally filtered by category or status.',
120
+ icon: 'UtensilsCrossed',
121
+ mode: 'read' as const,
122
+ category: 'Menu',
123
+ parameters: {
124
+ type: 'object' as const,
125
+ properties: {
126
+ category: { type: 'string' as const, description: 'Category name to filter by' },
127
+ status: { type: 'string' as const, enum: ['available', 'sold_out', 'hidden'] },
128
+ },
129
+ },
130
+ suggestions: [
131
+ { label: 'What items are on the menu?' },
132
+ { label: 'Which items are sold out right now?' },
133
+ ],
134
+ },
135
+ {
136
+ id: 'menu.toggle-availability',
137
+ name: 'toggleMenuItemAvailability',
138
+ description: 'Marks a menu item as available or sold out.',
139
+ icon: 'ToggleLeft',
140
+ mode: 'persist' as const,
141
+ category: 'Menu',
142
+ parameters: {
143
+ type: 'object' as const,
144
+ properties: {
145
+ itemName: { type: 'string' as const, description: 'Menu item name' },
146
+ status: { type: 'string' as const, enum: ['available', 'sold_out'] },
147
+ },
148
+ required: ['itemName', 'status'],
149
+ },
150
+ permission: { feature: 'menu', action: 'edit' as const },
151
+ },
152
+ ],
153
+ registries: menuRegistries,
154
+ settings: [
155
+ {
156
+ id: 'menu',
157
+ label: config.labels.pageTitle,
158
+ icon: 'UtensilsCrossed',
159
+ component: (() => {
160
+ const MenuSettingsTab: React.ComponentType<unknown> = () =>
161
+ React.createElement(PluginSettingsPanel, {
162
+ title: 'Menu Settings',
163
+ subtitle: 'Categories, allergens, and modifiers',
164
+ registries: menuRegistries,
165
+ routeBase: '/settings/menu',
166
+ })
167
+ MenuSettingsTab.displayName = 'MenuSettingsTab'
168
+ return MenuSettingsTab
169
+ })(),
170
+ order: 20,
171
+ permission: { feature: 'menu', action: 'read' as const },
172
+ },
173
+ ],
174
+ locales: menuLocales,
175
+ }
176
+ }
177
+
178
+ export type { MenuDataProvider } from './data/types'
179
+ export { createFayzMenuProvider } from './data/fayz'
180
+ export type { FayzMenuProviderOptions } from './data/fayz'
181
+ export type { ResolvedMenuConfig } from './MenuContext'
182
+ // Domain types — public so any host can implement a MenuDataProvider (e.g. an
183
+ // app-local Supabase provider during plugin incubation).
184
+ export type * from './types'
@@ -0,0 +1,64 @@
1
+ export const en: Record<string, string> = {
2
+ 'menu.title': 'Menu',
3
+ 'menu.nav.manager': 'Menu',
4
+ 'menu.nav.categories': 'Categories',
5
+ 'menu.nav.items': 'Items',
6
+ 'menu.nav.modifiers': 'Modifiers',
7
+ 'menu.manager.title': 'Menu Manager',
8
+ 'menu.manager.subtitle': 'Manage your menu items and categories',
9
+ 'menu.manager.search': 'Search menu...',
10
+ 'menu.manager.allCategories': 'All Categories',
11
+ 'menu.manager.newItem': 'New Item',
12
+ 'menu.manager.noItems': 'No menu items yet',
13
+ 'menu.manager.noItemsDesc': 'Add your first menu item to get started',
14
+ 'menu.manager.addFirst': 'Add first item',
15
+ 'menu.manager.available': 'Available',
16
+ 'menu.manager.soldOut': 'Sold Out',
17
+ 'menu.manager.hidden': 'Hidden',
18
+ 'menu.manager.pos': 'POS',
19
+ 'menu.manager.delivery': 'Delivery',
20
+ 'menu.manager.featured': 'Featured',
21
+ 'menu.manager.prepTime': 'Prep time',
22
+ 'menu.manager.min': 'min',
23
+ 'menu.manager.addCategory': 'Add Category',
24
+ 'menu.manager.addProduct': 'Add Product',
25
+ 'menu.manager.item': 'Item',
26
+ 'menu.manager.price': 'Price',
27
+ 'menu.manager.deliveryPrice': 'Delivery $',
28
+ 'menu.manager.itemsCount': '{{count}} items',
29
+ 'menu.manager.noResults': 'No categories or items match your search.',
30
+ 'menu.manager.noResultsDesc': 'Try a different search term or category filter.',
31
+ 'menu.itemForm.newTitle': 'New Menu Item',
32
+ 'menu.itemForm.editTitle': 'Edit Menu Item',
33
+ 'menu.itemForm.name': 'Item name',
34
+ 'menu.itemForm.description': 'Description',
35
+ 'menu.itemForm.price': 'Price',
36
+ 'menu.itemForm.cost': 'Cost',
37
+ 'menu.itemForm.category': 'Category',
38
+ 'menu.itemForm.prepTime': 'Prep time (min)',
39
+ 'menu.itemForm.image': 'Image URL',
40
+ 'menu.itemForm.allergens': 'Allergens',
41
+ 'menu.itemForm.dietaryTags': 'Dietary tags',
42
+ 'menu.itemForm.availableForPos': 'Available for POS',
43
+ 'menu.itemForm.availableForDelivery': 'Available for Delivery',
44
+ 'menu.itemForm.deliveryPrice': 'Delivery price',
45
+ 'menu.itemForm.isFeatured': 'Featured item',
46
+ 'menu.itemForm.save': 'Save',
47
+ 'menu.itemForm.cancel': 'Cancel',
48
+ 'menu.itemForm.saving': 'Saving...',
49
+ 'menu.categories.title': 'Categories',
50
+ 'menu.categories.newCategory': 'New Category',
51
+ 'menu.categories.name': 'Category name',
52
+ 'menu.settings.title': 'Menu Settings',
53
+ 'menu.settings.general': 'General',
54
+ 'menu.settings.taxRate': 'Default tax rate',
55
+ 'menu.settings.currency': 'Currency',
56
+ 'menu.form.categoryName': 'Category name',
57
+ 'menu.form.itemName': 'Item name',
58
+ 'menu.form.description': 'Description',
59
+ 'menu.form.category': 'Category',
60
+ 'menu.form.uncategorized': 'Uncategorized',
61
+ 'menu.form.optional': 'optional',
62
+ 'menu.form.save': 'Save',
63
+ 'menu.form.cancel': 'Cancel',
64
+ }
@@ -0,0 +1,4 @@
1
+ import { en } from './en'
2
+ import { ptBR } from './pt-BR'
3
+
4
+ export const menuLocales: Record<string, Record<string, string>> = { en, 'pt-BR': ptBR }
@@ -0,0 +1,64 @@
1
+ export const ptBR: Record<string, string> = {
2
+ 'menu.title': 'Cardápio',
3
+ 'menu.nav.manager': 'Cardápio',
4
+ 'menu.nav.categories': 'Categorias',
5
+ 'menu.nav.items': 'Itens',
6
+ 'menu.nav.modifiers': 'Modificadores',
7
+ 'menu.manager.title': 'Gerenciar Cardápio',
8
+ 'menu.manager.subtitle': 'Gerencie itens e categorias do cardápio',
9
+ 'menu.manager.search': 'Buscar no cardápio...',
10
+ 'menu.manager.allCategories': 'Todas as Categorias',
11
+ 'menu.manager.newItem': 'Novo Item',
12
+ 'menu.manager.noItems': 'Nenhum item no cardápio',
13
+ 'menu.manager.noItemsDesc': 'Adicione o primeiro item para começar',
14
+ 'menu.manager.addFirst': 'Adicionar primeiro item',
15
+ 'menu.manager.available': 'Disponível',
16
+ 'menu.manager.soldOut': 'Esgotado',
17
+ 'menu.manager.hidden': 'Oculto',
18
+ 'menu.manager.pos': 'PDV',
19
+ 'menu.manager.delivery': 'Delivery',
20
+ 'menu.manager.featured': 'Destaque',
21
+ 'menu.manager.prepTime': 'Tempo de preparo',
22
+ 'menu.manager.min': 'min',
23
+ 'menu.manager.addCategory': 'Adicionar Categoria',
24
+ 'menu.manager.addProduct': 'Adicionar Produto',
25
+ 'menu.manager.item': 'Item',
26
+ 'menu.manager.price': 'Preço',
27
+ 'menu.manager.deliveryPrice': 'Delivery R$',
28
+ 'menu.manager.itemsCount': '{{count}} itens',
29
+ 'menu.manager.noResults': 'Nenhuma categoria ou item encontrado.',
30
+ 'menu.manager.noResultsDesc': 'Tente um termo diferente ou filtro de categoria.',
31
+ 'menu.itemForm.newTitle': 'Novo Item do Cardápio',
32
+ 'menu.itemForm.editTitle': 'Editar Item do Cardápio',
33
+ 'menu.itemForm.name': 'Nome do item',
34
+ 'menu.itemForm.description': 'Descrição',
35
+ 'menu.itemForm.price': 'Preço',
36
+ 'menu.itemForm.cost': 'Custo',
37
+ 'menu.itemForm.category': 'Categoria',
38
+ 'menu.itemForm.prepTime': 'Tempo de preparo (min)',
39
+ 'menu.itemForm.image': 'URL da imagem',
40
+ 'menu.itemForm.allergens': 'Alérgenos',
41
+ 'menu.itemForm.dietaryTags': 'Tags dietéticas',
42
+ 'menu.itemForm.availableForPos': 'Disponível no PDV',
43
+ 'menu.itemForm.availableForDelivery': 'Disponível para Delivery',
44
+ 'menu.itemForm.deliveryPrice': 'Preço delivery',
45
+ 'menu.itemForm.isFeatured': 'Item em destaque',
46
+ 'menu.itemForm.save': 'Salvar',
47
+ 'menu.itemForm.cancel': 'Cancelar',
48
+ 'menu.itemForm.saving': 'Salvando...',
49
+ 'menu.categories.title': 'Categorias',
50
+ 'menu.categories.newCategory': 'Nova Categoria',
51
+ 'menu.categories.name': 'Nome da categoria',
52
+ 'menu.settings.title': 'Configurações do Cardápio',
53
+ 'menu.settings.general': 'Geral',
54
+ 'menu.settings.taxRate': 'Taxa padrão',
55
+ 'menu.settings.currency': 'Moeda',
56
+ 'menu.form.categoryName': 'Nome da categoria',
57
+ 'menu.form.itemName': 'Nome do item',
58
+ 'menu.form.description': 'Descrição',
59
+ 'menu.form.category': 'Categoria',
60
+ 'menu.form.uncategorized': 'Sem categoria',
61
+ 'menu.form.optional': 'opcional',
62
+ 'menu.form.save': 'Salvar',
63
+ 'menu.form.cancel': 'Cancelar',
64
+ }