@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.
- package/README.md +3 -3
- package/dist/{MenuPage-WOI5IELV.js → MenuPage-ZNKKQJKD.js} +4 -4
- package/dist/{MenuPage-WOI5IELV.js.map → MenuPage-ZNKKQJKD.js.map} +1 -1
- package/dist/{MenuContext.d.ts → context.d.ts} +2 -2
- package/dist/context.d.ts.map +1 -0
- package/dist/data/registries.d.ts.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/{MenuPage.d.ts → views/MenuPage.d.ts} +3 -3
- package/dist/views/MenuPage.d.ts.map +1 -0
- package/package.json +6 -8
- package/dist/MenuContext.d.ts.map +0 -1
- package/dist/MenuPage.d.ts.map +0 -1
- package/dist/registries.d.ts.map +0 -1
- package/src/MenuContext.tsx +0 -40
- package/src/MenuPage.tsx +0 -21
- package/src/data/mock.ts +0 -289
- package/src/data/platform.ts +0 -429
- package/src/data/types.ts +0 -33
- package/src/index.ts +0 -191
- package/src/locales/en.ts +0 -64
- package/src/locales/index.ts +0 -4
- package/src/locales/pt-BR.ts +0 -64
- package/src/registries.ts +0 -84
- package/src/store.ts +0 -161
- package/src/types.ts +0 -145
- package/src/views/MenuManagerView.tsx +0 -694
- /package/dist/{registries.d.ts → data/registries.d.ts} +0 -0
package/src/data/platform.ts
DELETED
|
@@ -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'
|