@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.
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/MenuContext.d.ts +30 -0
- package/dist/MenuContext.d.ts.map +1 -0
- package/dist/MenuPage-OABV6JYB.js +520 -0
- package/dist/MenuPage-OABV6JYB.js.map +1 -0
- package/dist/MenuPage-VMI2YAPY.cjs +522 -0
- package/dist/MenuPage-VMI2YAPY.cjs.map +1 -0
- package/dist/MenuPage.d.ts +13 -0
- package/dist/MenuPage.d.ts.map +1 -0
- package/dist/data/fayz.d.ts +12 -0
- package/dist/data/fayz.d.ts.map +1 -0
- package/dist/data/mock.d.ts +3 -0
- package/dist/data/mock.d.ts.map +1 -0
- package/dist/data/types.d.ts +22 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/index.cjs +1053 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1046 -0
- package/dist/index.js.map +1 -0
- package/dist/locales/en.d.ts +2 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/index.d.ts +2 -0
- package/dist/locales/index.d.ts.map +1 -0
- package/dist/locales/pt-BR.d.ts +2 -0
- package/dist/locales/pt-BR.d.ts.map +1 -0
- package/dist/registries.d.ts +3 -0
- package/dist/registries.d.ts.map +1 -0
- package/dist/store.d.ts +26 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/types.d.ts +112 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/views/MenuManagerView.d.ts +3 -0
- package/dist/views/MenuManagerView.d.ts.map +1 -0
- package/package.json +55 -0
- package/src/MenuContext.tsx +40 -0
- package/src/MenuPage.tsx +21 -0
- package/src/data/fayz.ts +429 -0
- package/src/data/mock.ts +289 -0
- package/src/data/types.ts +33 -0
- package/src/index.ts +184 -0
- package/src/locales/en.ts +64 -0
- package/src/locales/index.ts +4 -0
- package/src/locales/pt-BR.ts +64 -0
- package/src/registries.ts +84 -0
- package/src/store.ts +161 -0
- package/src/types.ts +145 -0
- package/src/views/MenuManagerView.tsx +688 -0
package/src/data/mock.ts
ADDED
|
@@ -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,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
|
+
}
|