@fayz-ai/plugin-menu 0.9.4 → 0.10.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/dist/MenuPage-2LLWRJGN.js +1785 -0
- package/dist/MenuPage-2LLWRJGN.js.map +1 -0
- package/dist/chunk-C6W53TML.js +65 -0
- package/dist/chunk-C6W53TML.js.map +1 -0
- package/dist/components/DraftSectionCard.d.ts +18 -0
- package/dist/components/DraftSectionCard.d.ts.map +1 -0
- package/dist/components/ItemRailPage.d.ts +36 -0
- package/dist/components/ItemRailPage.d.ts.map +1 -0
- package/dist/components/MenuPreview.d.ts +17 -0
- package/dist/components/MenuPreview.d.ts.map +1 -0
- package/dist/components/MenuRailPage.d.ts +17 -0
- package/dist/components/MenuRailPage.d.ts.map +1 -0
- package/dist/components/ScheduleEditor.d.ts +8 -0
- package/dist/components/ScheduleEditor.d.ts.map +1 -0
- package/dist/components/SectionCard.d.ts +32 -0
- package/dist/components/SectionCard.d.ts.map +1 -0
- package/dist/data/legacy-adapter.d.ts +4 -0
- package/dist/data/legacy-adapter.d.ts.map +1 -0
- package/dist/data/mock.d.ts +2 -2
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/registries.d.ts.map +1 -1
- package/dist/data/supabase.d.ts +7 -0
- package/dist/data/supabase.d.ts.map +1 -0
- package/dist/data/tables.d.ts +17 -0
- package/dist/data/tables.d.ts.map +1 -0
- package/dist/data/types.d.ts +36 -2
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2150 -356
- package/dist/index.js.map +1 -1
- package/dist/lib/agent-surface.d.ts +46 -0
- package/dist/lib/agent-surface.d.ts.map +1 -0
- package/dist/lib/availability.d.ts +16 -0
- package/dist/lib/availability.d.ts.map +1 -0
- package/dist/lib/extract-parse.d.ts +16 -0
- package/dist/lib/extract-parse.d.ts.map +1 -0
- package/dist/lib/extract-transport.d.ts +10 -0
- package/dist/lib/extract-transport.d.ts.map +1 -0
- package/dist/lib/images.d.ts +7 -0
- package/dist/lib/images.d.ts.map +1 -0
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +8 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/store.d.ts +46 -10
- package/dist/store.d.ts.map +1 -1
- package/dist/types.d.ts +151 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/views/MenuManagerView.d.ts.map +1 -1
- package/package.json +12 -6
- package/dist/MenuPage-ZNKKQJKD.js +0 -523
- package/dist/MenuPage-ZNKKQJKD.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
+
import { countExtracted, normalizeName } from './chunk-C6W53TML.js';
|
|
1
2
|
import React from 'react';
|
|
2
|
-
import { registerTranslations } from '@fayz-ai/core';
|
|
3
|
+
import { registerTranslations, createSafeDataProvider, tl, getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core';
|
|
3
4
|
import { createStore } from 'zustand/vanilla';
|
|
4
|
-
import { PluginSettingsPanel, dedup } from '@fayz-ai/admin';
|
|
5
5
|
import { toast } from 'sonner';
|
|
6
|
+
import { PluginSettingsPanel, dedup } from '@fayz-ai/admin';
|
|
6
7
|
import { createFayzClient } from '@fayz-ai/sdk';
|
|
7
8
|
|
|
8
|
-
// src/index.ts
|
|
9
|
-
|
|
10
9
|
// src/data/mock.ts
|
|
11
10
|
var uid = 0;
|
|
12
11
|
function nextId(prefix) {
|
|
@@ -17,103 +16,318 @@ function now() {
|
|
|
17
16
|
}
|
|
18
17
|
function paginate(items, page, pageSize) {
|
|
19
18
|
const p = page ?? 1;
|
|
20
|
-
const ps = pageSize ??
|
|
19
|
+
const ps = pageSize ?? 200;
|
|
21
20
|
const start = (p - 1) * ps;
|
|
22
21
|
return { data: items.slice(start, start + ps), total: items.length };
|
|
23
22
|
}
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
var TENANT = "mock-tenant";
|
|
24
|
+
function createMockMenuProvider() {
|
|
26
25
|
const ts = now();
|
|
27
|
-
|
|
28
|
-
{
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
const menus = [
|
|
27
|
+
{
|
|
28
|
+
id: nextId("menu"),
|
|
29
|
+
name: "Card\xE1pio principal",
|
|
30
|
+
description: "O de todo dia",
|
|
31
|
+
isActive: true,
|
|
32
|
+
sortOrder: 0,
|
|
33
|
+
channels: ["pos", "delivery", "digital"],
|
|
34
|
+
availability: {},
|
|
35
|
+
tenantId: TENANT,
|
|
36
|
+
createdAt: ts,
|
|
37
|
+
updatedAt: ts
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: nextId("menu"),
|
|
41
|
+
name: "Executivo",
|
|
42
|
+
description: "Almo\xE7o de segunda a sexta",
|
|
43
|
+
isActive: true,
|
|
44
|
+
sortOrder: 1,
|
|
45
|
+
channels: ["pos"],
|
|
46
|
+
availability: { days: [1, 2, 3, 4, 5], start: "11:30", end: "15:00" },
|
|
47
|
+
tenantId: TENANT,
|
|
48
|
+
createdAt: ts,
|
|
49
|
+
updatedAt: ts
|
|
50
|
+
}
|
|
33
51
|
];
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
52
|
+
const section = (menuId, name, order) => ({
|
|
53
|
+
id: nextId("sec"),
|
|
54
|
+
menuId,
|
|
55
|
+
name,
|
|
56
|
+
sortOrder: order,
|
|
57
|
+
isActive: true,
|
|
58
|
+
availability: {},
|
|
59
|
+
tenantId: TENANT,
|
|
60
|
+
createdAt: ts,
|
|
61
|
+
updatedAt: ts
|
|
62
|
+
});
|
|
63
|
+
const sections = [
|
|
64
|
+
section(menus[0].id, "Entradas", 0),
|
|
65
|
+
section(menus[0].id, "Pratos principais", 1),
|
|
66
|
+
section(menus[0].id, "Bebidas", 2),
|
|
67
|
+
section(menus[0].id, "Sobremesas", 3),
|
|
68
|
+
section(menus[1].id, "Prato do dia", 0),
|
|
69
|
+
section(menus[1].id, "Bebida inclusa", 1)
|
|
70
|
+
];
|
|
71
|
+
const item = (name, price, extra = {}) => ({
|
|
40
72
|
id: nextId("mi"),
|
|
41
73
|
name,
|
|
42
74
|
price,
|
|
43
75
|
currency: "BRL",
|
|
44
|
-
categoryId,
|
|
45
|
-
categoryName,
|
|
46
76
|
status: "available",
|
|
47
77
|
isActive: true,
|
|
48
78
|
availableForPos: true,
|
|
49
79
|
availableForDelivery: true,
|
|
50
80
|
sortOrder: 0,
|
|
51
81
|
isFeatured: false,
|
|
52
|
-
|
|
82
|
+
allergens: [],
|
|
83
|
+
dietaryTags: [],
|
|
84
|
+
tenantId: TENANT,
|
|
53
85
|
createdAt: ts,
|
|
54
86
|
updatedAt: ts,
|
|
55
|
-
...
|
|
87
|
+
...extra
|
|
56
88
|
});
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
item("
|
|
60
|
-
item("
|
|
61
|
-
item("
|
|
62
|
-
item("
|
|
63
|
-
item("
|
|
64
|
-
item("
|
|
65
|
-
item("
|
|
66
|
-
item("
|
|
67
|
-
item("
|
|
68
|
-
item("
|
|
69
|
-
item("Brownie", 14, desserts, "Desserts", { prepTimeMinutes: 1, sortOrder: order++ }),
|
|
70
|
-
item("Ice Cream", 12, desserts, "Desserts", { prepTimeMinutes: 2, sortOrder: order++, availableForDelivery: false })
|
|
89
|
+
const items = [
|
|
90
|
+
item("Bolinho de bacalhau", 38, { description: "6 unidades", prepTimeMinutes: 15 }),
|
|
91
|
+
item("P\xE3o de alho", 18, { prepTimeMinutes: 8, dietaryTags: ["Vegetariano"] }),
|
|
92
|
+
item("Picanha na chapa", 129, { description: "Serve 2 pessoas", prepTimeMinutes: 25, isFeatured: true }),
|
|
93
|
+
item("Moqueca de peixe", 98, { prepTimeMinutes: 30, allergens: ["Frutos do mar"] }),
|
|
94
|
+
item("Risoto de funghi", 72, { prepTimeMinutes: 22, dietaryTags: ["Vegetariano"] }),
|
|
95
|
+
item("Suco natural", 14, { prepTimeMinutes: 3 }),
|
|
96
|
+
item("Refrigerante", 9, { prepTimeMinutes: 1 }),
|
|
97
|
+
item("\xC1gua", 6, { prepTimeMinutes: 1 }),
|
|
98
|
+
item("Pudim da casa", 19, { prepTimeMinutes: 2, isFeatured: true }),
|
|
99
|
+
item("Petit g\xE2teau", 26, { prepTimeMinutes: 12, allergens: ["Gl\xFAten", "Lactose"] }),
|
|
100
|
+
item("Prato executivo do dia", 42, { description: "Prato + bebida", prepTimeMinutes: 15 })
|
|
71
101
|
];
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
102
|
+
const placement = (sectionId, itemId, order, priceOverride) => ({
|
|
103
|
+
id: nextId("pl"),
|
|
104
|
+
sectionId,
|
|
105
|
+
itemId,
|
|
106
|
+
sortOrder: order,
|
|
107
|
+
priceOverride,
|
|
108
|
+
isActive: true,
|
|
109
|
+
tenantId: TENANT
|
|
110
|
+
});
|
|
111
|
+
const placements = [
|
|
112
|
+
placement(sections[0].id, items[0].id, 0),
|
|
113
|
+
placement(sections[0].id, items[1].id, 1),
|
|
114
|
+
placement(sections[1].id, items[2].id, 0),
|
|
115
|
+
placement(sections[1].id, items[3].id, 1),
|
|
116
|
+
placement(sections[1].id, items[4].id, 2),
|
|
117
|
+
placement(sections[2].id, items[5].id, 0),
|
|
118
|
+
placement(sections[2].id, items[6].id, 1),
|
|
119
|
+
placement(sections[2].id, items[7].id, 2),
|
|
120
|
+
placement(sections[3].id, items[8].id, 0),
|
|
121
|
+
placement(sections[3].id, items[9].id, 1),
|
|
122
|
+
placement(sections[4].id, items[10].id, 0),
|
|
123
|
+
// The same risotto, cheaper at lunch — the reason placements carry a price.
|
|
124
|
+
placement(sections[4].id, items[4].id, 1, 49),
|
|
125
|
+
placement(sections[5].id, items[5].id, 0, 0)
|
|
126
|
+
];
|
|
127
|
+
const categories = [];
|
|
128
|
+
const g1 = nextId("mg");
|
|
129
|
+
const g2 = nextId("mg");
|
|
130
|
+
const modifierGroups = [
|
|
79
131
|
{
|
|
80
|
-
id:
|
|
81
|
-
name: "
|
|
82
|
-
minSelections:
|
|
83
|
-
maxSelections:
|
|
84
|
-
isRequired:
|
|
132
|
+
id: g1,
|
|
133
|
+
name: "Ponto da carne",
|
|
134
|
+
minSelections: 1,
|
|
135
|
+
maxSelections: 1,
|
|
136
|
+
isRequired: true,
|
|
85
137
|
sortOrder: 0,
|
|
86
|
-
tenantId:
|
|
138
|
+
tenantId: TENANT,
|
|
87
139
|
createdAt: ts,
|
|
88
140
|
items: [
|
|
89
|
-
{ id: nextId("mod"), groupId:
|
|
90
|
-
{ id: nextId("mod"), groupId:
|
|
91
|
-
{ id: nextId("mod"), groupId:
|
|
141
|
+
{ id: nextId("mod"), groupId: g1, name: "Mal passada", priceAdjustment: 0, isActive: true, sortOrder: 0 },
|
|
142
|
+
{ id: nextId("mod"), groupId: g1, name: "Ao ponto", priceAdjustment: 0, isActive: true, sortOrder: 1 },
|
|
143
|
+
{ id: nextId("mod"), groupId: g1, name: "Bem passada", priceAdjustment: 0, isActive: true, sortOrder: 2 }
|
|
92
144
|
]
|
|
93
145
|
},
|
|
94
146
|
{
|
|
95
|
-
id:
|
|
96
|
-
name: "
|
|
97
|
-
minSelections:
|
|
98
|
-
maxSelections:
|
|
99
|
-
isRequired:
|
|
147
|
+
id: g2,
|
|
148
|
+
name: "Acompanhamentos",
|
|
149
|
+
minSelections: 0,
|
|
150
|
+
maxSelections: 3,
|
|
151
|
+
isRequired: false,
|
|
100
152
|
sortOrder: 1,
|
|
101
|
-
tenantId:
|
|
153
|
+
tenantId: TENANT,
|
|
102
154
|
createdAt: ts,
|
|
103
155
|
items: [
|
|
104
|
-
{ id: nextId("mod"), groupId:
|
|
105
|
-
{ id: nextId("mod"), groupId:
|
|
106
|
-
{ id: nextId("mod"), groupId:
|
|
156
|
+
{ id: nextId("mod"), groupId: g2, name: "Farofa", priceAdjustment: 8, isActive: true, sortOrder: 0 },
|
|
157
|
+
{ id: nextId("mod"), groupId: g2, name: "Vinagrete", priceAdjustment: 6, isActive: true, sortOrder: 1 },
|
|
158
|
+
{ id: nextId("mod"), groupId: g2, name: "Arroz branco", priceAdjustment: 10, isActive: true, sortOrder: 2 }
|
|
107
159
|
]
|
|
108
160
|
}
|
|
109
161
|
];
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
162
|
+
const provider = {
|
|
163
|
+
// --- Menus ---
|
|
164
|
+
async getMenus() {
|
|
165
|
+
return menus.map((m) => {
|
|
166
|
+
const mine = sections.filter((s) => s.menuId === m.id);
|
|
167
|
+
const ids = new Set(mine.map((s) => s.id));
|
|
168
|
+
return { ...m, sectionCount: mine.length, itemCount: placements.filter((p) => ids.has(p.sectionId)).length };
|
|
169
|
+
}).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
170
|
+
},
|
|
171
|
+
async createMenu(input) {
|
|
172
|
+
const menu = {
|
|
173
|
+
id: nextId("menu"),
|
|
174
|
+
name: input.name,
|
|
175
|
+
description: input.description,
|
|
176
|
+
isActive: input.isActive ?? true,
|
|
177
|
+
sortOrder: input.sortOrder ?? menus.length,
|
|
178
|
+
channels: input.channels ?? ["pos", "delivery", "digital"],
|
|
179
|
+
availability: input.availability ?? {},
|
|
180
|
+
tenantId: TENANT,
|
|
181
|
+
createdAt: now(),
|
|
182
|
+
updatedAt: now()
|
|
183
|
+
};
|
|
184
|
+
menus.push(menu);
|
|
185
|
+
return menu;
|
|
186
|
+
},
|
|
187
|
+
async updateMenu(id, patch) {
|
|
188
|
+
const menu = menus.find((m) => m.id === id);
|
|
189
|
+
if (!menu) throw new Error("Menu not found");
|
|
190
|
+
Object.assign(menu, patch, { updatedAt: now() });
|
|
191
|
+
return menu;
|
|
192
|
+
},
|
|
193
|
+
async deleteMenu(id) {
|
|
194
|
+
const mine = sections.filter((s) => s.menuId === id);
|
|
195
|
+
for (const s of mine) {
|
|
196
|
+
for (let i = placements.length - 1; i >= 0; i--) if (placements[i].sectionId === s.id) placements.splice(i, 1);
|
|
197
|
+
sections.splice(sections.indexOf(s), 1);
|
|
198
|
+
}
|
|
199
|
+
const index = menus.findIndex((m) => m.id === id);
|
|
200
|
+
if (index >= 0) menus.splice(index, 1);
|
|
201
|
+
},
|
|
202
|
+
async duplicateMenu(id, name) {
|
|
203
|
+
const source = menus.find((m) => m.id === id);
|
|
204
|
+
if (!source) throw new Error("Menu not found");
|
|
205
|
+
const copy = await provider.createMenu({
|
|
206
|
+
name,
|
|
207
|
+
description: source.description,
|
|
208
|
+
channels: source.channels,
|
|
209
|
+
availability: source.availability,
|
|
210
|
+
isActive: false
|
|
211
|
+
});
|
|
212
|
+
for (const s of sections.filter((x) => x.menuId === id)) {
|
|
213
|
+
const clone = await provider.createSection({
|
|
214
|
+
menuId: copy.id,
|
|
215
|
+
name: s.name,
|
|
216
|
+
description: s.description,
|
|
217
|
+
sortOrder: s.sortOrder,
|
|
218
|
+
isActive: s.isActive,
|
|
219
|
+
availability: s.availability
|
|
220
|
+
});
|
|
221
|
+
for (const p of placements.filter((x) => x.sectionId === s.id)) {
|
|
222
|
+
await provider.placeItem({
|
|
223
|
+
sectionId: clone.id,
|
|
224
|
+
itemId: p.itemId,
|
|
225
|
+
sortOrder: p.sortOrder,
|
|
226
|
+
priceOverride: p.priceOverride,
|
|
227
|
+
isActive: p.isActive
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return copy;
|
|
232
|
+
},
|
|
233
|
+
async reorderMenus(ids) {
|
|
234
|
+
ids.forEach((id, index) => {
|
|
235
|
+
const menu = menus.find((m) => m.id === id);
|
|
236
|
+
if (menu) menu.sortOrder = index;
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
// --- Sections ---
|
|
240
|
+
async getSections(menuId) {
|
|
241
|
+
return sections.filter((s) => s.menuId === menuId).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
242
|
+
},
|
|
243
|
+
async createSection(input) {
|
|
244
|
+
const created = {
|
|
245
|
+
id: nextId("sec"),
|
|
246
|
+
menuId: input.menuId,
|
|
247
|
+
name: input.name,
|
|
248
|
+
description: input.description,
|
|
249
|
+
imageUrl: input.imageUrl,
|
|
250
|
+
sortOrder: input.sortOrder ?? sections.filter((s) => s.menuId === input.menuId).length,
|
|
251
|
+
isActive: input.isActive ?? true,
|
|
252
|
+
availability: input.availability ?? {},
|
|
253
|
+
tenantId: TENANT,
|
|
254
|
+
createdAt: now(),
|
|
255
|
+
updatedAt: now()
|
|
256
|
+
};
|
|
257
|
+
sections.push(created);
|
|
258
|
+
return created;
|
|
259
|
+
},
|
|
260
|
+
async updateSection(id, patch) {
|
|
261
|
+
const found = sections.find((s) => s.id === id);
|
|
262
|
+
if (!found) throw new Error("Section not found");
|
|
263
|
+
Object.assign(found, patch, { updatedAt: now() });
|
|
264
|
+
return found;
|
|
265
|
+
},
|
|
266
|
+
async deleteSection(id) {
|
|
267
|
+
for (let i = placements.length - 1; i >= 0; i--) if (placements[i].sectionId === id) placements.splice(i, 1);
|
|
268
|
+
const index = sections.findIndex((s) => s.id === id);
|
|
269
|
+
if (index >= 0) sections.splice(index, 1);
|
|
270
|
+
},
|
|
271
|
+
async reorderSections(_menuId, ids) {
|
|
272
|
+
ids.forEach((id, index) => {
|
|
273
|
+
const found = sections.find((s) => s.id === id);
|
|
274
|
+
if (found) found.sortOrder = index;
|
|
275
|
+
});
|
|
276
|
+
},
|
|
277
|
+
// --- Placements ---
|
|
278
|
+
async getPlacements(menuId) {
|
|
279
|
+
const ids = new Set(sections.filter((s) => s.menuId === menuId).map((s) => s.id));
|
|
280
|
+
return placements.filter((p) => ids.has(p.sectionId)).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
281
|
+
},
|
|
282
|
+
async placeItem(input) {
|
|
283
|
+
const existing = placements.find((p) => p.sectionId === input.sectionId && p.itemId === input.itemId);
|
|
284
|
+
if (existing) {
|
|
285
|
+
Object.assign(existing, input);
|
|
286
|
+
return existing;
|
|
287
|
+
}
|
|
288
|
+
const created = placement(
|
|
289
|
+
input.sectionId,
|
|
290
|
+
input.itemId,
|
|
291
|
+
input.sortOrder ?? placements.filter((p) => p.sectionId === input.sectionId).length,
|
|
292
|
+
input.priceOverride
|
|
293
|
+
);
|
|
294
|
+
created.isActive = input.isActive ?? true;
|
|
295
|
+
placements.push(created);
|
|
296
|
+
return created;
|
|
297
|
+
},
|
|
298
|
+
async updatePlacement(id, patch) {
|
|
299
|
+
const found = placements.find((p) => p.id === id);
|
|
300
|
+
if (!found) throw new Error("Placement not found");
|
|
301
|
+
if (patch.sectionId !== void 0) found.sectionId = patch.sectionId;
|
|
302
|
+
if (patch.sortOrder !== void 0) found.sortOrder = patch.sortOrder;
|
|
303
|
+
if (patch.priceOverride !== void 0) found.priceOverride = patch.priceOverride;
|
|
304
|
+
if (patch.isActive !== void 0) found.isActive = patch.isActive;
|
|
305
|
+
return found;
|
|
306
|
+
},
|
|
307
|
+
async removePlacement(id) {
|
|
308
|
+
const index = placements.findIndex((p) => p.id === id);
|
|
309
|
+
if (index >= 0) placements.splice(index, 1);
|
|
310
|
+
},
|
|
311
|
+
async reorderPlacements(_sectionId, ids) {
|
|
312
|
+
ids.forEach((id, index) => {
|
|
313
|
+
const found = placements.find((p) => p.id === id);
|
|
314
|
+
if (found) found.sortOrder = index;
|
|
315
|
+
});
|
|
316
|
+
},
|
|
317
|
+
async movePlacement(id, toSectionId, toIndex) {
|
|
318
|
+
const found = placements.find((p) => p.id === id);
|
|
319
|
+
if (!found) return;
|
|
320
|
+
found.sectionId = toSectionId;
|
|
321
|
+
found.sortOrder = toIndex;
|
|
322
|
+
placements.filter((p) => p.sectionId === toSectionId && p.id !== id).sort((a, b) => a.sortOrder - b.sortOrder).forEach((p, i) => {
|
|
323
|
+
p.sortOrder = i >= toIndex ? i + 1 : i;
|
|
324
|
+
});
|
|
325
|
+
},
|
|
326
|
+
// --- Media ---
|
|
327
|
+
async uploadImage(file) {
|
|
328
|
+
return typeof URL !== "undefined" && URL.createObjectURL ? URL.createObjectURL(file) : "";
|
|
329
|
+
},
|
|
330
|
+
// --- Categories (legacy surface, still answered) ---
|
|
117
331
|
async getCategories() {
|
|
118
332
|
return [...categories].sort((a, b) => a.sortOrder - b.sortOrder);
|
|
119
333
|
},
|
|
@@ -121,7 +335,7 @@ function createMockMenuProvider() {
|
|
|
121
335
|
return categories.find((c) => c.id === id) ?? null;
|
|
122
336
|
},
|
|
123
337
|
async createCategory(input) {
|
|
124
|
-
const
|
|
338
|
+
const created = {
|
|
125
339
|
id: nextId("cat"),
|
|
126
340
|
name: input.name,
|
|
127
341
|
parentId: input.parentId,
|
|
@@ -129,257 +343,1662 @@ function createMockMenuProvider() {
|
|
|
129
343
|
color: input.color,
|
|
130
344
|
sortOrder: input.sortOrder ?? categories.length,
|
|
131
345
|
isActive: true,
|
|
132
|
-
|
|
133
|
-
tenantId,
|
|
346
|
+
tenantId: TENANT,
|
|
134
347
|
createdAt: now(),
|
|
135
348
|
updatedAt: now()
|
|
136
349
|
};
|
|
137
|
-
categories.push(
|
|
138
|
-
return
|
|
350
|
+
categories.push(created);
|
|
351
|
+
return created;
|
|
139
352
|
},
|
|
140
|
-
async updateCategory(id,
|
|
141
|
-
const
|
|
142
|
-
if (!
|
|
143
|
-
Object.assign(
|
|
144
|
-
return
|
|
353
|
+
async updateCategory(id, patch) {
|
|
354
|
+
const found = categories.find((c) => c.id === id);
|
|
355
|
+
if (!found) throw new Error("Category not found");
|
|
356
|
+
Object.assign(found, patch, { updatedAt: now() });
|
|
357
|
+
return found;
|
|
145
358
|
},
|
|
146
359
|
async deleteCategory(id) {
|
|
147
|
-
const
|
|
148
|
-
if (
|
|
360
|
+
const index = categories.findIndex((c) => c.id === id);
|
|
361
|
+
if (index >= 0) categories.splice(index, 1);
|
|
149
362
|
},
|
|
150
363
|
async reorderCategories(ids) {
|
|
151
|
-
ids.forEach((id,
|
|
152
|
-
const
|
|
153
|
-
if (
|
|
364
|
+
ids.forEach((id, index) => {
|
|
365
|
+
const found = categories.find((c) => c.id === id);
|
|
366
|
+
if (found) found.sortOrder = index;
|
|
154
367
|
});
|
|
155
368
|
},
|
|
369
|
+
// --- Items ---
|
|
156
370
|
async getMenuItems(query) {
|
|
157
|
-
let
|
|
158
|
-
if (query.
|
|
371
|
+
let result = [...items];
|
|
372
|
+
if (query.search) {
|
|
373
|
+
const term = query.search.toLowerCase();
|
|
374
|
+
result = result.filter((i) => i.name.toLowerCase().includes(term) || (i.description ?? "").toLowerCase().includes(term));
|
|
375
|
+
}
|
|
376
|
+
if (query.categoryId) result = result.filter((i) => i.categoryId === query.categoryId);
|
|
159
377
|
if (query.status) {
|
|
160
378
|
const statuses = Array.isArray(query.status) ? query.status : [query.status];
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
if (query.availableForPos !== void 0) results = results.filter((i) => i.availableForPos === query.availableForPos);
|
|
164
|
-
if (query.availableForDelivery !== void 0) results = results.filter((i) => i.availableForDelivery === query.availableForDelivery);
|
|
165
|
-
if (query.isFeatured !== void 0) results = results.filter((i) => i.isFeatured === query.isFeatured);
|
|
166
|
-
if (query.search) {
|
|
167
|
-
const s = query.search.toLowerCase();
|
|
168
|
-
results = results.filter((i) => i.name.toLowerCase().includes(s) || i.description?.toLowerCase().includes(s));
|
|
379
|
+
result = result.filter((i) => statuses.includes(i.status));
|
|
169
380
|
}
|
|
170
|
-
|
|
171
|
-
|
|
381
|
+
if (query.availableForPos !== void 0) result = result.filter((i) => i.availableForPos === query.availableForPos);
|
|
382
|
+
if (query.availableForDelivery !== void 0) result = result.filter((i) => i.availableForDelivery === query.availableForDelivery);
|
|
383
|
+
if (query.isFeatured !== void 0) result = result.filter((i) => i.isFeatured === query.isFeatured);
|
|
384
|
+
return paginate(result, query.page, query.pageSize);
|
|
172
385
|
},
|
|
173
386
|
async getMenuItemById(id) {
|
|
174
387
|
return items.find((i) => i.id === id) ?? null;
|
|
175
388
|
},
|
|
176
389
|
async createMenuItem(input) {
|
|
177
|
-
const
|
|
178
|
-
const item = {
|
|
179
|
-
id: nextId("mi"),
|
|
180
|
-
name: input.name,
|
|
390
|
+
const created = item(input.name, input.price, {
|
|
181
391
|
description: input.description,
|
|
182
|
-
price: input.price,
|
|
183
392
|
cost: input.cost,
|
|
184
|
-
currency: "BRL",
|
|
185
393
|
imageUrl: input.imageUrl,
|
|
186
394
|
categoryId: input.categoryId,
|
|
187
|
-
categoryName: cat?.name,
|
|
188
395
|
status: input.status ?? "available",
|
|
189
|
-
isActive: true,
|
|
190
396
|
prepTimeMinutes: input.prepTimeMinutes,
|
|
191
|
-
allergens: input.allergens,
|
|
192
|
-
dietaryTags: input.dietaryTags,
|
|
397
|
+
allergens: input.allergens ?? [],
|
|
398
|
+
dietaryTags: input.dietaryTags ?? [],
|
|
193
399
|
availableForPos: input.availableForPos ?? true,
|
|
194
400
|
availableForDelivery: input.availableForDelivery ?? true,
|
|
195
401
|
deliveryPrice: input.deliveryPrice,
|
|
196
|
-
sortOrder: items.length,
|
|
197
402
|
isFeatured: input.isFeatured ?? false,
|
|
198
|
-
modifierGroupIds: input.modifierGroupIds
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
items.push(item);
|
|
204
|
-
if (cat) cat.itemCount = (cat.itemCount ?? 0) + 1;
|
|
205
|
-
return item;
|
|
403
|
+
modifierGroupIds: input.modifierGroupIds
|
|
404
|
+
});
|
|
405
|
+
items.push(created);
|
|
406
|
+
if (input.sectionId) await provider.placeItem({ sectionId: input.sectionId, itemId: created.id });
|
|
407
|
+
return created;
|
|
206
408
|
},
|
|
207
|
-
async updateMenuItem(id,
|
|
208
|
-
const
|
|
209
|
-
if (!
|
|
210
|
-
Object.assign(
|
|
211
|
-
return
|
|
409
|
+
async updateMenuItem(id, patch) {
|
|
410
|
+
const found = items.find((i) => i.id === id);
|
|
411
|
+
if (!found) throw new Error("Item not found");
|
|
412
|
+
Object.assign(found, patch, { updatedAt: now() });
|
|
413
|
+
return found;
|
|
212
414
|
},
|
|
213
415
|
async deleteMenuItem(id) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (removed.categoryId) {
|
|
218
|
-
const cat = categories.find((c) => c.id === removed.categoryId);
|
|
219
|
-
if (cat && cat.itemCount) cat.itemCount--;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
416
|
+
for (let i = placements.length - 1; i >= 0; i--) if (placements[i].itemId === id) placements.splice(i, 1);
|
|
417
|
+
const index = items.findIndex((i) => i.id === id);
|
|
418
|
+
if (index >= 0) items.splice(index, 1);
|
|
222
419
|
},
|
|
223
420
|
async toggleAvailability(id, field, value) {
|
|
224
|
-
const
|
|
225
|
-
if (!
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return
|
|
421
|
+
const found = items.find((i) => i.id === id);
|
|
422
|
+
if (!found) throw new Error("Item not found");
|
|
423
|
+
found[field] = value;
|
|
424
|
+
found.updatedAt = now();
|
|
425
|
+
return found;
|
|
229
426
|
},
|
|
230
|
-
async reorderItems(
|
|
231
|
-
ids.forEach((id,
|
|
232
|
-
const
|
|
233
|
-
if (
|
|
427
|
+
async reorderItems(_categoryId, ids) {
|
|
428
|
+
ids.forEach((id, index) => {
|
|
429
|
+
const found = items.find((i) => i.id === id);
|
|
430
|
+
if (found) found.sortOrder = index;
|
|
234
431
|
});
|
|
235
432
|
},
|
|
433
|
+
// --- Modifier groups ---
|
|
236
434
|
async getModifierGroups() {
|
|
237
435
|
return [...modifierGroups].sort((a, b) => a.sortOrder - b.sortOrder);
|
|
238
436
|
},
|
|
239
437
|
async createModifierGroup(input) {
|
|
240
|
-
const
|
|
438
|
+
const created = {
|
|
241
439
|
id: nextId("mg"),
|
|
242
|
-
name: input.name ?? "",
|
|
440
|
+
name: input.name ?? "Grupo",
|
|
243
441
|
minSelections: input.minSelections ?? 0,
|
|
244
442
|
maxSelections: input.maxSelections ?? 1,
|
|
245
443
|
isRequired: input.isRequired ?? false,
|
|
246
444
|
sortOrder: input.sortOrder ?? modifierGroups.length,
|
|
247
445
|
items: input.items ?? [],
|
|
248
|
-
tenantId,
|
|
446
|
+
tenantId: TENANT,
|
|
249
447
|
createdAt: now()
|
|
250
448
|
};
|
|
251
|
-
modifierGroups.push(
|
|
252
|
-
return
|
|
449
|
+
modifierGroups.push(created);
|
|
450
|
+
return created;
|
|
253
451
|
},
|
|
254
|
-
async updateModifierGroup(id,
|
|
255
|
-
const
|
|
256
|
-
if (!
|
|
257
|
-
Object.assign(
|
|
258
|
-
return
|
|
452
|
+
async updateModifierGroup(id, patch) {
|
|
453
|
+
const found = modifierGroups.find((g) => g.id === id);
|
|
454
|
+
if (!found) throw new Error("Modifier group not found");
|
|
455
|
+
Object.assign(found, patch);
|
|
456
|
+
return found;
|
|
259
457
|
},
|
|
260
458
|
async deleteModifierGroup(id) {
|
|
261
|
-
const
|
|
262
|
-
if (
|
|
459
|
+
const index = modifierGroups.findIndex((g) => g.id === id);
|
|
460
|
+
if (index >= 0) modifierGroups.splice(index, 1);
|
|
263
461
|
},
|
|
462
|
+
// --- Summary ---
|
|
264
463
|
async getSummary() {
|
|
265
|
-
const
|
|
266
|
-
const soldOut = items.filter((i) => i.status === "sold_out");
|
|
267
|
-
const hidden = items.filter((i) => i.status === "hidden");
|
|
268
|
-
const prices = items.filter((i) => i.price > 0).map((i) => i.price);
|
|
464
|
+
const prices = items.map((i) => i.price);
|
|
269
465
|
return {
|
|
270
466
|
totalItems: items.length,
|
|
271
|
-
availableItems: available.length,
|
|
272
|
-
soldOutItems:
|
|
273
|
-
hiddenItems: hidden.length,
|
|
274
|
-
categoryCount:
|
|
467
|
+
availableItems: items.filter((i) => i.status === "available").length,
|
|
468
|
+
soldOutItems: items.filter((i) => i.status === "sold_out").length,
|
|
469
|
+
hiddenItems: items.filter((i) => i.status === "hidden").length,
|
|
470
|
+
categoryCount: sections.length,
|
|
275
471
|
averagePrice: prices.length ? prices.reduce((a, b) => a + b, 0) / prices.length : 0,
|
|
276
472
|
featuredCount: items.filter((i) => i.isFeatured).length
|
|
277
473
|
};
|
|
278
474
|
}
|
|
279
475
|
};
|
|
476
|
+
return provider;
|
|
280
477
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
478
|
+
|
|
479
|
+
// src/data/tables.ts
|
|
480
|
+
var T = {
|
|
481
|
+
menus: "plg_menu_menus",
|
|
482
|
+
sections: "plg_menu_sections",
|
|
483
|
+
placements: "plg_menu_placements",
|
|
484
|
+
items: "plg_menu_items",
|
|
485
|
+
allergens: "plg_menu_allergens",
|
|
486
|
+
dietaryTags: "plg_menu_dietary_tags",
|
|
487
|
+
modifierGroups: "plg_menu_modifier_groups",
|
|
488
|
+
modifiers: "plg_menu_modifiers",
|
|
489
|
+
/** Spine tables this plugin reads/writes through. */
|
|
490
|
+
products: "products",
|
|
491
|
+
categories: "categories",
|
|
492
|
+
/** Bucket for item, section and import photos. */
|
|
493
|
+
mediaBucket: "menu-images"
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
// src/data/supabase.ts
|
|
497
|
+
var CATEGORY_KIND = "menu_category";
|
|
498
|
+
function rowToMenu(row, counts) {
|
|
499
|
+
return {
|
|
500
|
+
id: row.id,
|
|
501
|
+
name: row.name,
|
|
502
|
+
description: row.description ?? void 0,
|
|
503
|
+
isActive: row.is_active ?? true,
|
|
504
|
+
sortOrder: row.sort_order ?? 0,
|
|
505
|
+
channels: row.channels ?? ["pos", "delivery", "digital"],
|
|
506
|
+
availability: row.availability ?? {},
|
|
507
|
+
sectionCount: counts?.sections,
|
|
508
|
+
itemCount: counts?.items,
|
|
509
|
+
tenantId: row.tenant_id,
|
|
510
|
+
createdAt: row.created_at,
|
|
511
|
+
updatedAt: row.updated_at
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function rowToSection(row) {
|
|
515
|
+
return {
|
|
516
|
+
id: row.id,
|
|
517
|
+
menuId: row.menu_id,
|
|
518
|
+
name: row.name,
|
|
519
|
+
description: row.description ?? void 0,
|
|
520
|
+
imageUrl: row.image_url ?? void 0,
|
|
521
|
+
sortOrder: row.sort_order ?? 0,
|
|
522
|
+
isActive: row.is_active ?? true,
|
|
523
|
+
availability: row.availability ?? {},
|
|
524
|
+
tenantId: row.tenant_id,
|
|
525
|
+
createdAt: row.created_at,
|
|
526
|
+
updatedAt: row.updated_at
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
function rowToPlacement(row) {
|
|
530
|
+
return {
|
|
531
|
+
id: row.id,
|
|
532
|
+
sectionId: row.section_id,
|
|
533
|
+
itemId: row.item_id,
|
|
534
|
+
sortOrder: row.sort_order ?? 0,
|
|
535
|
+
priceOverride: row.price_override ?? void 0,
|
|
536
|
+
isActive: row.is_active ?? true,
|
|
537
|
+
tenantId: row.tenant_id
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function rowToCategory(row) {
|
|
541
|
+
return {
|
|
542
|
+
id: row.id,
|
|
543
|
+
name: row.name,
|
|
544
|
+
parentId: row.parent_id ?? void 0,
|
|
545
|
+
icon: row.icon ?? void 0,
|
|
546
|
+
color: row.color ?? void 0,
|
|
547
|
+
sortOrder: row.sort_order ?? 0,
|
|
548
|
+
isActive: row.is_active ?? true,
|
|
549
|
+
tenantId: row.tenant_id,
|
|
550
|
+
createdAt: row.created_at,
|
|
551
|
+
updatedAt: row.updated_at
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
function rowToItem(row, currency) {
|
|
555
|
+
const ext = Array.isArray(row.plg_menu_items) ? row.plg_menu_items[0] : row.plg_menu_items;
|
|
556
|
+
const meta = row.metadata ?? {};
|
|
557
|
+
return {
|
|
558
|
+
id: row.id,
|
|
559
|
+
name: row.name,
|
|
560
|
+
description: row.description ?? void 0,
|
|
561
|
+
price: Number(row.price ?? 0),
|
|
562
|
+
cost: row.cost != null ? Number(row.cost) : void 0,
|
|
563
|
+
currency,
|
|
564
|
+
sku: row.sku ?? void 0,
|
|
565
|
+
imageUrl: row.image_url ?? void 0,
|
|
566
|
+
categoryId: row.category_id ?? void 0,
|
|
567
|
+
status: ext?.status ?? "available",
|
|
568
|
+
isActive: row.is_active ?? true,
|
|
569
|
+
prepTimeMinutes: ext?.prep_time_minutes ?? void 0,
|
|
570
|
+
allergens: ext?.allergens ?? [],
|
|
571
|
+
dietaryTags: ext?.dietary_tags ?? [],
|
|
572
|
+
availableForPos: ext?.available_for_pos ?? true,
|
|
573
|
+
availableForDelivery: ext?.available_for_delivery ?? true,
|
|
574
|
+
deliveryPrice: ext?.delivery_price != null ? Number(ext.delivery_price) : void 0,
|
|
575
|
+
sortOrder: ext?.sort_order ?? 0,
|
|
576
|
+
isFeatured: ext?.is_featured ?? false,
|
|
577
|
+
metadata: meta,
|
|
578
|
+
tenantId: row.tenant_id,
|
|
579
|
+
createdAt: row.created_at,
|
|
580
|
+
updatedAt: row.updated_at
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function createSupabaseMenuProvider(options) {
|
|
584
|
+
const currency = options?.currencyCode ?? "BRL";
|
|
585
|
+
function db() {
|
|
586
|
+
const client = getSupabaseClientOptional();
|
|
587
|
+
if (!client) throw new Error("Supabase not initialized");
|
|
588
|
+
return client;
|
|
589
|
+
}
|
|
590
|
+
function tenant() {
|
|
591
|
+
return getActiveTenantId() ?? void 0;
|
|
592
|
+
}
|
|
593
|
+
function fail(error) {
|
|
594
|
+
if (error) throw new Error(error.message ?? "Menu request failed");
|
|
595
|
+
}
|
|
596
|
+
async function nextOrder(table, column, value) {
|
|
597
|
+
const { data } = await db().from(table).select("sort_order").eq(column, value).order("sort_order", { ascending: false }).limit(1);
|
|
598
|
+
return (data?.[0]?.sort_order ?? 0) + 1;
|
|
599
|
+
}
|
|
600
|
+
const provider = {
|
|
601
|
+
// --- Menus -------------------------------------------------------------
|
|
602
|
+
async getMenus() {
|
|
603
|
+
const { data, error } = await db().from(T.menus).select("*").order("sort_order", { ascending: true });
|
|
604
|
+
fail(error);
|
|
605
|
+
let menus = data ?? [];
|
|
606
|
+
if (menus.length === 0) {
|
|
607
|
+
const created = await provider.createMenu({ name: "Card\xE1pio principal" });
|
|
608
|
+
menus = [{
|
|
609
|
+
...created,
|
|
610
|
+
is_active: true,
|
|
611
|
+
sort_order: 0,
|
|
612
|
+
tenant_id: created.tenantId,
|
|
613
|
+
created_at: created.createdAt,
|
|
614
|
+
updated_at: created.updatedAt,
|
|
615
|
+
availability: {},
|
|
616
|
+
channels: created.channels
|
|
617
|
+
}];
|
|
618
|
+
}
|
|
619
|
+
const menuIds = menus.map((m) => m.id);
|
|
620
|
+
const { data: sections } = await db().from(T.sections).select("id, menu_id").in("menu_id", menuIds);
|
|
621
|
+
const sectionIds = (sections ?? []).map((s) => s.id);
|
|
622
|
+
const { data: placements } = sectionIds.length ? await db().from(T.placements).select("section_id").in("section_id", sectionIds) : { data: [] };
|
|
623
|
+
const sectionToMenu = new Map((sections ?? []).map((s) => [s.id, s.menu_id]));
|
|
624
|
+
return menus.map((row) => {
|
|
625
|
+
const mySections = (sections ?? []).filter((s) => s.menu_id === row.id);
|
|
626
|
+
const myItems = (placements ?? []).filter((p) => sectionToMenu.get(p.section_id) === row.id);
|
|
627
|
+
return rowToMenu(row, { sections: mySections.length, items: myItems.length });
|
|
298
628
|
});
|
|
299
629
|
},
|
|
300
|
-
async
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
630
|
+
async createMenu(input) {
|
|
631
|
+
const { data, error } = await db().from(T.menus).insert({
|
|
632
|
+
tenant_id: tenant(),
|
|
633
|
+
name: input.name,
|
|
634
|
+
description: input.description ?? null,
|
|
635
|
+
is_active: input.isActive ?? true,
|
|
636
|
+
sort_order: input.sortOrder ?? await nextOrder(T.menus, "tenant_id", tenant() ?? ""),
|
|
637
|
+
channels: input.channels ?? ["pos", "delivery", "digital"],
|
|
638
|
+
availability: input.availability ?? {}
|
|
639
|
+
}).select().single();
|
|
640
|
+
fail(error);
|
|
641
|
+
return rowToMenu(data);
|
|
306
642
|
},
|
|
307
|
-
async
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
643
|
+
async updateMenu(id, patch) {
|
|
644
|
+
const row = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
645
|
+
if (patch.name !== void 0) row.name = patch.name;
|
|
646
|
+
if (patch.description !== void 0) row.description = patch.description;
|
|
647
|
+
if (patch.isActive !== void 0) row.is_active = patch.isActive;
|
|
648
|
+
if (patch.sortOrder !== void 0) row.sort_order = patch.sortOrder;
|
|
649
|
+
if (patch.channels !== void 0) row.channels = patch.channels;
|
|
650
|
+
if (patch.availability !== void 0) row.availability = patch.availability;
|
|
651
|
+
const { data, error } = await db().from(T.menus).update(row).eq("id", id).select().single();
|
|
652
|
+
fail(error);
|
|
653
|
+
return rowToMenu(data);
|
|
313
654
|
},
|
|
314
|
-
async
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
655
|
+
async deleteMenu(id) {
|
|
656
|
+
const { error } = await db().from(T.menus).delete().eq("id", id);
|
|
657
|
+
fail(error);
|
|
658
|
+
},
|
|
659
|
+
async duplicateMenu(id, name) {
|
|
660
|
+
const { data: source, error } = await db().from(T.menus).select("*").eq("id", id).single();
|
|
661
|
+
fail(error);
|
|
662
|
+
const copy = await provider.createMenu({
|
|
663
|
+
name,
|
|
664
|
+
description: source.description ?? void 0,
|
|
665
|
+
channels: source.channels,
|
|
666
|
+
availability: source.availability,
|
|
667
|
+
isActive: false
|
|
668
|
+
// a copy starts off-air: it is edited before it is served
|
|
319
669
|
});
|
|
670
|
+
const sections = await provider.getSections(id);
|
|
671
|
+
const placements = await provider.getPlacements(id);
|
|
672
|
+
for (const section of sections) {
|
|
673
|
+
const clone = await provider.createSection({
|
|
674
|
+
menuId: copy.id,
|
|
675
|
+
name: section.name,
|
|
676
|
+
description: section.description,
|
|
677
|
+
imageUrl: section.imageUrl,
|
|
678
|
+
sortOrder: section.sortOrder,
|
|
679
|
+
isActive: section.isActive,
|
|
680
|
+
availability: section.availability
|
|
681
|
+
});
|
|
682
|
+
const mine = placements.filter((p) => p.sectionId === section.id);
|
|
683
|
+
for (const placement of mine) {
|
|
684
|
+
await provider.placeItem({
|
|
685
|
+
sectionId: clone.id,
|
|
686
|
+
itemId: placement.itemId,
|
|
687
|
+
sortOrder: placement.sortOrder,
|
|
688
|
+
priceOverride: placement.priceOverride,
|
|
689
|
+
isActive: placement.isActive
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return copy;
|
|
694
|
+
},
|
|
695
|
+
async reorderMenus(ids) {
|
|
696
|
+
await Promise.all(ids.map((id, index) => db().from(T.menus).update({ sort_order: index }).eq("id", id)));
|
|
697
|
+
},
|
|
698
|
+
// --- Sections ----------------------------------------------------------
|
|
699
|
+
async getSections(menuId) {
|
|
700
|
+
const { data, error } = await db().from(T.sections).select("*").eq("menu_id", menuId).order("sort_order", { ascending: true });
|
|
701
|
+
fail(error);
|
|
702
|
+
return (data ?? []).map(rowToSection);
|
|
703
|
+
},
|
|
704
|
+
async createSection(input) {
|
|
705
|
+
const { data, error } = await db().from(T.sections).insert({
|
|
706
|
+
tenant_id: tenant(),
|
|
707
|
+
menu_id: input.menuId,
|
|
708
|
+
name: input.name,
|
|
709
|
+
description: input.description ?? null,
|
|
710
|
+
image_url: input.imageUrl ?? null,
|
|
711
|
+
sort_order: input.sortOrder ?? await nextOrder(T.sections, "menu_id", input.menuId),
|
|
712
|
+
is_active: input.isActive ?? true,
|
|
713
|
+
availability: input.availability ?? {}
|
|
714
|
+
}).select().single();
|
|
715
|
+
fail(error);
|
|
716
|
+
return rowToSection(data);
|
|
717
|
+
},
|
|
718
|
+
async updateSection(id, patch) {
|
|
719
|
+
const row = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
720
|
+
if (patch.name !== void 0) row.name = patch.name;
|
|
721
|
+
if (patch.description !== void 0) row.description = patch.description;
|
|
722
|
+
if (patch.imageUrl !== void 0) row.image_url = patch.imageUrl;
|
|
723
|
+
if (patch.sortOrder !== void 0) row.sort_order = patch.sortOrder;
|
|
724
|
+
if (patch.isActive !== void 0) row.is_active = patch.isActive;
|
|
725
|
+
if (patch.availability !== void 0) row.availability = patch.availability;
|
|
726
|
+
const { data, error } = await db().from(T.sections).update(row).eq("id", id).select().single();
|
|
727
|
+
fail(error);
|
|
728
|
+
return rowToSection(data);
|
|
729
|
+
},
|
|
730
|
+
async deleteSection(id) {
|
|
731
|
+
const { error } = await db().from(T.sections).delete().eq("id", id);
|
|
732
|
+
fail(error);
|
|
733
|
+
},
|
|
734
|
+
async reorderSections(_menuId, ids) {
|
|
735
|
+
await Promise.all(ids.map((id, index) => db().from(T.sections).update({ sort_order: index }).eq("id", id)));
|
|
736
|
+
},
|
|
737
|
+
// --- Placements --------------------------------------------------------
|
|
738
|
+
async getPlacements(menuId) {
|
|
739
|
+
const { data: sections } = await db().from(T.sections).select("id").eq("menu_id", menuId);
|
|
740
|
+
const ids = (sections ?? []).map((s) => s.id);
|
|
741
|
+
if (ids.length === 0) return [];
|
|
742
|
+
const { data, error } = await db().from(T.placements).select("*").in("section_id", ids).order("sort_order", { ascending: true });
|
|
743
|
+
fail(error);
|
|
744
|
+
return (data ?? []).map(rowToPlacement);
|
|
745
|
+
},
|
|
746
|
+
async placeItem(input) {
|
|
747
|
+
const { data, error } = await db().from(T.placements).upsert({
|
|
748
|
+
tenant_id: tenant(),
|
|
749
|
+
section_id: input.sectionId,
|
|
750
|
+
item_id: input.itemId,
|
|
751
|
+
sort_order: input.sortOrder ?? await nextOrder(T.placements, "section_id", input.sectionId),
|
|
752
|
+
price_override: input.priceOverride ?? null,
|
|
753
|
+
is_active: input.isActive ?? true
|
|
754
|
+
}, { onConflict: "section_id,item_id" }).select().single();
|
|
755
|
+
fail(error);
|
|
756
|
+
return rowToPlacement(data);
|
|
757
|
+
},
|
|
758
|
+
async updatePlacement(id, patch) {
|
|
759
|
+
const row = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
760
|
+
if (patch.sectionId !== void 0) row.section_id = patch.sectionId;
|
|
761
|
+
if (patch.sortOrder !== void 0) row.sort_order = patch.sortOrder;
|
|
762
|
+
if (patch.priceOverride !== void 0) row.price_override = patch.priceOverride;
|
|
763
|
+
if (patch.isActive !== void 0) row.is_active = patch.isActive;
|
|
764
|
+
const { data, error } = await db().from(T.placements).update(row).eq("id", id).select().single();
|
|
765
|
+
fail(error);
|
|
766
|
+
return rowToPlacement(data);
|
|
767
|
+
},
|
|
768
|
+
async removePlacement(id) {
|
|
769
|
+
const { error } = await db().from(T.placements).delete().eq("id", id);
|
|
770
|
+
fail(error);
|
|
771
|
+
},
|
|
772
|
+
async reorderPlacements(_sectionId, placementIds) {
|
|
773
|
+
await Promise.all(placementIds.map((id, index) => db().from(T.placements).update({ sort_order: index }).eq("id", id)));
|
|
774
|
+
},
|
|
775
|
+
async movePlacement(placementId2, toSectionId, toIndex) {
|
|
776
|
+
const { error } = await db().from(T.placements).update({ section_id: toSectionId, sort_order: toIndex, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", placementId2);
|
|
777
|
+
fail(error);
|
|
778
|
+
},
|
|
779
|
+
// --- Media -------------------------------------------------------------
|
|
780
|
+
async uploadImage(file, kind) {
|
|
781
|
+
const tenantId = tenant();
|
|
782
|
+
if (!tenantId) throw new Error("No active workspace for the upload");
|
|
783
|
+
const ext = (file.name.split(".").pop() ?? "jpg").toLowerCase().replace(/[^a-z0-9]/g, "") || "jpg";
|
|
784
|
+
const path = `${tenantId}/${kind}/${crypto.randomUUID()}.${ext}`;
|
|
785
|
+
const { error } = await db().storage.from(T.mediaBucket).upload(path, file, { contentType: file.type, upsert: false });
|
|
786
|
+
fail(error);
|
|
787
|
+
const { data } = db().storage.from(T.mediaBucket).getPublicUrl(path);
|
|
788
|
+
return data.publicUrl;
|
|
789
|
+
},
|
|
790
|
+
// --- Categories (legacy flat grouping, still readable) -----------------
|
|
791
|
+
async getCategories() {
|
|
792
|
+
const { data, error } = await db().from(T.categories).select("*").eq("kind", CATEGORY_KIND).order("sort_order", { ascending: true });
|
|
793
|
+
fail(error);
|
|
794
|
+
return (data ?? []).map(rowToCategory);
|
|
795
|
+
},
|
|
796
|
+
async getCategoryById(id) {
|
|
797
|
+
const { data } = await db().from(T.categories).select("*").eq("id", id).maybeSingle();
|
|
798
|
+
return data ? rowToCategory(data) : null;
|
|
799
|
+
},
|
|
800
|
+
async createCategory(input) {
|
|
801
|
+
const { data, error } = await db().from(T.categories).insert({
|
|
802
|
+
tenant_id: tenant(),
|
|
803
|
+
kind: CATEGORY_KIND,
|
|
804
|
+
name: input.name,
|
|
805
|
+
parent_id: input.parentId ?? null,
|
|
806
|
+
icon: input.icon ?? null,
|
|
807
|
+
color: input.color ?? null,
|
|
808
|
+
sort_order: input.sortOrder ?? 0,
|
|
809
|
+
is_active: true
|
|
810
|
+
}).select().single();
|
|
811
|
+
fail(error);
|
|
812
|
+
return rowToCategory(data);
|
|
813
|
+
},
|
|
814
|
+
async updateCategory(id, patch) {
|
|
815
|
+
const row = {};
|
|
816
|
+
if (patch.name !== void 0) row.name = patch.name;
|
|
817
|
+
if (patch.sortOrder !== void 0) row.sort_order = patch.sortOrder;
|
|
818
|
+
if (patch.isActive !== void 0) row.is_active = patch.isActive;
|
|
819
|
+
if (patch.icon !== void 0) row.icon = patch.icon;
|
|
820
|
+
if (patch.color !== void 0) row.color = patch.color;
|
|
821
|
+
const { data, error } = await db().from(T.categories).update(row).eq("id", id).select().single();
|
|
822
|
+
fail(error);
|
|
823
|
+
return rowToCategory(data);
|
|
824
|
+
},
|
|
825
|
+
async deleteCategory(id) {
|
|
826
|
+
const { error } = await db().from(T.categories).delete().eq("id", id);
|
|
827
|
+
fail(error);
|
|
828
|
+
},
|
|
829
|
+
async reorderCategories(ids) {
|
|
830
|
+
await Promise.all(ids.map((id, index) => db().from(T.categories).update({ sort_order: index }).eq("id", id)));
|
|
831
|
+
},
|
|
832
|
+
// --- Items -------------------------------------------------------------
|
|
833
|
+
async getMenuItems(query) {
|
|
834
|
+
let qb = db().from(T.products).select(`*, ${T.items}!inner(*)`, { count: "exact" });
|
|
835
|
+
if (query.search) qb = qb.ilike("name", `%${query.search}%`);
|
|
836
|
+
if (query.categoryId) qb = qb.eq("category_id", query.categoryId);
|
|
837
|
+
if (query.status) {
|
|
838
|
+
const statuses = Array.isArray(query.status) ? query.status : [query.status];
|
|
839
|
+
qb = qb.in(`${T.items}.status`, statuses);
|
|
840
|
+
}
|
|
841
|
+
if (query.availableForPos !== void 0) qb = qb.eq(`${T.items}.available_for_pos`, query.availableForPos);
|
|
842
|
+
if (query.availableForDelivery !== void 0) qb = qb.eq(`${T.items}.available_for_delivery`, query.availableForDelivery);
|
|
843
|
+
if (query.isFeatured !== void 0) qb = qb.eq(`${T.items}.is_featured`, query.isFeatured);
|
|
844
|
+
const page = query.page ?? 1;
|
|
845
|
+
const pageSize = query.pageSize ?? 200;
|
|
846
|
+
qb = qb.range((page - 1) * pageSize, page * pageSize - 1).order("name", { ascending: true });
|
|
847
|
+
const { data, count, error } = await qb;
|
|
848
|
+
fail(error);
|
|
849
|
+
return { data: (data ?? []).map((row) => rowToItem(row, currency)), total: count ?? 0 };
|
|
850
|
+
},
|
|
851
|
+
async getMenuItemById(id) {
|
|
852
|
+
const { data } = await db().from(T.products).select(`*, ${T.items}(*)`).eq("id", id).maybeSingle();
|
|
853
|
+
return data ? rowToItem(data, currency) : null;
|
|
320
854
|
},
|
|
321
855
|
async createMenuItem(input) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
856
|
+
const tenantId = tenant();
|
|
857
|
+
const { data: product, error } = await db().from(T.products).insert({
|
|
858
|
+
tenant_id: tenantId,
|
|
859
|
+
name: input.name,
|
|
860
|
+
description: input.description ?? null,
|
|
861
|
+
price: input.price ?? 0,
|
|
862
|
+
cost: input.cost ?? 0,
|
|
863
|
+
image_url: input.imageUrl ?? null,
|
|
864
|
+
category_id: input.categoryId ?? null,
|
|
865
|
+
is_active: true,
|
|
866
|
+
metadata: { productType: "sale" }
|
|
867
|
+
}).select().single();
|
|
868
|
+
fail(error);
|
|
869
|
+
const { error: extError } = await db().from(T.items).insert({
|
|
870
|
+
product_id: product.id,
|
|
871
|
+
tenant_id: tenantId,
|
|
872
|
+
status: input.status ?? "available",
|
|
873
|
+
available_for_pos: input.availableForPos ?? true,
|
|
874
|
+
available_for_delivery: input.availableForDelivery ?? true,
|
|
875
|
+
delivery_price: input.deliveryPrice ?? null,
|
|
876
|
+
prep_time_minutes: input.prepTimeMinutes ?? null,
|
|
877
|
+
allergens: input.allergens ?? [],
|
|
878
|
+
dietary_tags: input.dietaryTags ?? [],
|
|
879
|
+
is_featured: input.isFeatured ?? false
|
|
880
|
+
});
|
|
881
|
+
if (extError) {
|
|
882
|
+
await db().from(T.products).delete().eq("id", product.id);
|
|
883
|
+
throw new Error(extError.message);
|
|
332
884
|
}
|
|
885
|
+
if (input.sectionId) {
|
|
886
|
+
await provider.placeItem({ sectionId: input.sectionId, itemId: product.id });
|
|
887
|
+
}
|
|
888
|
+
return await provider.getMenuItemById(product.id);
|
|
333
889
|
},
|
|
334
|
-
async updateMenuItem(id,
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
890
|
+
async updateMenuItem(id, patch) {
|
|
891
|
+
const spine = {};
|
|
892
|
+
if (patch.name !== void 0) spine.name = patch.name;
|
|
893
|
+
if (patch.description !== void 0) spine.description = patch.description;
|
|
894
|
+
if (patch.price !== void 0) spine.price = patch.price;
|
|
895
|
+
if (patch.cost !== void 0) spine.cost = patch.cost;
|
|
896
|
+
if (patch.imageUrl !== void 0) spine.image_url = patch.imageUrl;
|
|
897
|
+
if (patch.categoryId !== void 0) spine.category_id = patch.categoryId ?? null;
|
|
898
|
+
if (Object.keys(spine).length > 0) {
|
|
899
|
+
const { error } = await db().from(T.products).update(spine).eq("id", id);
|
|
900
|
+
fail(error);
|
|
901
|
+
}
|
|
902
|
+
const ext = {};
|
|
903
|
+
if (patch.status !== void 0) ext.status = patch.status;
|
|
904
|
+
if (patch.availableForPos !== void 0) ext.available_for_pos = patch.availableForPos;
|
|
905
|
+
if (patch.availableForDelivery !== void 0) ext.available_for_delivery = patch.availableForDelivery;
|
|
906
|
+
if (patch.deliveryPrice !== void 0) ext.delivery_price = patch.deliveryPrice;
|
|
907
|
+
if (patch.prepTimeMinutes !== void 0) ext.prep_time_minutes = patch.prepTimeMinutes;
|
|
908
|
+
if (patch.allergens !== void 0) ext.allergens = patch.allergens;
|
|
909
|
+
if (patch.dietaryTags !== void 0) ext.dietary_tags = patch.dietaryTags;
|
|
910
|
+
if (patch.isFeatured !== void 0) ext.is_featured = patch.isFeatured;
|
|
911
|
+
if (Object.keys(ext).length > 0) {
|
|
912
|
+
ext.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
913
|
+
const { error } = await db().from(T.items).upsert({ product_id: id, tenant_id: tenant(), ...ext }, { onConflict: "product_id" });
|
|
914
|
+
fail(error);
|
|
345
915
|
}
|
|
916
|
+
return await provider.getMenuItemById(id);
|
|
346
917
|
},
|
|
347
918
|
async deleteMenuItem(id) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const [result, summary] = await Promise.all([provider.getMenuItems(query), provider.getSummary()]);
|
|
352
|
-
set({ menuItems: result.data, menuItemsTotal: result.total, summary });
|
|
353
|
-
toast.success("Menu item deleted");
|
|
354
|
-
} catch (err) {
|
|
355
|
-
toast.error("Failed to delete menu item", { description: err?.message });
|
|
356
|
-
throw err;
|
|
357
|
-
}
|
|
919
|
+
const { error } = await db().from(T.items).delete().eq("product_id", id);
|
|
920
|
+
fail(error);
|
|
921
|
+
await db().from(T.placements).delete().eq("item_id", id);
|
|
358
922
|
},
|
|
359
923
|
async toggleAvailability(id, field, value) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
throw err;
|
|
368
|
-
}
|
|
924
|
+
const column = field === "status" ? "status" : field === "availableForPos" ? "available_for_pos" : field === "isFeatured" ? "is_featured" : "available_for_delivery";
|
|
925
|
+
const { error } = await db().from(T.items).upsert(
|
|
926
|
+
{ product_id: id, tenant_id: tenant(), [column]: value, updated_at: (/* @__PURE__ */ new Date()).toISOString() },
|
|
927
|
+
{ onConflict: "product_id" }
|
|
928
|
+
);
|
|
929
|
+
fail(error);
|
|
930
|
+
return await provider.getMenuItemById(id);
|
|
369
931
|
},
|
|
370
|
-
async
|
|
932
|
+
async reorderItems(_categoryId, ids) {
|
|
933
|
+
await Promise.all(ids.map((id, index) => db().from(T.items).update({ sort_order: index }).eq("product_id", id)));
|
|
934
|
+
},
|
|
935
|
+
// --- Modifier groups ---------------------------------------------------
|
|
936
|
+
async getModifierGroups() {
|
|
937
|
+
const { data, error } = await db().from(T.modifierGroups).select(`*, ${T.modifiers}(*)`).order("sort_order", { ascending: true });
|
|
938
|
+
fail(error);
|
|
939
|
+
return (data ?? []).map((row) => ({
|
|
940
|
+
id: row.id,
|
|
941
|
+
name: row.name,
|
|
942
|
+
minSelections: row.min_selections ?? 0,
|
|
943
|
+
maxSelections: row.max_selections ?? 1,
|
|
944
|
+
isRequired: row.is_required ?? false,
|
|
945
|
+
sortOrder: row.sort_order ?? 0,
|
|
946
|
+
items: (row[T.modifiers] ?? []).map((m) => ({
|
|
947
|
+
id: m.id,
|
|
948
|
+
groupId: m.group_id,
|
|
949
|
+
name: m.name,
|
|
950
|
+
priceAdjustment: Number(m.price_adjustment ?? 0),
|
|
951
|
+
isActive: m.is_active ?? true,
|
|
952
|
+
sortOrder: m.sort_order ?? 0
|
|
953
|
+
})),
|
|
954
|
+
tenantId: row.tenant_id,
|
|
955
|
+
createdAt: row.created_at
|
|
956
|
+
}));
|
|
957
|
+
},
|
|
958
|
+
async createModifierGroup(input) {
|
|
959
|
+
const { data, error } = await db().from(T.modifierGroups).insert({
|
|
960
|
+
tenant_id: tenant(),
|
|
961
|
+
name: input.name ?? "Grupo",
|
|
962
|
+
min_selections: input.minSelections ?? 0,
|
|
963
|
+
max_selections: input.maxSelections ?? 1,
|
|
964
|
+
is_required: input.isRequired ?? false,
|
|
965
|
+
sort_order: input.sortOrder ?? 0
|
|
966
|
+
}).select().single();
|
|
967
|
+
fail(error);
|
|
968
|
+
return { ...data, items: [], minSelections: data.min_selections, maxSelections: data.max_selections, isRequired: data.is_required, sortOrder: data.sort_order, tenantId: data.tenant_id, createdAt: data.created_at };
|
|
969
|
+
},
|
|
970
|
+
async updateModifierGroup(id, patch) {
|
|
971
|
+
const row = {};
|
|
972
|
+
if (patch.name !== void 0) row.name = patch.name;
|
|
973
|
+
if (patch.minSelections !== void 0) row.min_selections = patch.minSelections;
|
|
974
|
+
if (patch.maxSelections !== void 0) row.max_selections = patch.maxSelections;
|
|
975
|
+
if (patch.isRequired !== void 0) row.is_required = patch.isRequired;
|
|
976
|
+
if (patch.sortOrder !== void 0) row.sort_order = patch.sortOrder;
|
|
977
|
+
const { error } = await db().from(T.modifierGroups).update(row).eq("id", id);
|
|
978
|
+
fail(error);
|
|
979
|
+
const groups = await provider.getModifierGroups();
|
|
980
|
+
return groups.find((g) => g.id === id);
|
|
981
|
+
},
|
|
982
|
+
async deleteModifierGroup(id) {
|
|
983
|
+
const { error } = await db().from(T.modifierGroups).delete().eq("id", id);
|
|
984
|
+
fail(error);
|
|
985
|
+
},
|
|
986
|
+
// --- Summary -----------------------------------------------------------
|
|
987
|
+
async getSummary() {
|
|
988
|
+
const [{ data: items }, { data: sections }] = await Promise.all([
|
|
989
|
+
db().from(T.items).select("status, is_featured"),
|
|
990
|
+
db().from(T.sections).select("id")
|
|
991
|
+
]);
|
|
992
|
+
const { data: prices } = await db().from(T.products).select(`price, ${T.items}!inner(product_id)`);
|
|
993
|
+
const rows = items ?? [];
|
|
994
|
+
const priceList = (prices ?? []).map((p) => Number(p.price ?? 0));
|
|
995
|
+
return {
|
|
996
|
+
totalItems: rows.length,
|
|
997
|
+
availableItems: rows.filter((r) => r.status === "available").length,
|
|
998
|
+
soldOutItems: rows.filter((r) => r.status === "sold_out").length,
|
|
999
|
+
hiddenItems: rows.filter((r) => r.status === "hidden").length,
|
|
1000
|
+
categoryCount: (sections ?? []).length,
|
|
1001
|
+
averagePrice: priceList.length ? priceList.reduce((a, b) => a + b, 0) / priceList.length : 0,
|
|
1002
|
+
featuredCount: rows.filter((r) => r.is_featured).length
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
return provider;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// src/migrations/index.ts
|
|
1010
|
+
var MIGRATION_001_MENU_BASE = `-- Menu Plugin: base tables.
|
|
1011
|
+
--
|
|
1012
|
+
-- Three levels, because a restaurant does not have "a menu": it has a lunch
|
|
1013
|
+
-- menu, a weekend menu and a delivery menu, and the same dish appears in more
|
|
1014
|
+
-- than one of them at more than one price.
|
|
1015
|
+
-- plg_menu_menus \u2014 the menu itself, with when and where it is served
|
|
1016
|
+
-- plg_menu_sections \u2014 ordered sections inside a menu
|
|
1017
|
+
-- plg_menu_placements \u2014 an item's seat in a section (order + price override)
|
|
1018
|
+
-- Items stay in public.products (the spine); plg_menu_items carries only what
|
|
1019
|
+
-- is menu-specific about a product.
|
|
1020
|
+
|
|
1021
|
+
CREATE TABLE IF NOT EXISTS public.plg_menu_menus (
|
|
1022
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1023
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
1024
|
+
name text NOT NULL,
|
|
1025
|
+
description text,
|
|
1026
|
+
is_active boolean NOT NULL DEFAULT true,
|
|
1027
|
+
sort_order integer NOT NULL DEFAULT 0,
|
|
1028
|
+
-- Where it shows: pos | delivery | digital.
|
|
1029
|
+
channels text[] NOT NULL DEFAULT ARRAY['pos','delivery','digital']::text[],
|
|
1030
|
+
-- { days:[0..6], start:'HH:MM', end:'HH:MM', from:'YYYY-MM-DD', to:'YYYY-MM-DD' }
|
|
1031
|
+
-- An empty object means "always", which is the common case.
|
|
1032
|
+
availability jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
1033
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
1034
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
1035
|
+
);
|
|
1036
|
+
ALTER TABLE public.plg_menu_menus ENABLE ROW LEVEL SECURITY;
|
|
1037
|
+
CREATE INDEX IF NOT EXISTS idx_plg_menu_menus_tenant ON public.plg_menu_menus(tenant_id, sort_order);
|
|
1038
|
+
|
|
1039
|
+
CREATE TABLE IF NOT EXISTS public.plg_menu_sections (
|
|
1040
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1041
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
1042
|
+
menu_id uuid NOT NULL REFERENCES public.plg_menu_menus(id) ON DELETE CASCADE,
|
|
1043
|
+
name text NOT NULL,
|
|
1044
|
+
description text,
|
|
1045
|
+
image_url text,
|
|
1046
|
+
sort_order integer NOT NULL DEFAULT 0,
|
|
1047
|
+
is_active boolean NOT NULL DEFAULT true,
|
|
1048
|
+
availability jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
1049
|
+
-- Set when the section was adopted from a legacy flat category, so the
|
|
1050
|
+
-- backfill can run twice without doubling the sections.
|
|
1051
|
+
legacy_category_id uuid,
|
|
1052
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
1053
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
1054
|
+
);
|
|
1055
|
+
ALTER TABLE public.plg_menu_sections ENABLE ROW LEVEL SECURITY;
|
|
1056
|
+
CREATE INDEX IF NOT EXISTS idx_plg_menu_sections_menu ON public.plg_menu_sections(menu_id, sort_order);
|
|
1057
|
+
CREATE INDEX IF NOT EXISTS idx_plg_menu_sections_tenant ON public.plg_menu_sections(tenant_id);
|
|
1058
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_plg_menu_sections_legacy ON public.plg_menu_sections(menu_id, legacy_category_id)
|
|
1059
|
+
WHERE legacy_category_id IS NOT NULL;
|
|
1060
|
+
|
|
1061
|
+
CREATE TABLE IF NOT EXISTS public.plg_menu_placements (
|
|
1062
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1063
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
1064
|
+
section_id uuid NOT NULL REFERENCES public.plg_menu_sections(id) ON DELETE CASCADE,
|
|
1065
|
+
item_id uuid NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,
|
|
1066
|
+
sort_order integer NOT NULL DEFAULT 0,
|
|
1067
|
+
price_override numeric(14,2),
|
|
1068
|
+
is_active boolean NOT NULL DEFAULT true,
|
|
1069
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
1070
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
1071
|
+
);
|
|
1072
|
+
ALTER TABLE public.plg_menu_placements ENABLE ROW LEVEL SECURITY;
|
|
1073
|
+
CREATE INDEX IF NOT EXISTS idx_plg_menu_placements_section ON public.plg_menu_placements(section_id, sort_order);
|
|
1074
|
+
CREATE INDEX IF NOT EXISTS idx_plg_menu_placements_item ON public.plg_menu_placements(item_id);
|
|
1075
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_plg_menu_placements_seat ON public.plg_menu_placements(section_id, item_id);
|
|
1076
|
+
|
|
1077
|
+
CREATE TABLE IF NOT EXISTS public.plg_menu_items (
|
|
1078
|
+
product_id uuid PRIMARY KEY REFERENCES public.products(id) ON DELETE CASCADE,
|
|
1079
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
1080
|
+
status text NOT NULL DEFAULT 'available',
|
|
1081
|
+
available_for_pos boolean NOT NULL DEFAULT true,
|
|
1082
|
+
available_for_delivery boolean NOT NULL DEFAULT true,
|
|
1083
|
+
delivery_price numeric(14,2),
|
|
1084
|
+
prep_time_minutes integer,
|
|
1085
|
+
allergens text[] NOT NULL DEFAULT ARRAY[]::text[],
|
|
1086
|
+
dietary_tags text[] NOT NULL DEFAULT ARRAY[]::text[],
|
|
1087
|
+
is_featured boolean NOT NULL DEFAULT false,
|
|
1088
|
+
sort_order integer NOT NULL DEFAULT 0,
|
|
1089
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
1090
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
1091
|
+
);
|
|
1092
|
+
ALTER TABLE public.plg_menu_items ENABLE ROW LEVEL SECURITY;
|
|
1093
|
+
CREATE INDEX IF NOT EXISTS idx_plg_menu_items_tenant ON public.plg_menu_items(tenant_id);
|
|
1094
|
+
|
|
1095
|
+
-- Modifier groups (add-ons): declared by the plugin's registries, owned here.
|
|
1096
|
+
CREATE TABLE IF NOT EXISTS public.plg_menu_modifier_groups (
|
|
1097
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1098
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
1099
|
+
name text NOT NULL,
|
|
1100
|
+
min_selections integer NOT NULL DEFAULT 0,
|
|
1101
|
+
max_selections integer NOT NULL DEFAULT 1,
|
|
1102
|
+
is_required boolean NOT NULL DEFAULT false,
|
|
1103
|
+
sort_order integer NOT NULL DEFAULT 0,
|
|
1104
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
1105
|
+
);
|
|
1106
|
+
ALTER TABLE public.plg_menu_modifier_groups ENABLE ROW LEVEL SECURITY;
|
|
1107
|
+
CREATE INDEX IF NOT EXISTS idx_plg_menu_modifier_groups_tenant ON public.plg_menu_modifier_groups(tenant_id);
|
|
1108
|
+
|
|
1109
|
+
CREATE TABLE IF NOT EXISTS public.plg_menu_modifiers (
|
|
1110
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1111
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
1112
|
+
group_id uuid NOT NULL REFERENCES public.plg_menu_modifier_groups(id) ON DELETE CASCADE,
|
|
1113
|
+
name text NOT NULL,
|
|
1114
|
+
price_adjustment numeric(14,2) NOT NULL DEFAULT 0,
|
|
1115
|
+
is_active boolean NOT NULL DEFAULT true,
|
|
1116
|
+
sort_order integer NOT NULL DEFAULT 0,
|
|
1117
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
1118
|
+
);
|
|
1119
|
+
ALTER TABLE public.plg_menu_modifiers ENABLE ROW LEVEL SECURITY;
|
|
1120
|
+
CREATE INDEX IF NOT EXISTS idx_plg_menu_modifiers_group ON public.plg_menu_modifiers(group_id, sort_order);
|
|
1121
|
+
|
|
1122
|
+
CREATE TABLE IF NOT EXISTS public.plg_menu_item_modifier_groups (
|
|
1123
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1124
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
1125
|
+
product_id uuid NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,
|
|
1126
|
+
group_id uuid NOT NULL REFERENCES public.plg_menu_modifier_groups(id) ON DELETE CASCADE,
|
|
1127
|
+
sort_order integer NOT NULL DEFAULT 0
|
|
1128
|
+
);
|
|
1129
|
+
ALTER TABLE public.plg_menu_item_modifier_groups ENABLE ROW LEVEL SECURITY;
|
|
1130
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_plg_menu_item_modifier_groups ON public.plg_menu_item_modifier_groups(product_id, group_id);
|
|
1131
|
+
|
|
1132
|
+
CREATE TABLE IF NOT EXISTS public.plg_menu_allergens (
|
|
1133
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1134
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
1135
|
+
name text NOT NULL,
|
|
1136
|
+
icon text,
|
|
1137
|
+
is_active boolean NOT NULL DEFAULT true,
|
|
1138
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
1139
|
+
);
|
|
1140
|
+
ALTER TABLE public.plg_menu_allergens ENABLE ROW LEVEL SECURITY;
|
|
1141
|
+
CREATE INDEX IF NOT EXISTS idx_plg_menu_allergens_tenant ON public.plg_menu_allergens(tenant_id);
|
|
1142
|
+
|
|
1143
|
+
CREATE TABLE IF NOT EXISTS public.plg_menu_dietary_tags (
|
|
1144
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1145
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
1146
|
+
name text NOT NULL,
|
|
1147
|
+
color text,
|
|
1148
|
+
is_active boolean NOT NULL DEFAULT true,
|
|
1149
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
1150
|
+
);
|
|
1151
|
+
ALTER TABLE public.plg_menu_dietary_tags ENABLE ROW LEVEL SECURITY;
|
|
1152
|
+
CREATE INDEX IF NOT EXISTS idx_plg_menu_dietary_tags_tenant ON public.plg_menu_dietary_tags(tenant_id);
|
|
1153
|
+
|
|
1154
|
+
-- Canonical per-tenant RLS + grants for every table above.
|
|
1155
|
+
DO $$
|
|
1156
|
+
DECLARE t text;
|
|
1157
|
+
BEGIN
|
|
1158
|
+
FOR t IN SELECT unnest(ARRAY[
|
|
1159
|
+
'plg_menu_menus','plg_menu_sections','plg_menu_placements','plg_menu_items',
|
|
1160
|
+
'plg_menu_modifier_groups','plg_menu_modifiers','plg_menu_item_modifier_groups',
|
|
1161
|
+
'plg_menu_allergens','plg_menu_dietary_tags'
|
|
1162
|
+
])
|
|
1163
|
+
LOOP
|
|
1164
|
+
EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', t);
|
|
1165
|
+
EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON public.%I TO authenticated', t);
|
|
1166
|
+
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_select') THEN
|
|
1167
|
+
EXECUTE format('CREATE POLICY %I ON public.%I FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_select', t);
|
|
1168
|
+
END IF;
|
|
1169
|
+
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_insert') THEN
|
|
1170
|
+
EXECUTE format('CREATE POLICY %I ON public.%I FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_insert', t);
|
|
1171
|
+
END IF;
|
|
1172
|
+
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_update') THEN
|
|
1173
|
+
EXECUTE format('CREATE POLICY %I ON public.%I FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_update', t);
|
|
1174
|
+
END IF;
|
|
1175
|
+
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_delete') THEN
|
|
1176
|
+
EXECUTE format('CREATE POLICY %I ON public.%I FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_delete', t);
|
|
1177
|
+
END IF;
|
|
1178
|
+
END LOOP;
|
|
1179
|
+
END $$;
|
|
1180
|
+
`;
|
|
1181
|
+
var MIGRATION_002_MENU_BACKFILL = `-- Menu Plugin: adopt whatever menu already exists in the pool.
|
|
1182
|
+
--
|
|
1183
|
+
-- Pools that ran the incubator version of this plugin have a flat world:
|
|
1184
|
+
-- categories in public.categories (kind='menu_category'), item flags in a
|
|
1185
|
+
-- public.menu_items extension, and no menus at all. Rather than ask the
|
|
1186
|
+
-- restaurant to type its menu again, the first menu is BUILT from that data:
|
|
1187
|
+
-- one "Card\xE1pio principal" per tenant, its sections are the old categories, and
|
|
1188
|
+
-- every product already sitting in a category takes its seat.
|
|
1189
|
+
--
|
|
1190
|
+
-- Every step is guarded twice: by the existence of the legacy object (a fresh
|
|
1191
|
+
-- pool has none of it) and by a NOT EXISTS on the target, so re-applying does
|
|
1192
|
+
-- nothing. The legacy tables are never written to or dropped \u2014 an app still
|
|
1193
|
+
-- reading them keeps working.
|
|
1194
|
+
|
|
1195
|
+
DO $$
|
|
1196
|
+
DECLARE
|
|
1197
|
+
has_categories boolean := to_regclass('public.categories') IS NOT NULL;
|
|
1198
|
+
has_legacy_items boolean := to_regclass('public.menu_items') IS NOT NULL;
|
|
1199
|
+
has_products boolean := to_regclass('public.products') IS NOT NULL;
|
|
1200
|
+
has_category_kind boolean := EXISTS (
|
|
1201
|
+
SELECT 1 FROM information_schema.columns
|
|
1202
|
+
WHERE table_schema='public' AND table_name='categories' AND column_name='kind'
|
|
1203
|
+
);
|
|
1204
|
+
has_product_category boolean := EXISTS (
|
|
1205
|
+
SELECT 1 FROM information_schema.columns
|
|
1206
|
+
WHERE table_schema='public' AND table_name='products' AND column_name='category_id'
|
|
1207
|
+
);
|
|
1208
|
+
BEGIN
|
|
1209
|
+
-- 1. One default menu per tenant that has menu categories or menu items.
|
|
1210
|
+
IF has_categories AND has_category_kind THEN
|
|
1211
|
+
EXECUTE $sql$
|
|
1212
|
+
INSERT INTO public.plg_menu_menus (tenant_id, name, description, sort_order)
|
|
1213
|
+
SELECT DISTINCT c.tenant_id, 'Card\xE1pio principal', NULL, 0
|
|
1214
|
+
FROM public.categories c
|
|
1215
|
+
WHERE c.kind = 'menu_category'
|
|
1216
|
+
AND c.tenant_id IS NOT NULL
|
|
1217
|
+
AND NOT EXISTS (SELECT 1 FROM public.plg_menu_menus m WHERE m.tenant_id = c.tenant_id)
|
|
1218
|
+
$sql$;
|
|
1219
|
+
END IF;
|
|
1220
|
+
|
|
1221
|
+
IF has_legacy_items THEN
|
|
1222
|
+
EXECUTE $sql$
|
|
1223
|
+
INSERT INTO public.plg_menu_menus (tenant_id, name, description, sort_order)
|
|
1224
|
+
SELECT DISTINCT mi.tenant_id, 'Card\xE1pio principal', NULL, 0
|
|
1225
|
+
FROM public.menu_items mi
|
|
1226
|
+
WHERE mi.tenant_id IS NOT NULL
|
|
1227
|
+
AND NOT EXISTS (SELECT 1 FROM public.plg_menu_menus m WHERE m.tenant_id = mi.tenant_id)
|
|
1228
|
+
$sql$;
|
|
1229
|
+
END IF;
|
|
1230
|
+
|
|
1231
|
+
-- 2. Sections from the flat categories, keeping name and order.
|
|
1232
|
+
IF has_categories AND has_category_kind THEN
|
|
1233
|
+
EXECUTE $sql$
|
|
1234
|
+
INSERT INTO public.plg_menu_sections (tenant_id, menu_id, name, sort_order, is_active, legacy_category_id)
|
|
1235
|
+
SELECT c.tenant_id, m.id, c.name,
|
|
1236
|
+
COALESCE(NULLIF(c.sort_order, 0), row_number() OVER (PARTITION BY c.tenant_id ORDER BY c.sort_order, c.name)),
|
|
1237
|
+
COALESCE(c.is_active, true), c.id
|
|
1238
|
+
FROM public.categories c
|
|
1239
|
+
JOIN public.plg_menu_menus m
|
|
1240
|
+
ON m.tenant_id = c.tenant_id AND m.sort_order = 0
|
|
1241
|
+
WHERE c.kind = 'menu_category'
|
|
1242
|
+
AND NOT EXISTS (
|
|
1243
|
+
SELECT 1 FROM public.plg_menu_sections s
|
|
1244
|
+
WHERE s.menu_id = m.id AND s.legacy_category_id = c.id
|
|
1245
|
+
)
|
|
1246
|
+
$sql$;
|
|
1247
|
+
END IF;
|
|
1248
|
+
|
|
1249
|
+
-- 3. Seat every product that already belongs to one of those categories.
|
|
1250
|
+
IF has_products AND has_product_category AND has_categories AND has_category_kind THEN
|
|
1251
|
+
EXECUTE $sql$
|
|
1252
|
+
INSERT INTO public.plg_menu_placements (tenant_id, section_id, item_id, sort_order)
|
|
1253
|
+
SELECT p.tenant_id, s.id, p.id,
|
|
1254
|
+
row_number() OVER (PARTITION BY s.id ORDER BY p.name)
|
|
1255
|
+
FROM public.products p
|
|
1256
|
+
JOIN public.plg_menu_sections s ON s.legacy_category_id = p.category_id AND s.tenant_id = p.tenant_id
|
|
1257
|
+
WHERE NOT EXISTS (
|
|
1258
|
+
SELECT 1 FROM public.plg_menu_placements pl
|
|
1259
|
+
WHERE pl.section_id = s.id AND pl.item_id = p.id
|
|
1260
|
+
)
|
|
1261
|
+
$sql$;
|
|
1262
|
+
END IF;
|
|
1263
|
+
|
|
1264
|
+
-- 4. Carry the menu-specific item flags over from the legacy extension.
|
|
1265
|
+
IF has_legacy_items THEN
|
|
1266
|
+
EXECUTE $sql$
|
|
1267
|
+
INSERT INTO public.plg_menu_items (
|
|
1268
|
+
product_id, tenant_id, status, available_for_pos, available_for_delivery,
|
|
1269
|
+
delivery_price, prep_time_minutes, allergens, dietary_tags, is_featured, sort_order
|
|
1270
|
+
)
|
|
1271
|
+
SELECT mi.product_id, mi.tenant_id,
|
|
1272
|
+
COALESCE(mi.status, 'available'),
|
|
1273
|
+
COALESCE(mi.available_for_pos, true),
|
|
1274
|
+
COALESCE(mi.available_for_delivery, true),
|
|
1275
|
+
mi.delivery_price, mi.prep_time_minutes,
|
|
1276
|
+
COALESCE(mi.allergens, ARRAY[]::text[]),
|
|
1277
|
+
COALESCE(mi.dietary_tags, ARRAY[]::text[]),
|
|
1278
|
+
COALESCE(mi.is_featured, false),
|
|
1279
|
+
COALESCE(mi.sort_order, 0)
|
|
1280
|
+
FROM public.menu_items mi
|
|
1281
|
+
WHERE mi.tenant_id IS NOT NULL
|
|
1282
|
+
ON CONFLICT (product_id) DO NOTHING
|
|
1283
|
+
$sql$;
|
|
1284
|
+
END IF;
|
|
1285
|
+
END $$;
|
|
1286
|
+
|
|
1287
|
+
-- Seed the allergen / dietary registries once per tenant that has a menu.
|
|
1288
|
+
INSERT INTO public.plg_menu_allergens (tenant_id, name, icon)
|
|
1289
|
+
SELECT m.tenant_id, v.name, v.icon
|
|
1290
|
+
FROM (SELECT DISTINCT tenant_id FROM public.plg_menu_menus) m
|
|
1291
|
+
CROSS JOIN (VALUES
|
|
1292
|
+
('Gl\xFAten', 'Wheat'), ('Lactose', 'Milk'), ('Ovos', 'Egg'),
|
|
1293
|
+
('Amendoim', 'Nut'), ('Frutos do mar', 'Fish'), ('Soja', 'Bean')
|
|
1294
|
+
) AS v(name, icon)
|
|
1295
|
+
WHERE NOT EXISTS (
|
|
1296
|
+
SELECT 1 FROM public.plg_menu_allergens a WHERE a.tenant_id = m.tenant_id AND a.name = v.name
|
|
1297
|
+
);
|
|
1298
|
+
|
|
1299
|
+
INSERT INTO public.plg_menu_dietary_tags (tenant_id, name, color)
|
|
1300
|
+
SELECT m.tenant_id, v.name, v.color
|
|
1301
|
+
FROM (SELECT DISTINCT tenant_id FROM public.plg_menu_menus) m
|
|
1302
|
+
CROSS JOIN (VALUES
|
|
1303
|
+
('Vegetariano', 'green'), ('Vegano', 'emerald'), ('Sem gl\xFAten', 'amber')
|
|
1304
|
+
) AS v(name, color)
|
|
1305
|
+
WHERE NOT EXISTS (
|
|
1306
|
+
SELECT 1 FROM public.plg_menu_dietary_tags d WHERE d.tenant_id = m.tenant_id AND d.name = v.name
|
|
1307
|
+
);
|
|
1308
|
+
`;
|
|
1309
|
+
var MIGRATION_003_MENU_STORAGE = `-- Menu Plugin: bucket for item, section and import photos.
|
|
1310
|
+
--
|
|
1311
|
+
-- Public on read: these images are the menu the customer looks at, and a signed
|
|
1312
|
+
-- URL per dish would mean re-signing the whole menu on every render. Writing is
|
|
1313
|
+
-- the guarded half \u2014 only a member of the tenant whose folder it is.
|
|
1314
|
+
-- Path grammar: {tenant_id}/{item|section|menu|import}/{uuid}.{ext}
|
|
1315
|
+
|
|
1316
|
+
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
|
1317
|
+
VALUES (
|
|
1318
|
+
'menu-images',
|
|
1319
|
+
'menu-images',
|
|
1320
|
+
true,
|
|
1321
|
+
8388608, -- 8 MB: a phone photo of a menu page, already compressed by the client
|
|
1322
|
+
ARRAY['image/jpeg','image/png','image/webp','image/heic','image/heif']
|
|
1323
|
+
)
|
|
1324
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
1325
|
+
public = EXCLUDED.public,
|
|
1326
|
+
file_size_limit = EXCLUDED.file_size_limit,
|
|
1327
|
+
allowed_mime_types = EXCLUDED.allowed_mime_types;
|
|
1328
|
+
|
|
1329
|
+
DROP POLICY IF EXISTS plg_menu_images_read ON storage.objects;
|
|
1330
|
+
DROP POLICY IF EXISTS plg_menu_images_insert ON storage.objects;
|
|
1331
|
+
DROP POLICY IF EXISTS plg_menu_images_update ON storage.objects;
|
|
1332
|
+
DROP POLICY IF EXISTS plg_menu_images_delete ON storage.objects;
|
|
1333
|
+
|
|
1334
|
+
CREATE POLICY plg_menu_images_read ON storage.objects
|
|
1335
|
+
FOR SELECT TO anon, authenticated
|
|
1336
|
+
USING (bucket_id = 'menu-images');
|
|
1337
|
+
|
|
1338
|
+
CREATE POLICY plg_menu_images_insert ON storage.objects
|
|
1339
|
+
FOR INSERT TO authenticated
|
|
1340
|
+
WITH CHECK (
|
|
1341
|
+
bucket_id = 'menu-images'
|
|
1342
|
+
AND (storage.foldername(name))[1] IN (SELECT public.user_tenant_ids()::text)
|
|
1343
|
+
);
|
|
1344
|
+
|
|
1345
|
+
CREATE POLICY plg_menu_images_update ON storage.objects
|
|
1346
|
+
FOR UPDATE TO authenticated
|
|
1347
|
+
USING (
|
|
1348
|
+
bucket_id = 'menu-images'
|
|
1349
|
+
AND (storage.foldername(name))[1] IN (SELECT public.user_tenant_ids()::text)
|
|
1350
|
+
);
|
|
1351
|
+
|
|
1352
|
+
CREATE POLICY plg_menu_images_delete ON storage.objects
|
|
1353
|
+
FOR DELETE TO authenticated
|
|
1354
|
+
USING (
|
|
1355
|
+
bucket_id = 'menu-images'
|
|
1356
|
+
AND (storage.foldername(name))[1] IN (SELECT public.user_tenant_ids()::text)
|
|
1357
|
+
);
|
|
1358
|
+
`;
|
|
1359
|
+
var MIGRATIONS = [
|
|
1360
|
+
{ id: "001_menu_base", sql: MIGRATION_001_MENU_BASE },
|
|
1361
|
+
{ id: "002_menu_backfill", sql: MIGRATION_002_MENU_BACKFILL },
|
|
1362
|
+
{ id: "003_menu_storage", sql: MIGRATION_003_MENU_STORAGE }
|
|
1363
|
+
];
|
|
1364
|
+
|
|
1365
|
+
// src/data/types.ts
|
|
1366
|
+
function supportsMenus(provider) {
|
|
1367
|
+
return typeof provider.getMenus === "function" && typeof provider.createSection === "function";
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// src/data/legacy-adapter.ts
|
|
1371
|
+
var LEGACY_MENU_ID = "__default__";
|
|
1372
|
+
var placementId = (itemId) => `legacy:${itemId}`;
|
|
1373
|
+
var itemIdOf = (placement) => placement.replace(/^legacy:/, "");
|
|
1374
|
+
function unsupported(what) {
|
|
1375
|
+
throw new Error(`[menu] This workspace's menu backend does not support ${what} yet.`);
|
|
1376
|
+
}
|
|
1377
|
+
function wrapLegacyProvider(provider) {
|
|
1378
|
+
async function allItems() {
|
|
1379
|
+
const result = await provider.getMenuItems({ pageSize: 1e3 });
|
|
1380
|
+
return result.data;
|
|
1381
|
+
}
|
|
1382
|
+
const legacyMenu = async () => {
|
|
1383
|
+
const [categories, items] = await Promise.all([provider.getCategories(), allItems()]);
|
|
1384
|
+
const first = items[0];
|
|
1385
|
+
return {
|
|
1386
|
+
id: LEGACY_MENU_ID,
|
|
1387
|
+
name: "Card\xE1pio principal",
|
|
1388
|
+
isActive: true,
|
|
1389
|
+
sortOrder: 0,
|
|
1390
|
+
channels: ["pos", "delivery", "digital"],
|
|
1391
|
+
availability: {},
|
|
1392
|
+
sectionCount: categories.length,
|
|
1393
|
+
itemCount: items.length,
|
|
1394
|
+
tenantId: first?.tenantId ?? "",
|
|
1395
|
+
createdAt: first?.createdAt ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1396
|
+
updatedAt: first?.updatedAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
|
|
1397
|
+
};
|
|
1398
|
+
};
|
|
1399
|
+
const overrides = {
|
|
1400
|
+
async getMenus() {
|
|
1401
|
+
return [await legacyMenu()];
|
|
1402
|
+
},
|
|
1403
|
+
async createMenu() {
|
|
1404
|
+
unsupported("multiple menus");
|
|
1405
|
+
},
|
|
1406
|
+
async updateMenu() {
|
|
1407
|
+
unsupported("multiple menus");
|
|
1408
|
+
},
|
|
1409
|
+
async deleteMenu() {
|
|
1410
|
+
unsupported("multiple menus");
|
|
1411
|
+
},
|
|
1412
|
+
async duplicateMenu() {
|
|
1413
|
+
unsupported("multiple menus");
|
|
1414
|
+
},
|
|
1415
|
+
async reorderMenus() {
|
|
1416
|
+
},
|
|
1417
|
+
async getSections() {
|
|
1418
|
+
const categories = await provider.getCategories();
|
|
1419
|
+
return categories.map((c) => ({
|
|
1420
|
+
id: c.id,
|
|
1421
|
+
menuId: LEGACY_MENU_ID,
|
|
1422
|
+
name: c.name,
|
|
1423
|
+
sortOrder: c.sortOrder,
|
|
1424
|
+
isActive: c.isActive,
|
|
1425
|
+
tenantId: c.tenantId,
|
|
1426
|
+
createdAt: c.createdAt,
|
|
1427
|
+
updatedAt: c.updatedAt
|
|
1428
|
+
}));
|
|
1429
|
+
},
|
|
1430
|
+
async createSection(input) {
|
|
1431
|
+
const category = await provider.createCategory({ name: input.name, sortOrder: input.sortOrder });
|
|
1432
|
+
return {
|
|
1433
|
+
id: category.id,
|
|
1434
|
+
menuId: LEGACY_MENU_ID,
|
|
1435
|
+
name: category.name,
|
|
1436
|
+
sortOrder: category.sortOrder,
|
|
1437
|
+
isActive: category.isActive,
|
|
1438
|
+
tenantId: category.tenantId,
|
|
1439
|
+
createdAt: category.createdAt,
|
|
1440
|
+
updatedAt: category.updatedAt
|
|
1441
|
+
};
|
|
1442
|
+
},
|
|
1443
|
+
async updateSection(id, data) {
|
|
1444
|
+
const category = await provider.updateCategory(id, {
|
|
1445
|
+
name: data.name,
|
|
1446
|
+
sortOrder: data.sortOrder,
|
|
1447
|
+
isActive: data.isActive
|
|
1448
|
+
});
|
|
1449
|
+
return {
|
|
1450
|
+
id: category.id,
|
|
1451
|
+
menuId: LEGACY_MENU_ID,
|
|
1452
|
+
name: category.name,
|
|
1453
|
+
sortOrder: category.sortOrder,
|
|
1454
|
+
isActive: category.isActive,
|
|
1455
|
+
tenantId: category.tenantId,
|
|
1456
|
+
createdAt: category.createdAt,
|
|
1457
|
+
updatedAt: category.updatedAt
|
|
1458
|
+
};
|
|
1459
|
+
},
|
|
1460
|
+
async deleteSection(id) {
|
|
1461
|
+
await provider.deleteCategory(id);
|
|
1462
|
+
},
|
|
1463
|
+
async reorderSections(_menuId, ids) {
|
|
1464
|
+
await provider.reorderCategories(ids);
|
|
1465
|
+
},
|
|
1466
|
+
async getPlacements() {
|
|
1467
|
+
const items = await allItems();
|
|
1468
|
+
return items.filter((item) => !!item.categoryId).map((item) => ({
|
|
1469
|
+
id: placementId(item.id),
|
|
1470
|
+
sectionId: item.categoryId,
|
|
1471
|
+
itemId: item.id,
|
|
1472
|
+
sortOrder: item.sortOrder ?? 0,
|
|
1473
|
+
isActive: item.status !== "hidden",
|
|
1474
|
+
tenantId: item.tenantId
|
|
1475
|
+
}));
|
|
1476
|
+
},
|
|
1477
|
+
async placeItem(input) {
|
|
1478
|
+
await provider.updateMenuItem(input.itemId, { categoryId: input.sectionId });
|
|
1479
|
+
return {
|
|
1480
|
+
id: placementId(input.itemId),
|
|
1481
|
+
sectionId: input.sectionId,
|
|
1482
|
+
itemId: input.itemId,
|
|
1483
|
+
sortOrder: input.sortOrder ?? 0,
|
|
1484
|
+
isActive: input.isActive ?? true,
|
|
1485
|
+
tenantId: ""
|
|
1486
|
+
};
|
|
1487
|
+
},
|
|
1488
|
+
async updatePlacement(id, data) {
|
|
1489
|
+
if (data.sectionId) await provider.updateMenuItem(itemIdOf(id), { categoryId: data.sectionId });
|
|
1490
|
+
return {
|
|
1491
|
+
id,
|
|
1492
|
+
sectionId: data.sectionId ?? "",
|
|
1493
|
+
itemId: itemIdOf(id),
|
|
1494
|
+
sortOrder: data.sortOrder ?? 0,
|
|
1495
|
+
isActive: data.isActive ?? true,
|
|
1496
|
+
tenantId: ""
|
|
1497
|
+
};
|
|
1498
|
+
},
|
|
1499
|
+
async removePlacement(id) {
|
|
1500
|
+
await provider.updateMenuItem(itemIdOf(id), { categoryId: void 0 });
|
|
1501
|
+
},
|
|
1502
|
+
async reorderPlacements(sectionId, placementIds) {
|
|
1503
|
+
await provider.reorderItems(sectionId, placementIds.map(itemIdOf));
|
|
1504
|
+
},
|
|
1505
|
+
async movePlacement(id, toSectionId) {
|
|
1506
|
+
await provider.updateMenuItem(itemIdOf(id), { categoryId: toSectionId });
|
|
1507
|
+
}
|
|
1508
|
+
};
|
|
1509
|
+
return new Proxy({}, {
|
|
1510
|
+
get: (_t, prop) => overrides[prop] ?? provider[prop]
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// src/store.ts
|
|
1515
|
+
var optimisticSeq = 0;
|
|
1516
|
+
function createMenuStore(rawProvider) {
|
|
1517
|
+
const multiMenu = supportsMenus(rawProvider);
|
|
1518
|
+
const provider = multiMenu ? rawProvider : wrapLegacyProvider(rawProvider);
|
|
1519
|
+
return createStore((set, get) => {
|
|
1520
|
+
async function writeItemField(id, field, value) {
|
|
1521
|
+
const previous = get().menuItems;
|
|
1522
|
+
set({ menuItems: previous.map((i) => i.id === id ? { ...i, [field]: value } : i) });
|
|
371
1523
|
try {
|
|
372
|
-
|
|
373
|
-
const categories = await provider.getCategories();
|
|
374
|
-
set({ categories });
|
|
375
|
-
toast.success("Category created");
|
|
376
|
-
return category;
|
|
1524
|
+
await provider.toggleAvailability(id, field, value);
|
|
377
1525
|
} catch (err) {
|
|
378
|
-
|
|
379
|
-
|
|
1526
|
+
set({ menuItems: previous });
|
|
1527
|
+
toast.error(tl("Could not update the item", "N\xE3o foi poss\xEDvel atualizar o item"), { description: err?.message });
|
|
380
1528
|
}
|
|
381
1529
|
}
|
|
382
|
-
|
|
1530
|
+
return {
|
|
1531
|
+
multiMenu,
|
|
1532
|
+
menus: [],
|
|
1533
|
+
menusLoading: false,
|
|
1534
|
+
activeMenuId: null,
|
|
1535
|
+
sections: [],
|
|
1536
|
+
placements: [],
|
|
1537
|
+
menuLoading: false,
|
|
1538
|
+
pendingIds: [],
|
|
1539
|
+
menuItems: [],
|
|
1540
|
+
menuItemsTotal: 0,
|
|
1541
|
+
menuItemsLoading: false,
|
|
1542
|
+
menuItemQuery: {},
|
|
1543
|
+
modifierGroups: [],
|
|
1544
|
+
modifiersLoading: false,
|
|
1545
|
+
summary: null,
|
|
1546
|
+
summaryLoading: false,
|
|
1547
|
+
// --- Reads -------------------------------------------------------------
|
|
1548
|
+
async fetchMenus() {
|
|
1549
|
+
set({ menusLoading: true });
|
|
1550
|
+
try {
|
|
1551
|
+
const menus = await dedup("menu:menus", () => provider.getMenus());
|
|
1552
|
+
const active = get().activeMenuId && menus.some((m) => m.id === get().activeMenuId) ? get().activeMenuId : menus[0]?.id ?? null;
|
|
1553
|
+
set({ menus, activeMenuId: active, menusLoading: false });
|
|
1554
|
+
if (active) await get().fetchMenuContent(active);
|
|
1555
|
+
} catch (err) {
|
|
1556
|
+
set({ menusLoading: false });
|
|
1557
|
+
toast.error(tl("Could not load the menus", "N\xE3o foi poss\xEDvel carregar os card\xE1pios"), { description: err?.message });
|
|
1558
|
+
}
|
|
1559
|
+
},
|
|
1560
|
+
async openMenu(menuId) {
|
|
1561
|
+
set({ activeMenuId: menuId });
|
|
1562
|
+
await get().fetchMenuContent(menuId);
|
|
1563
|
+
},
|
|
1564
|
+
async fetchMenuContent(menuId) {
|
|
1565
|
+
set({ menuLoading: true });
|
|
1566
|
+
try {
|
|
1567
|
+
const [sections, placements] = await Promise.all([
|
|
1568
|
+
provider.getSections(menuId),
|
|
1569
|
+
provider.getPlacements(menuId)
|
|
1570
|
+
]);
|
|
1571
|
+
set({ sections, placements, menuLoading: false });
|
|
1572
|
+
} catch (err) {
|
|
1573
|
+
set({ menuLoading: false });
|
|
1574
|
+
toast.error(tl("Could not load the menu", "N\xE3o foi poss\xEDvel carregar o card\xE1pio"), { description: err?.message });
|
|
1575
|
+
}
|
|
1576
|
+
},
|
|
1577
|
+
async fetchMenuItems(query = {}) {
|
|
1578
|
+
set({ menuItemsLoading: true, menuItemQuery: query });
|
|
1579
|
+
try {
|
|
1580
|
+
const result = await provider.getMenuItems(query);
|
|
1581
|
+
set({ menuItems: result.data, menuItemsTotal: result.total, menuItemsLoading: false });
|
|
1582
|
+
} catch (err) {
|
|
1583
|
+
set({ menuItemsLoading: false });
|
|
1584
|
+
toast.error(tl("Could not load the items", "N\xE3o foi poss\xEDvel carregar os itens"), { description: err?.message });
|
|
1585
|
+
}
|
|
1586
|
+
},
|
|
1587
|
+
async fetchModifierGroups() {
|
|
1588
|
+
set({ modifiersLoading: true });
|
|
1589
|
+
try {
|
|
1590
|
+
const modifierGroups = await dedup("menu:modifiers", () => provider.getModifierGroups());
|
|
1591
|
+
set({ modifierGroups, modifiersLoading: false });
|
|
1592
|
+
} catch {
|
|
1593
|
+
set({ modifiersLoading: false, modifierGroups: [] });
|
|
1594
|
+
}
|
|
1595
|
+
},
|
|
1596
|
+
async fetchSummary() {
|
|
1597
|
+
set({ summaryLoading: true });
|
|
1598
|
+
try {
|
|
1599
|
+
const summary = await dedup("menu:summary", () => provider.getSummary());
|
|
1600
|
+
set({ summary, summaryLoading: false });
|
|
1601
|
+
} catch {
|
|
1602
|
+
set({ summaryLoading: false });
|
|
1603
|
+
}
|
|
1604
|
+
},
|
|
1605
|
+
// --- Menus -------------------------------------------------------------
|
|
1606
|
+
async createMenu(input) {
|
|
1607
|
+
const menu = await provider.createMenu(input);
|
|
1608
|
+
set({ menus: [...get().menus, menu] });
|
|
1609
|
+
await get().openMenu(menu.id);
|
|
1610
|
+
toast.success(tl("Menu created", "Card\xE1pio criado"));
|
|
1611
|
+
return menu;
|
|
1612
|
+
},
|
|
1613
|
+
async updateMenu(id, data) {
|
|
1614
|
+
const updated = await provider.updateMenu(id, data);
|
|
1615
|
+
set({ menus: get().menus.map((m) => m.id === id ? { ...m, ...updated } : m) });
|
|
1616
|
+
},
|
|
1617
|
+
async deleteMenu(id) {
|
|
1618
|
+
await provider.deleteMenu(id);
|
|
1619
|
+
const menus = get().menus.filter((m) => m.id !== id);
|
|
1620
|
+
set({ menus });
|
|
1621
|
+
const next = menus[0]?.id ?? null;
|
|
1622
|
+
set({ activeMenuId: next, sections: [], placements: [] });
|
|
1623
|
+
if (next) await get().fetchMenuContent(next);
|
|
1624
|
+
toast.success(tl("Menu deleted", "Card\xE1pio exclu\xEDdo"));
|
|
1625
|
+
},
|
|
1626
|
+
async duplicateMenu(id, name) {
|
|
1627
|
+
const copy = await provider.duplicateMenu(id, name);
|
|
1628
|
+
await get().fetchMenus();
|
|
1629
|
+
await get().openMenu(copy.id);
|
|
1630
|
+
toast.success(tl("Menu duplicated", "Card\xE1pio duplicado"), { description: tl("The copy starts inactive \u2014 publish it when it is ready.", "A c\xF3pia come\xE7a desativada \u2014 publique quando estiver pronta.") });
|
|
1631
|
+
return copy;
|
|
1632
|
+
},
|
|
1633
|
+
// --- Sections ----------------------------------------------------------
|
|
1634
|
+
async createSection(name, extra) {
|
|
1635
|
+
const menuId = get().activeMenuId;
|
|
1636
|
+
if (!menuId) return null;
|
|
1637
|
+
const tempId = `pending-${++optimisticSeq}`;
|
|
1638
|
+
const optimistic = {
|
|
1639
|
+
id: tempId,
|
|
1640
|
+
menuId,
|
|
1641
|
+
name,
|
|
1642
|
+
sortOrder: extra?.sortOrder ?? get().sections.length,
|
|
1643
|
+
isActive: extra?.isActive ?? true,
|
|
1644
|
+
availability: extra?.availability ?? {},
|
|
1645
|
+
tenantId: "",
|
|
1646
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1647
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1648
|
+
};
|
|
1649
|
+
set({
|
|
1650
|
+
sections: [...get().sections, optimistic],
|
|
1651
|
+
pendingIds: [...get().pendingIds, tempId]
|
|
1652
|
+
});
|
|
1653
|
+
try {
|
|
1654
|
+
const section = await provider.createSection({ menuId, name, ...extra });
|
|
1655
|
+
set({
|
|
1656
|
+
sections: get().sections.map((s) => s.id === tempId ? section : s),
|
|
1657
|
+
pendingIds: get().pendingIds.filter((id) => id !== tempId)
|
|
1658
|
+
});
|
|
1659
|
+
return section;
|
|
1660
|
+
} catch (err) {
|
|
1661
|
+
set({
|
|
1662
|
+
sections: get().sections.filter((s) => s.id !== tempId),
|
|
1663
|
+
pendingIds: get().pendingIds.filter((id) => id !== tempId)
|
|
1664
|
+
});
|
|
1665
|
+
toast.error(tl("Could not create the section", "N\xE3o foi poss\xEDvel criar a se\xE7\xE3o"), { description: err?.message });
|
|
1666
|
+
return null;
|
|
1667
|
+
}
|
|
1668
|
+
},
|
|
1669
|
+
async updateSection(id, data) {
|
|
1670
|
+
const previous = get().sections;
|
|
1671
|
+
set({ sections: previous.map((s) => s.id === id ? { ...s, ...data } : s) });
|
|
1672
|
+
try {
|
|
1673
|
+
await provider.updateSection(id, data);
|
|
1674
|
+
} catch (err) {
|
|
1675
|
+
set({ sections: previous });
|
|
1676
|
+
toast.error(tl("Could not save the section", "N\xE3o foi poss\xEDvel salvar a se\xE7\xE3o"), { description: err?.message });
|
|
1677
|
+
}
|
|
1678
|
+
},
|
|
1679
|
+
async deleteSection(id) {
|
|
1680
|
+
try {
|
|
1681
|
+
await provider.deleteSection(id);
|
|
1682
|
+
set({
|
|
1683
|
+
sections: get().sections.filter((s) => s.id !== id),
|
|
1684
|
+
placements: get().placements.filter((p) => p.sectionId !== id)
|
|
1685
|
+
});
|
|
1686
|
+
toast.success(tl("Section removed", "Se\xE7\xE3o removida"));
|
|
1687
|
+
} catch (err) {
|
|
1688
|
+
toast.error(tl("Could not remove the section", "N\xE3o foi poss\xEDvel remover a se\xE7\xE3o"), { description: err?.message });
|
|
1689
|
+
}
|
|
1690
|
+
},
|
|
1691
|
+
async reorderSections(orderedIds) {
|
|
1692
|
+
const menuId = get().activeMenuId;
|
|
1693
|
+
if (!menuId) return;
|
|
1694
|
+
const previous = get().sections;
|
|
1695
|
+
set({
|
|
1696
|
+
sections: orderedIds.map((id, index) => {
|
|
1697
|
+
const section = previous.find((s) => s.id === id);
|
|
1698
|
+
return section ? { ...section, sortOrder: index } : null;
|
|
1699
|
+
}).filter(Boolean)
|
|
1700
|
+
});
|
|
1701
|
+
try {
|
|
1702
|
+
await provider.reorderSections(menuId, orderedIds);
|
|
1703
|
+
} catch (err) {
|
|
1704
|
+
set({ sections: previous });
|
|
1705
|
+
toast.error(tl("Could not save the new order", "N\xE3o foi poss\xEDvel salvar a ordem"), { description: err?.message });
|
|
1706
|
+
}
|
|
1707
|
+
},
|
|
1708
|
+
// --- Items in the open menu -------------------------------------------
|
|
1709
|
+
async addItemToSection(sectionId, input) {
|
|
1710
|
+
const tempId = `pending-${++optimisticSeq}`;
|
|
1711
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
1712
|
+
const optimisticItem = {
|
|
1713
|
+
id: tempId,
|
|
1714
|
+
name: input.name,
|
|
1715
|
+
description: input.description,
|
|
1716
|
+
price: input.price ?? 0,
|
|
1717
|
+
currency: "",
|
|
1718
|
+
imageUrl: input.imageUrl,
|
|
1719
|
+
status: input.status ?? "available",
|
|
1720
|
+
isActive: true,
|
|
1721
|
+
availableForPos: input.availableForPos ?? true,
|
|
1722
|
+
availableForDelivery: input.availableForDelivery ?? true,
|
|
1723
|
+
deliveryPrice: input.deliveryPrice,
|
|
1724
|
+
prepTimeMinutes: input.prepTimeMinutes,
|
|
1725
|
+
allergens: input.allergens ?? [],
|
|
1726
|
+
dietaryTags: input.dietaryTags ?? [],
|
|
1727
|
+
sortOrder: 0,
|
|
1728
|
+
isFeatured: input.isFeatured ?? false,
|
|
1729
|
+
tenantId: "",
|
|
1730
|
+
createdAt: now2,
|
|
1731
|
+
updatedAt: now2
|
|
1732
|
+
};
|
|
1733
|
+
const optimisticPlacement = {
|
|
1734
|
+
id: `pending-placement-${optimisticSeq}`,
|
|
1735
|
+
sectionId,
|
|
1736
|
+
itemId: tempId,
|
|
1737
|
+
sortOrder: get().placements.filter((p) => p.sectionId === sectionId).length,
|
|
1738
|
+
isActive: true,
|
|
1739
|
+
tenantId: ""
|
|
1740
|
+
};
|
|
1741
|
+
set({
|
|
1742
|
+
menuItems: [...get().menuItems, optimisticItem],
|
|
1743
|
+
placements: [...get().placements, optimisticPlacement],
|
|
1744
|
+
pendingIds: [...get().pendingIds, optimisticPlacement.id]
|
|
1745
|
+
});
|
|
1746
|
+
try {
|
|
1747
|
+
const item = await provider.createMenuItem({ ...input, sectionId });
|
|
1748
|
+
const menuId = get().activeMenuId;
|
|
1749
|
+
set({ menuItems: get().menuItems.map((i) => i.id === tempId ? item : i) });
|
|
1750
|
+
if (menuId) await get().fetchMenuContent(menuId);
|
|
1751
|
+
set({ pendingIds: get().pendingIds.filter((id) => id !== optimisticPlacement.id) });
|
|
1752
|
+
void get().fetchSummary();
|
|
1753
|
+
return item;
|
|
1754
|
+
} catch (err) {
|
|
1755
|
+
set({
|
|
1756
|
+
menuItems: get().menuItems.filter((i) => i.id !== tempId),
|
|
1757
|
+
placements: get().placements.filter((p) => p.id !== optimisticPlacement.id),
|
|
1758
|
+
pendingIds: get().pendingIds.filter((id) => id !== optimisticPlacement.id)
|
|
1759
|
+
});
|
|
1760
|
+
toast.error(tl("Could not create the item", "N\xE3o foi poss\xEDvel criar o item"), { description: err?.message });
|
|
1761
|
+
return null;
|
|
1762
|
+
}
|
|
1763
|
+
},
|
|
1764
|
+
async placeExistingItem(sectionId, itemId) {
|
|
1765
|
+
try {
|
|
1766
|
+
const placement = await provider.placeItem({ sectionId, itemId });
|
|
1767
|
+
set({ placements: [...get().placements.filter((p) => p.id !== placement.id), placement] });
|
|
1768
|
+
} catch (err) {
|
|
1769
|
+
toast.error(tl("Could not add it to the menu", "N\xE3o foi poss\xEDvel adicionar ao card\xE1pio"), { description: err?.message });
|
|
1770
|
+
}
|
|
1771
|
+
},
|
|
1772
|
+
async removePlacement(placementId2) {
|
|
1773
|
+
const previous = get().placements;
|
|
1774
|
+
set({ placements: previous.filter((p) => p.id !== placementId2) });
|
|
1775
|
+
try {
|
|
1776
|
+
await provider.removePlacement(placementId2);
|
|
1777
|
+
} catch (err) {
|
|
1778
|
+
set({ placements: previous });
|
|
1779
|
+
toast.error(tl("Could not remove it from the menu", "N\xE3o foi poss\xEDvel remover do card\xE1pio"), { description: err?.message });
|
|
1780
|
+
}
|
|
1781
|
+
},
|
|
1782
|
+
async setPlacementPrice(placementId2, price) {
|
|
1783
|
+
const previous = get().placements;
|
|
1784
|
+
set({ placements: previous.map((p) => p.id === placementId2 ? { ...p, priceOverride: price } : p) });
|
|
1785
|
+
try {
|
|
1786
|
+
await provider.updatePlacement(placementId2, { priceOverride: price });
|
|
1787
|
+
} catch (err) {
|
|
1788
|
+
set({ placements: previous });
|
|
1789
|
+
toast.error(tl("Could not save the price", "N\xE3o foi poss\xEDvel salvar o pre\xE7o"), { description: err?.message });
|
|
1790
|
+
}
|
|
1791
|
+
},
|
|
1792
|
+
async reorderPlacements(sectionId, orderedIds) {
|
|
1793
|
+
const previous = get().placements;
|
|
1794
|
+
const reordered = previous.map((p) => {
|
|
1795
|
+
const index = orderedIds.indexOf(p.id);
|
|
1796
|
+
return index >= 0 ? { ...p, sortOrder: index } : p;
|
|
1797
|
+
});
|
|
1798
|
+
set({ placements: reordered });
|
|
1799
|
+
try {
|
|
1800
|
+
await provider.reorderPlacements(sectionId, orderedIds);
|
|
1801
|
+
} catch (err) {
|
|
1802
|
+
set({ placements: previous });
|
|
1803
|
+
toast.error(tl("Could not save the new order", "N\xE3o foi poss\xEDvel salvar a ordem"), { description: err?.message });
|
|
1804
|
+
}
|
|
1805
|
+
},
|
|
1806
|
+
async movePlacement(placementId2, toSectionId, toIndex) {
|
|
1807
|
+
const previous = get().placements;
|
|
1808
|
+
set({
|
|
1809
|
+
placements: previous.map((p) => p.id === placementId2 ? { ...p, sectionId: toSectionId, sortOrder: toIndex } : p)
|
|
1810
|
+
});
|
|
1811
|
+
try {
|
|
1812
|
+
await provider.movePlacement(placementId2, toSectionId, toIndex);
|
|
1813
|
+
} catch (err) {
|
|
1814
|
+
set({ placements: previous });
|
|
1815
|
+
toast.error(tl("Could not move the item", "N\xE3o foi poss\xEDvel mover o item"), { description: err?.message });
|
|
1816
|
+
}
|
|
1817
|
+
},
|
|
1818
|
+
// --- Items -------------------------------------------------------------
|
|
1819
|
+
async updateMenuItem(id, data) {
|
|
1820
|
+
const previous = get().menuItems;
|
|
1821
|
+
set({ menuItems: previous.map((i) => i.id === id ? { ...i, ...data } : i) });
|
|
1822
|
+
try {
|
|
1823
|
+
const updated = await provider.updateMenuItem(id, data);
|
|
1824
|
+
set({ menuItems: get().menuItems.map((i) => i.id === id ? updated : i) });
|
|
1825
|
+
} catch (err) {
|
|
1826
|
+
set({ menuItems: previous });
|
|
1827
|
+
toast.error(tl("Could not save the item", "N\xE3o foi poss\xEDvel salvar o item"), { description: err?.message });
|
|
1828
|
+
}
|
|
1829
|
+
},
|
|
1830
|
+
async deleteMenuItem(id) {
|
|
1831
|
+
try {
|
|
1832
|
+
await provider.deleteMenuItem(id);
|
|
1833
|
+
set({
|
|
1834
|
+
menuItems: get().menuItems.filter((i) => i.id !== id),
|
|
1835
|
+
placements: get().placements.filter((p) => p.itemId !== id)
|
|
1836
|
+
});
|
|
1837
|
+
void get().fetchSummary();
|
|
1838
|
+
toast.success(tl("Item removed", "Item removido"));
|
|
1839
|
+
} catch (err) {
|
|
1840
|
+
toast.error(tl("Could not remove the item", "N\xE3o foi poss\xEDvel remover o item"), { description: err?.message });
|
|
1841
|
+
}
|
|
1842
|
+
},
|
|
1843
|
+
async duplicateMenuItem(id, sectionId) {
|
|
1844
|
+
const source = get().menuItems.find((i) => i.id === id);
|
|
1845
|
+
if (!source) return;
|
|
1846
|
+
await get().addItemToSection(sectionId ?? get().placements.find((p) => p.itemId === id)?.sectionId ?? "", {
|
|
1847
|
+
name: `${source.name} (c\xF3pia)`,
|
|
1848
|
+
description: source.description,
|
|
1849
|
+
price: source.price,
|
|
1850
|
+
cost: source.cost,
|
|
1851
|
+
imageUrl: source.imageUrl,
|
|
1852
|
+
prepTimeMinutes: source.prepTimeMinutes,
|
|
1853
|
+
allergens: source.allergens,
|
|
1854
|
+
dietaryTags: source.dietaryTags,
|
|
1855
|
+
availableForPos: source.availableForPos,
|
|
1856
|
+
availableForDelivery: source.availableForDelivery,
|
|
1857
|
+
deliveryPrice: source.deliveryPrice
|
|
1858
|
+
});
|
|
1859
|
+
},
|
|
1860
|
+
async setItemStatus(id, status) {
|
|
1861
|
+
await writeItemField(id, "status", status);
|
|
1862
|
+
},
|
|
1863
|
+
async toggleAvailability(id, field, value) {
|
|
1864
|
+
await writeItemField(id, field, value);
|
|
1865
|
+
},
|
|
1866
|
+
async uploadImage(file, kind) {
|
|
1867
|
+
if (!provider.uploadImage) {
|
|
1868
|
+
toast.error(tl("This workspace does not store menu photos yet.", "Este espa\xE7o ainda n\xE3o guarda fotos do card\xE1pio."));
|
|
1869
|
+
return null;
|
|
1870
|
+
}
|
|
1871
|
+
try {
|
|
1872
|
+
return await provider.uploadImage(file, kind);
|
|
1873
|
+
} catch (err) {
|
|
1874
|
+
toast.error("N\xE3o foi poss\xEDvel enviar a foto", { description: err?.message });
|
|
1875
|
+
return null;
|
|
1876
|
+
}
|
|
1877
|
+
},
|
|
1878
|
+
// --- AI import ---------------------------------------------------------
|
|
1879
|
+
extracting: false,
|
|
1880
|
+
draft: null,
|
|
1881
|
+
async runExtraction(images, extract) {
|
|
1882
|
+
if (images.length === 0) return;
|
|
1883
|
+
set({ extracting: true });
|
|
1884
|
+
try {
|
|
1885
|
+
const result = await extract(images);
|
|
1886
|
+
const counts = countExtracted(result);
|
|
1887
|
+
if (counts.items === 0) {
|
|
1888
|
+
set({ extracting: false });
|
|
1889
|
+
toast.error(tl("I could not read any items in those photos", "N\xE3o consegui ler itens nessas fotos"), {
|
|
1890
|
+
description: tl("Try one photo per page, closer and without glare.", "Tente uma foto por p\xE1gina, mais perto e sem reflexo.")
|
|
1891
|
+
});
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
const existing = get().sections;
|
|
1895
|
+
let key = 0;
|
|
1896
|
+
const draft = {
|
|
1897
|
+
source: "photo",
|
|
1898
|
+
model: result.model,
|
|
1899
|
+
sections: result.sections.map((section) => ({
|
|
1900
|
+
key: `ds-${++key}`,
|
|
1901
|
+
name: section.name,
|
|
1902
|
+
targetSectionId: existing.find((s) => normalizeName(s.name) === normalizeName(section.name))?.id,
|
|
1903
|
+
items: section.items.map((item) => ({
|
|
1904
|
+
key: `di-${++key}`,
|
|
1905
|
+
name: item.name,
|
|
1906
|
+
price: item.price,
|
|
1907
|
+
description: item.description
|
|
1908
|
+
}))
|
|
1909
|
+
}))
|
|
1910
|
+
};
|
|
1911
|
+
set({ draft, extracting: false });
|
|
1912
|
+
} catch (err) {
|
|
1913
|
+
set({ extracting: false });
|
|
1914
|
+
toast.error(tl("I could not read the photos", "N\xE3o consegui ler as fotos"), { description: err?.message });
|
|
1915
|
+
}
|
|
1916
|
+
},
|
|
1917
|
+
updateDraftItem(sectionKey, itemKey, patch) {
|
|
1918
|
+
const draft = get().draft;
|
|
1919
|
+
if (!draft) return;
|
|
1920
|
+
set({
|
|
1921
|
+
draft: {
|
|
1922
|
+
...draft,
|
|
1923
|
+
sections: draft.sections.map((section) => section.key !== sectionKey ? section : {
|
|
1924
|
+
...section,
|
|
1925
|
+
items: section.items.map((item) => item.key === itemKey ? { ...item, ...patch } : item)
|
|
1926
|
+
})
|
|
1927
|
+
}
|
|
1928
|
+
});
|
|
1929
|
+
},
|
|
1930
|
+
removeDraftItem(sectionKey, itemKey) {
|
|
1931
|
+
const draft = get().draft;
|
|
1932
|
+
if (!draft) return;
|
|
1933
|
+
const sections = draft.sections.map((section) => section.key !== sectionKey ? section : {
|
|
1934
|
+
...section,
|
|
1935
|
+
items: section.items.filter((item) => item.key !== itemKey)
|
|
1936
|
+
}).filter((section) => section.items.length > 0);
|
|
1937
|
+
set({ draft: sections.length > 0 ? { ...draft, sections } : null });
|
|
1938
|
+
},
|
|
1939
|
+
updateDraftSection(sectionKey, patch) {
|
|
1940
|
+
const draft = get().draft;
|
|
1941
|
+
if (!draft) return;
|
|
1942
|
+
set({
|
|
1943
|
+
draft: {
|
|
1944
|
+
...draft,
|
|
1945
|
+
sections: draft.sections.map((section) => section.key === sectionKey ? { ...section, ...patch } : section)
|
|
1946
|
+
}
|
|
1947
|
+
});
|
|
1948
|
+
},
|
|
1949
|
+
removeDraftSection(sectionKey) {
|
|
1950
|
+
const draft = get().draft;
|
|
1951
|
+
if (!draft) return;
|
|
1952
|
+
const sections = draft.sections.filter((section) => section.key !== sectionKey);
|
|
1953
|
+
set({ draft: sections.length > 0 ? { ...draft, sections } : null });
|
|
1954
|
+
},
|
|
1955
|
+
discardDraft() {
|
|
1956
|
+
set({ draft: null });
|
|
1957
|
+
},
|
|
1958
|
+
async commitDraft() {
|
|
1959
|
+
const draft = get().draft;
|
|
1960
|
+
const menuId = get().activeMenuId;
|
|
1961
|
+
if (!draft || !menuId) return { sectionsCreated: 0, itemsCreated: 0, failed: 0 };
|
|
1962
|
+
const result = await get().applyImportPlan({
|
|
1963
|
+
menuId,
|
|
1964
|
+
sections: draft.sections.map((section) => ({
|
|
1965
|
+
sectionId: section.targetSectionId,
|
|
1966
|
+
name: section.name,
|
|
1967
|
+
items: section.items.map(({ name, price, description }) => ({ name, price, description }))
|
|
1968
|
+
}))
|
|
1969
|
+
});
|
|
1970
|
+
set({ draft: null });
|
|
1971
|
+
return result;
|
|
1972
|
+
},
|
|
1973
|
+
async applyImportPlan(plan) {
|
|
1974
|
+
const result = { sectionsCreated: 0, itemsCreated: 0, failed: 0 };
|
|
1975
|
+
for (const section of plan.sections) {
|
|
1976
|
+
let sectionId = section.sectionId;
|
|
1977
|
+
if (!sectionId) {
|
|
1978
|
+
const created = await provider.createSection({ menuId: plan.menuId, name: section.name });
|
|
1979
|
+
sectionId = created.id;
|
|
1980
|
+
result.sectionsCreated++;
|
|
1981
|
+
}
|
|
1982
|
+
for (const item of section.items) {
|
|
1983
|
+
try {
|
|
1984
|
+
await provider.createMenuItem({
|
|
1985
|
+
name: item.name,
|
|
1986
|
+
description: item.description,
|
|
1987
|
+
price: item.price,
|
|
1988
|
+
sectionId
|
|
1989
|
+
});
|
|
1990
|
+
result.itemsCreated++;
|
|
1991
|
+
} catch {
|
|
1992
|
+
result.failed++;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
await get().fetchMenus();
|
|
1997
|
+
void get().fetchSummary();
|
|
1998
|
+
return result;
|
|
1999
|
+
}
|
|
2000
|
+
};
|
|
2001
|
+
});
|
|
383
2002
|
}
|
|
384
2003
|
|
|
385
2004
|
// src/data/registries.ts
|
|
@@ -394,7 +2013,7 @@ var allergenEntity = {
|
|
|
394
2013
|
{ key: "icon", label: "Icon", type: "text", showInTable: true },
|
|
395
2014
|
{ key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true }
|
|
396
2015
|
],
|
|
397
|
-
data: { table:
|
|
2016
|
+
data: { table: T.allergens, tenantScoped: true }
|
|
398
2017
|
};
|
|
399
2018
|
var dietaryTagEntity = {
|
|
400
2019
|
name: "Dietary Tag",
|
|
@@ -407,7 +2026,7 @@ var dietaryTagEntity = {
|
|
|
407
2026
|
{ key: "color", label: "Color", type: "text", showInTable: true },
|
|
408
2027
|
{ key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true }
|
|
409
2028
|
],
|
|
410
|
-
data: { table:
|
|
2029
|
+
data: { table: T.dietaryTags, tenantScoped: true }
|
|
411
2030
|
};
|
|
412
2031
|
var modifierGroupEntity = {
|
|
413
2032
|
name: "Modifier Group",
|
|
@@ -421,7 +2040,7 @@ var modifierGroupEntity = {
|
|
|
421
2040
|
{ key: "maxSelections", label: "Max Selections", type: "number", showInTable: true, defaultValue: 1 },
|
|
422
2041
|
{ key: "isRequired", label: "Required", type: "boolean", showInTable: true, defaultValue: false }
|
|
423
2042
|
],
|
|
424
|
-
data: { table:
|
|
2043
|
+
data: { table: T.modifierGroups, tenantScoped: true }
|
|
425
2044
|
};
|
|
426
2045
|
var menuRegistries = [
|
|
427
2046
|
{
|
|
@@ -466,132 +2085,282 @@ var menuRegistries = [
|
|
|
466
2085
|
var en = {
|
|
467
2086
|
"menu.title": "Menu",
|
|
468
2087
|
"menu.nav.manager": "Menu",
|
|
469
|
-
|
|
470
|
-
"menu.
|
|
471
|
-
"menu.
|
|
472
|
-
"menu.manager.
|
|
473
|
-
"menu.manager.
|
|
474
|
-
"menu.manager.
|
|
475
|
-
"menu.manager.
|
|
476
|
-
"menu.manager.
|
|
477
|
-
"menu.manager.
|
|
478
|
-
"menu.manager.
|
|
479
|
-
"menu.manager.
|
|
2088
|
+
// Manager
|
|
2089
|
+
"menu.manager.title": "Menu",
|
|
2090
|
+
"menu.manager.subtitle": "Build your menus, with sections and order",
|
|
2091
|
+
"menu.manager.search": "Search the menu...",
|
|
2092
|
+
"menu.manager.newMenu": "New menu",
|
|
2093
|
+
"menu.manager.menuActions": "Menu actions",
|
|
2094
|
+
"menu.manager.editMenu": "Edit menu",
|
|
2095
|
+
"menu.manager.duplicateMenu": "Duplicate menu",
|
|
2096
|
+
"menu.manager.activateMenu": "Activate menu",
|
|
2097
|
+
"menu.manager.deactivateMenu": "Deactivate menu",
|
|
2098
|
+
"menu.manager.deleteMenu": "Delete menu",
|
|
2099
|
+
"menu.manager.menuOff": "Off",
|
|
2100
|
+
"menu.manager.servingNow": "Serving now",
|
|
2101
|
+
"menu.manager.notServing": "Outside its hours",
|
|
2102
|
+
"menu.manager.addSection": "New section",
|
|
2103
|
+
"menu.manager.newSectionName": "New section",
|
|
2104
|
+
"menu.manager.editSection": "Turn section on/off",
|
|
2105
|
+
"menu.manager.deleteSection": "Delete section",
|
|
2106
|
+
"menu.manager.sectionActions": "Section actions",
|
|
2107
|
+
"menu.manager.sectionOff": "Section off",
|
|
2108
|
+
"menu.manager.emptySection": "Nothing here yet \u2014 click to add",
|
|
2109
|
+
"menu.manager.addItem": "Add item",
|
|
2110
|
+
"menu.manager.itemActions": "Item actions",
|
|
2111
|
+
"menu.manager.itemsCount": "{{count}} items",
|
|
2112
|
+
"menu.manager.edit": "Edit",
|
|
2113
|
+
"menu.manager.duplicate": "Duplicate",
|
|
2114
|
+
"menu.manager.markSoldOut": "Mark sold out",
|
|
2115
|
+
"menu.manager.markAvailable": "Mark available",
|
|
2116
|
+
"menu.manager.feature": "Feature",
|
|
2117
|
+
"menu.manager.unfeature": "Remove from featured",
|
|
2118
|
+
"menu.manager.removeFromMenu": "Remove from this menu",
|
|
2119
|
+
"menu.manager.deleteItem": "Delete item",
|
|
2120
|
+
"menu.manager.dragSection": "Drag section",
|
|
2121
|
+
"menu.manager.dragItem": "Drag item",
|
|
2122
|
+
"menu.manager.expand": "Expand",
|
|
2123
|
+
"menu.manager.collapse": "Collapse",
|
|
480
2124
|
"menu.manager.available": "Available",
|
|
481
|
-
"menu.manager.soldOut": "Sold
|
|
2125
|
+
"menu.manager.soldOut": "Sold out",
|
|
482
2126
|
"menu.manager.hidden": "Hidden",
|
|
483
2127
|
"menu.manager.pos": "POS",
|
|
484
2128
|
"menu.manager.delivery": "Delivery",
|
|
485
2129
|
"menu.manager.featured": "Featured",
|
|
486
|
-
"menu.manager.
|
|
487
|
-
"menu.manager.
|
|
488
|
-
"menu.manager.
|
|
489
|
-
"menu.manager.
|
|
490
|
-
|
|
491
|
-
"menu.
|
|
492
|
-
"menu.
|
|
493
|
-
"menu.
|
|
494
|
-
"menu.
|
|
495
|
-
"menu.
|
|
496
|
-
"menu.
|
|
497
|
-
"menu.
|
|
498
|
-
"menu.
|
|
2130
|
+
"menu.manager.overridden": "this menu",
|
|
2131
|
+
"menu.manager.overriddenHint": "This item costs a different price on this menu",
|
|
2132
|
+
"menu.manager.empty": "Your menu is still empty",
|
|
2133
|
+
"menu.manager.emptyHint": "Create the first section, or send photos of the printed menu and the assistant fills it in.",
|
|
2134
|
+
// Menu dialog
|
|
2135
|
+
"menu.menuDialog.newTitle": "New menu",
|
|
2136
|
+
"menu.menuDialog.editTitle": "Edit menu",
|
|
2137
|
+
"menu.menuDialog.name": "Name",
|
|
2138
|
+
"menu.menuDialog.namePlaceholder": "Lunch, Weekend, Delivery...",
|
|
2139
|
+
"menu.menuDialog.description": "Description",
|
|
2140
|
+
"menu.menuDialog.descriptionPlaceholder": "Weekday lunch",
|
|
2141
|
+
"menu.menuDialog.channels": "Where it shows",
|
|
2142
|
+
"menu.menuDialog.active": "Menu active",
|
|
2143
|
+
"menu.menuDialog.activeHint": "Turned off it leaves the floor and delivery without being deleted",
|
|
2144
|
+
"menu.channel.pos": "Dine-in / POS",
|
|
2145
|
+
"menu.channel.delivery": "Delivery",
|
|
2146
|
+
"menu.channel.digital": "Digital menu",
|
|
2147
|
+
// Schedule
|
|
2148
|
+
"menu.schedule.always": "Every day, all day",
|
|
2149
|
+
"menu.schedule.restrict": "Only on certain days and hours",
|
|
2150
|
+
"menu.schedule.restrictHint": "This is how a weekday lunch menu is born",
|
|
2151
|
+
"menu.schedule.days": "Days",
|
|
2152
|
+
"menu.schedule.from": "From",
|
|
2153
|
+
"menu.schedule.to": "To",
|
|
2154
|
+
"menu.schedule.overnightHint": "If the end is before the start, the menu crosses midnight (22:00\u201302:00).",
|
|
2155
|
+
"menu.schedule.presetWeekdays": "Weekdays",
|
|
2156
|
+
"menu.schedule.presetWeekend": "Weekend",
|
|
2157
|
+
"menu.schedule.presetEveryDay": "Every day",
|
|
2158
|
+
// Item form
|
|
2159
|
+
"menu.itemForm.newTitle": "New item",
|
|
2160
|
+
"menu.itemForm.editTitle": "Edit item",
|
|
2161
|
+
"menu.itemForm.name": "Name",
|
|
2162
|
+
"menu.itemForm.namePlaceholder": "Grilled picanha",
|
|
499
2163
|
"menu.itemForm.description": "Description",
|
|
2164
|
+
"menu.itemForm.descriptionPlaceholder": "Serves 2, comes with farofa",
|
|
2165
|
+
"menu.itemForm.photo": "Photo",
|
|
2166
|
+
"menu.itemForm.photoHint": "Square, well lit. Shows on the digital menu and delivery.",
|
|
2167
|
+
"menu.itemForm.pricingGroup": "Pricing",
|
|
2168
|
+
"menu.itemForm.menuGroup": "On the menu",
|
|
2169
|
+
"menu.itemForm.posHint": "Shows for whoever is serving the floor",
|
|
2170
|
+
"menu.itemForm.deliveryHint": "Shows for orders from outside",
|
|
2171
|
+
"menu.itemForm.featuredHint": "Moves to the top of its section",
|
|
500
2172
|
"menu.itemForm.price": "Price",
|
|
501
2173
|
"menu.itemForm.cost": "Cost",
|
|
502
|
-
"menu.itemForm.category": "Category",
|
|
503
|
-
"menu.itemForm.prepTime": "Prep time (min)",
|
|
504
|
-
"menu.itemForm.image": "Image URL",
|
|
505
|
-
"menu.itemForm.allergens": "Allergens",
|
|
506
|
-
"menu.itemForm.dietaryTags": "Dietary tags",
|
|
507
|
-
"menu.itemForm.availableForPos": "Available for POS",
|
|
508
|
-
"menu.itemForm.availableForDelivery": "Available for Delivery",
|
|
509
2174
|
"menu.itemForm.deliveryPrice": "Delivery price",
|
|
510
|
-
"menu.itemForm.
|
|
511
|
-
"menu.itemForm.
|
|
512
|
-
"menu.itemForm.
|
|
513
|
-
"menu.itemForm.
|
|
514
|
-
"menu.
|
|
515
|
-
"menu.
|
|
516
|
-
|
|
517
|
-
"menu.
|
|
518
|
-
"menu.
|
|
519
|
-
"menu.
|
|
520
|
-
"menu.
|
|
521
|
-
"menu.
|
|
522
|
-
"menu.
|
|
523
|
-
"menu.
|
|
524
|
-
"menu.
|
|
525
|
-
"menu.
|
|
526
|
-
"menu.
|
|
2175
|
+
"menu.itemForm.prepTime": "Prep (min)",
|
|
2176
|
+
"menu.itemForm.section": "Section",
|
|
2177
|
+
"menu.itemForm.status": "Status",
|
|
2178
|
+
"menu.itemForm.priceOverride": "Price on this menu only",
|
|
2179
|
+
"menu.itemForm.priceOverrideHint": "The same dish can cost less at lunch. Empty = the item price.",
|
|
2180
|
+
"menu.itemForm.clearOverride": "Use the item price",
|
|
2181
|
+
// AI import
|
|
2182
|
+
"menu.import.action": "Add with AI",
|
|
2183
|
+
"menu.import.uploadCta": "Upload a photo of the menu",
|
|
2184
|
+
"menu.import.uploadHint": "One photo per page, straight on, no glare. Several at once is fine.",
|
|
2185
|
+
"menu.import.askPrompt": "I want to add my menu from photos.",
|
|
2186
|
+
"menu.import.askHint": "Describe the menu, or ask to start from photos\u2026",
|
|
2187
|
+
"menu.import.askPersona": "Menu setup",
|
|
2188
|
+
"menu.import.askPitch": "Send me photos of the menu and I will type it in.",
|
|
2189
|
+
"menu.import.askDescription": "One photo per page, straight on, no glare. I read them, show what I understood right here on the screen, and we check it together before saving.",
|
|
2190
|
+
"menu.import.askSuggestion.0": "I will send photos of the printed menu",
|
|
2191
|
+
"menu.import.askSuggestion.1": "Create a weekday lunch menu",
|
|
2192
|
+
"menu.import.askSuggestion.2": "Add a desserts section",
|
|
2193
|
+
"menu.import.createSection": "Create new section",
|
|
2194
|
+
"menu.import.intoSection": "Add into: {{name}}",
|
|
2195
|
+
"menu.import.willAppend": "goes into {{name}}",
|
|
2196
|
+
"menu.import.dropItem": "Drop item",
|
|
2197
|
+
"menu.import.dropSection": "Drop section",
|
|
2198
|
+
"menu.import.draftCount": "{{items}} items read across {{sections}} sections",
|
|
2199
|
+
"menu.import.draftHint": "Check and fix them right here \u2014 nothing is saved yet.",
|
|
2200
|
+
"menu.import.draftMissingPrice": "{{count}} items came without a price. Fill them in before saving.",
|
|
2201
|
+
"menu.import.discard": "Discard",
|
|
2202
|
+
"menu.import.commit": "Save {{count}} items",
|
|
2203
|
+
"menu.import.done": "{{items}} items in {{sections}} sections created",
|
|
2204
|
+
"menu.import.someFailed": "{{count}} items could not be created",
|
|
2205
|
+
// Preview
|
|
2206
|
+
"menu.preview.action": "Preview",
|
|
2207
|
+
"menu.preview.title": "Menu",
|
|
2208
|
+
"menu.preview.notServingNow": "outside its hours right now",
|
|
2209
|
+
// Confirmations
|
|
2210
|
+
"menu.confirm.sectionTitle": "Delete the section {{name}}?",
|
|
2211
|
+
"menu.confirm.sectionDesc": "Its items leave this menu but stay registered.",
|
|
2212
|
+
"menu.confirm.itemTitle": "Delete the item {{name}}?",
|
|
2213
|
+
"menu.confirm.itemDesc": "It leaves every menu it appears in.",
|
|
2214
|
+
"menu.confirm.menuTitle": "Delete the menu {{name}}?",
|
|
2215
|
+
"menu.confirm.menuDesc": "Its sections go with it. The dishes stay registered.",
|
|
2216
|
+
"menu.confirm.delete": "Delete",
|
|
2217
|
+
// Shared form bits
|
|
527
2218
|
"menu.form.save": "Save",
|
|
528
|
-
"menu.form.
|
|
2219
|
+
"menu.form.saving": "Saving...",
|
|
2220
|
+
"menu.form.cancel": "Cancel",
|
|
2221
|
+
// Settings
|
|
2222
|
+
"menu.settings.title": "Menu settings"
|
|
529
2223
|
};
|
|
530
2224
|
|
|
531
2225
|
// src/locales/pt-BR.ts
|
|
532
2226
|
var ptBR = {
|
|
533
2227
|
"menu.title": "Card\xE1pio",
|
|
534
2228
|
"menu.nav.manager": "Card\xE1pio",
|
|
535
|
-
|
|
536
|
-
"menu.
|
|
537
|
-
"menu.
|
|
538
|
-
"menu.manager.
|
|
539
|
-
"menu.manager.
|
|
540
|
-
"menu.manager.
|
|
541
|
-
"menu.manager.
|
|
542
|
-
"menu.manager.
|
|
543
|
-
"menu.manager.
|
|
544
|
-
"menu.manager.
|
|
545
|
-
"menu.manager.
|
|
2229
|
+
// Manager
|
|
2230
|
+
"menu.manager.title": "Card\xE1pio",
|
|
2231
|
+
"menu.manager.subtitle": "Monte os card\xE1pios da casa, com se\xE7\xF5es e ordem",
|
|
2232
|
+
"menu.manager.search": "Buscar item no card\xE1pio...",
|
|
2233
|
+
"menu.manager.newMenu": "Novo card\xE1pio",
|
|
2234
|
+
"menu.manager.menuActions": "A\xE7\xF5es do card\xE1pio",
|
|
2235
|
+
"menu.manager.editMenu": "Editar card\xE1pio",
|
|
2236
|
+
"menu.manager.duplicateMenu": "Duplicar card\xE1pio",
|
|
2237
|
+
"menu.manager.activateMenu": "Ativar card\xE1pio",
|
|
2238
|
+
"menu.manager.deactivateMenu": "Desativar card\xE1pio",
|
|
2239
|
+
"menu.manager.deleteMenu": "Excluir card\xE1pio",
|
|
2240
|
+
"menu.manager.menuOff": "Desativado",
|
|
2241
|
+
"menu.manager.servingNow": "Servindo agora",
|
|
2242
|
+
"menu.manager.notServing": "Fora do hor\xE1rio",
|
|
2243
|
+
"menu.manager.addSection": "Nova se\xE7\xE3o",
|
|
2244
|
+
"menu.manager.newSectionName": "Nova se\xE7\xE3o",
|
|
2245
|
+
"menu.manager.editSection": "Ativar/desativar se\xE7\xE3o",
|
|
2246
|
+
"menu.manager.deleteSection": "Excluir se\xE7\xE3o",
|
|
2247
|
+
"menu.manager.sectionActions": "A\xE7\xF5es da se\xE7\xE3o",
|
|
2248
|
+
"menu.manager.sectionOff": "Se\xE7\xE3o desativada",
|
|
2249
|
+
"menu.manager.emptySection": "Nenhum item aqui ainda \u2014 clique para adicionar",
|
|
2250
|
+
"menu.manager.addItem": "Adicionar item",
|
|
2251
|
+
"menu.manager.itemActions": "A\xE7\xF5es do item",
|
|
2252
|
+
"menu.manager.itemsCount": "{{count}} itens",
|
|
2253
|
+
"menu.manager.edit": "Editar",
|
|
2254
|
+
"menu.manager.duplicate": "Duplicar",
|
|
2255
|
+
"menu.manager.markSoldOut": "Marcar como esgotado",
|
|
2256
|
+
"menu.manager.markAvailable": "Marcar como dispon\xEDvel",
|
|
2257
|
+
"menu.manager.feature": "Colocar em destaque",
|
|
2258
|
+
"menu.manager.unfeature": "Tirar do destaque",
|
|
2259
|
+
"menu.manager.removeFromMenu": "Tirar deste card\xE1pio",
|
|
2260
|
+
"menu.manager.deleteItem": "Excluir item",
|
|
2261
|
+
"menu.manager.dragSection": "Arrastar se\xE7\xE3o",
|
|
2262
|
+
"menu.manager.dragItem": "Arrastar item",
|
|
2263
|
+
"menu.manager.expand": "Expandir",
|
|
2264
|
+
"menu.manager.collapse": "Recolher",
|
|
546
2265
|
"menu.manager.available": "Dispon\xEDvel",
|
|
547
2266
|
"menu.manager.soldOut": "Esgotado",
|
|
548
2267
|
"menu.manager.hidden": "Oculto",
|
|
549
2268
|
"menu.manager.pos": "PDV",
|
|
550
2269
|
"menu.manager.delivery": "Delivery",
|
|
551
2270
|
"menu.manager.featured": "Destaque",
|
|
552
|
-
"menu.manager.
|
|
553
|
-
"menu.manager.
|
|
554
|
-
"menu.manager.
|
|
555
|
-
"menu.manager.
|
|
556
|
-
|
|
557
|
-
"menu.
|
|
558
|
-
"menu.
|
|
559
|
-
"menu.
|
|
560
|
-
"menu.
|
|
561
|
-
"menu.
|
|
562
|
-
"menu.
|
|
563
|
-
"menu.
|
|
564
|
-
"menu.
|
|
2271
|
+
"menu.manager.overridden": "pre\xE7o deste card\xE1pio",
|
|
2272
|
+
"menu.manager.overriddenHint": "Este item custa diferente neste card\xE1pio",
|
|
2273
|
+
"menu.manager.empty": "Seu card\xE1pio ainda est\xE1 vazio",
|
|
2274
|
+
"menu.manager.emptyHint": "Crie a primeira se\xE7\xE3o, ou mande fotos do card\xE1pio impresso e o assistente cadastra pra voc\xEA.",
|
|
2275
|
+
// Menu dialog
|
|
2276
|
+
"menu.menuDialog.newTitle": "Novo card\xE1pio",
|
|
2277
|
+
"menu.menuDialog.editTitle": "Editar card\xE1pio",
|
|
2278
|
+
"menu.menuDialog.name": "Nome",
|
|
2279
|
+
"menu.menuDialog.namePlaceholder": "Executivo, Fim de semana, Delivery...",
|
|
2280
|
+
"menu.menuDialog.description": "Descri\xE7\xE3o",
|
|
2281
|
+
"menu.menuDialog.descriptionPlaceholder": "Almo\xE7o de segunda a sexta",
|
|
2282
|
+
"menu.menuDialog.channels": "Onde aparece",
|
|
2283
|
+
"menu.menuDialog.active": "Card\xE1pio ativo",
|
|
2284
|
+
"menu.menuDialog.activeHint": "Desativado, ele some do sal\xE3o e do delivery sem ser apagado",
|
|
2285
|
+
"menu.channel.pos": "Sal\xE3o / PDV",
|
|
2286
|
+
"menu.channel.delivery": "Delivery",
|
|
2287
|
+
"menu.channel.digital": "Card\xE1pio digital",
|
|
2288
|
+
// Schedule
|
|
2289
|
+
"menu.schedule.always": "Todo dia, o dia inteiro",
|
|
2290
|
+
"menu.schedule.restrict": "S\xF3 em certos dias e hor\xE1rios",
|
|
2291
|
+
"menu.schedule.restrictHint": "\xC9 assim que nasce um executivo de segunda a sexta",
|
|
2292
|
+
"menu.schedule.days": "Dias",
|
|
2293
|
+
"menu.schedule.from": "Das",
|
|
2294
|
+
"menu.schedule.to": "At\xE9",
|
|
2295
|
+
"menu.schedule.overnightHint": "Se o fim for antes do come\xE7o, o card\xE1pio atravessa a meia-noite (22:00\u201302:00).",
|
|
2296
|
+
"menu.schedule.presetWeekdays": "Dias de semana",
|
|
2297
|
+
"menu.schedule.presetWeekend": "Fim de semana",
|
|
2298
|
+
"menu.schedule.presetEveryDay": "Todo dia",
|
|
2299
|
+
// Item form
|
|
2300
|
+
"menu.itemForm.newTitle": "Novo item",
|
|
2301
|
+
"menu.itemForm.editTitle": "Editar item",
|
|
2302
|
+
"menu.itemForm.name": "Nome",
|
|
2303
|
+
"menu.itemForm.namePlaceholder": "Picanha na chapa",
|
|
565
2304
|
"menu.itemForm.description": "Descri\xE7\xE3o",
|
|
2305
|
+
"menu.itemForm.descriptionPlaceholder": "Serve 2 pessoas, acompanha farofa",
|
|
2306
|
+
"menu.itemForm.photo": "Foto",
|
|
2307
|
+
"menu.itemForm.photoHint": "Quadrada, boa luz. Aparece no card\xE1pio digital e no delivery.",
|
|
2308
|
+
"menu.itemForm.pricingGroup": "Pre\xE7os",
|
|
2309
|
+
"menu.itemForm.menuGroup": "No card\xE1pio",
|
|
2310
|
+
"menu.itemForm.posHint": "Aparece para quem atende no sal\xE3o",
|
|
2311
|
+
"menu.itemForm.deliveryHint": "Aparece para quem pede de fora",
|
|
2312
|
+
"menu.itemForm.featuredHint": "Sobe para o topo da se\xE7\xE3o",
|
|
566
2313
|
"menu.itemForm.price": "Pre\xE7o",
|
|
567
2314
|
"menu.itemForm.cost": "Custo",
|
|
568
|
-
"menu.itemForm.category": "Categoria",
|
|
569
|
-
"menu.itemForm.prepTime": "Tempo de preparo (min)",
|
|
570
|
-
"menu.itemForm.image": "URL da imagem",
|
|
571
|
-
"menu.itemForm.allergens": "Al\xE9rgenos",
|
|
572
|
-
"menu.itemForm.dietaryTags": "Tags diet\xE9ticas",
|
|
573
|
-
"menu.itemForm.availableForPos": "Dispon\xEDvel no PDV",
|
|
574
|
-
"menu.itemForm.availableForDelivery": "Dispon\xEDvel para Delivery",
|
|
575
2315
|
"menu.itemForm.deliveryPrice": "Pre\xE7o delivery",
|
|
576
|
-
"menu.itemForm.
|
|
577
|
-
"menu.itemForm.
|
|
578
|
-
"menu.itemForm.
|
|
579
|
-
"menu.itemForm.
|
|
580
|
-
"menu.
|
|
581
|
-
"menu.
|
|
582
|
-
|
|
583
|
-
"menu.
|
|
584
|
-
"menu.
|
|
585
|
-
"menu.
|
|
586
|
-
"menu.
|
|
587
|
-
"menu.
|
|
588
|
-
"menu.
|
|
589
|
-
"menu.
|
|
590
|
-
"menu.
|
|
591
|
-
"menu.
|
|
592
|
-
"menu.
|
|
2316
|
+
"menu.itemForm.prepTime": "Preparo (min)",
|
|
2317
|
+
"menu.itemForm.section": "Se\xE7\xE3o",
|
|
2318
|
+
"menu.itemForm.status": "Situa\xE7\xE3o",
|
|
2319
|
+
"menu.itemForm.priceOverride": "Pre\xE7o s\xF3 neste card\xE1pio",
|
|
2320
|
+
"menu.itemForm.priceOverrideHint": "O mesmo prato pode custar menos no executivo. Em branco, vale o pre\xE7o do item.",
|
|
2321
|
+
"menu.itemForm.clearOverride": "Usar o pre\xE7o do item",
|
|
2322
|
+
// AI import
|
|
2323
|
+
"menu.import.action": "Inserir com IA",
|
|
2324
|
+
"menu.import.uploadCta": "Subir foto do card\xE1pio",
|
|
2325
|
+
"menu.import.uploadHint": "Uma foto por p\xE1gina, de frente e sem reflexo. Pode mandar v\xE1rias.",
|
|
2326
|
+
"menu.import.askPrompt": "Quero cadastrar meu card\xE1pio a partir de fotos.",
|
|
2327
|
+
"menu.import.askHint": "Descreva o card\xE1pio, ou pe\xE7a para come\xE7ar pelas fotos\u2026",
|
|
2328
|
+
"menu.import.askPersona": "Cadastro de card\xE1pio",
|
|
2329
|
+
"menu.import.askPitch": "Me manda as fotos do card\xE1pio que eu cadastro.",
|
|
2330
|
+
"menu.import.askDescription": "Uma foto por p\xE1gina, de frente e sem reflexo. Eu leio, mostro o que entendi aqui na tela e a gente confere junto antes de gravar.",
|
|
2331
|
+
"menu.import.askSuggestion.0": "Vou mandar as fotos do card\xE1pio impresso",
|
|
2332
|
+
"menu.import.askSuggestion.1": "Cria um card\xE1pio executivo de segunda a sexta",
|
|
2333
|
+
"menu.import.askSuggestion.2": "Adiciona uma se\xE7\xE3o de sobremesas",
|
|
2334
|
+
"menu.import.createSection": "Criar se\xE7\xE3o nova",
|
|
2335
|
+
"menu.import.intoSection": "Somar em: {{name}}",
|
|
2336
|
+
"menu.import.willAppend": "entra em {{name}}",
|
|
2337
|
+
"menu.import.dropItem": "Descartar item",
|
|
2338
|
+
"menu.import.dropSection": "Descartar se\xE7\xE3o",
|
|
2339
|
+
"menu.import.draftCount": "{{items}} itens lidos em {{sections}} se\xE7\xF5es",
|
|
2340
|
+
"menu.import.draftHint": "Confira e ajuste aqui mesmo \u2014 nada foi gravado ainda.",
|
|
2341
|
+
"menu.import.draftMissingPrice": "{{count}} itens vieram sem pre\xE7o. Preencha antes de gravar.",
|
|
2342
|
+
"menu.import.discard": "Descartar",
|
|
2343
|
+
"menu.import.commit": "Gravar {{count}} itens",
|
|
2344
|
+
"menu.import.done": "{{items}} itens em {{sections}} se\xE7\xF5es cadastrados",
|
|
2345
|
+
"menu.import.someFailed": "{{count}} itens n\xE3o puderam ser criados",
|
|
2346
|
+
// Preview
|
|
2347
|
+
"menu.preview.action": "Ver como fica",
|
|
2348
|
+
"menu.preview.title": "Card\xE1pio",
|
|
2349
|
+
"menu.preview.notServingNow": "fora do hor\xE1rio agora",
|
|
2350
|
+
// Confirmations
|
|
2351
|
+
"menu.confirm.sectionTitle": "Excluir a se\xE7\xE3o {{name}}?",
|
|
2352
|
+
"menu.confirm.sectionDesc": "Os itens dela saem deste card\xE1pio, mas continuam cadastrados.",
|
|
2353
|
+
"menu.confirm.itemTitle": "Excluir o item {{name}}?",
|
|
2354
|
+
"menu.confirm.itemDesc": "Ele sai de todos os card\xE1pios em que aparece.",
|
|
2355
|
+
"menu.confirm.menuTitle": "Excluir o card\xE1pio {{name}}?",
|
|
2356
|
+
"menu.confirm.menuDesc": "As se\xE7\xF5es deste card\xE1pio somem. Os pratos continuam cadastrados.",
|
|
2357
|
+
"menu.confirm.delete": "Excluir",
|
|
2358
|
+
// Shared form bits
|
|
593
2359
|
"menu.form.save": "Salvar",
|
|
594
|
-
"menu.form.
|
|
2360
|
+
"menu.form.saving": "Salvando...",
|
|
2361
|
+
"menu.form.cancel": "Cancelar",
|
|
2362
|
+
// Settings
|
|
2363
|
+
"menu.settings.title": "Configura\xE7\xF5es do Card\xE1pio"
|
|
595
2364
|
};
|
|
596
2365
|
|
|
597
2366
|
// src/locales/index.ts
|
|
@@ -613,7 +2382,7 @@ function menuMetadata(row) {
|
|
|
613
2382
|
const raw = row.metadata?.[MENU_METADATA_KEY];
|
|
614
2383
|
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
615
2384
|
}
|
|
616
|
-
function
|
|
2385
|
+
function rowToCategory2(row, itemCount = 0) {
|
|
617
2386
|
return {
|
|
618
2387
|
id: row.id,
|
|
619
2388
|
name: row.name,
|
|
@@ -628,7 +2397,7 @@ function rowToCategory(row, itemCount = 0) {
|
|
|
628
2397
|
updatedAt: row.updated_at ?? row.created_at ?? nowIso()
|
|
629
2398
|
};
|
|
630
2399
|
}
|
|
631
|
-
function
|
|
2400
|
+
function rowToItem2(row, categoryName) {
|
|
632
2401
|
const metadata = menuMetadata(row);
|
|
633
2402
|
const status = metadata.status ?? row.status ?? "available";
|
|
634
2403
|
return {
|
|
@@ -782,7 +2551,7 @@ function createFayzMenuProvider(options = {}) {
|
|
|
782
2551
|
}
|
|
783
2552
|
async function getItems() {
|
|
784
2553
|
const [rows, categoryMap] = await Promise.all([getItemsRaw(), getCategoryMap()]);
|
|
785
|
-
return rows.map((row) =>
|
|
2554
|
+
return rows.map((row) => rowToItem2(row, row.category_id ? categoryMap.get(row.category_id)?.name : void 0)).sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name));
|
|
786
2555
|
}
|
|
787
2556
|
return {
|
|
788
2557
|
async getCategories() {
|
|
@@ -791,7 +2560,7 @@ function createFayzMenuProvider(options = {}) {
|
|
|
791
2560
|
for (const item of items) {
|
|
792
2561
|
if (item.category_id) counts.set(item.category_id, (counts.get(item.category_id) ?? 0) + 1);
|
|
793
2562
|
}
|
|
794
|
-
return categories.map((category) =>
|
|
2563
|
+
return categories.map((category) => rowToCategory2(category, counts.get(category.id) ?? 0));
|
|
795
2564
|
},
|
|
796
2565
|
async getCategoryById(id) {
|
|
797
2566
|
const response = await client.data.listRows({
|
|
@@ -800,7 +2569,7 @@ function createFayzMenuProvider(options = {}) {
|
|
|
800
2569
|
filters: [{ column: "id", operator: "eq", value: id }],
|
|
801
2570
|
limit: 1
|
|
802
2571
|
});
|
|
803
|
-
return response.rows[0] ?
|
|
2572
|
+
return response.rows[0] ? rowToCategory2(response.rows[0]) : null;
|
|
804
2573
|
},
|
|
805
2574
|
async createCategory(input) {
|
|
806
2575
|
const row = await client.data.createRow({
|
|
@@ -808,7 +2577,7 @@ function createFayzMenuProvider(options = {}) {
|
|
|
808
2577
|
table: categoriesTable,
|
|
809
2578
|
row: categoryPayload(input, categoryKind)
|
|
810
2579
|
});
|
|
811
|
-
return
|
|
2580
|
+
return rowToCategory2(row);
|
|
812
2581
|
},
|
|
813
2582
|
async updateCategory(id, data) {
|
|
814
2583
|
const row = await client.data.updateRow({
|
|
@@ -824,7 +2593,7 @@ function createFayzMenuProvider(options = {}) {
|
|
|
824
2593
|
is_active: data.isActive
|
|
825
2594
|
}
|
|
826
2595
|
});
|
|
827
|
-
return
|
|
2596
|
+
return rowToCategory2(row);
|
|
828
2597
|
},
|
|
829
2598
|
async deleteCategory(id) {
|
|
830
2599
|
await client.data.updateRow({
|
|
@@ -851,7 +2620,7 @@ function createFayzMenuProvider(options = {}) {
|
|
|
851
2620
|
if (!response.rows[0]) return null;
|
|
852
2621
|
const categoryMap = await getCategoryMap();
|
|
853
2622
|
const row = response.rows[0];
|
|
854
|
-
return
|
|
2623
|
+
return rowToItem2(row, row.category_id ? categoryMap.get(row.category_id)?.name : void 0);
|
|
855
2624
|
},
|
|
856
2625
|
async createMenuItem(input) {
|
|
857
2626
|
const row = await client.data.createRow({
|
|
@@ -860,7 +2629,7 @@ function createFayzMenuProvider(options = {}) {
|
|
|
860
2629
|
row: itemCreatePayload(input)
|
|
861
2630
|
});
|
|
862
2631
|
const categoryMap = await getCategoryMap();
|
|
863
|
-
return
|
|
2632
|
+
return rowToItem2(row, row.category_id ? categoryMap.get(row.category_id)?.name : void 0);
|
|
864
2633
|
},
|
|
865
2634
|
async updateMenuItem(id, data) {
|
|
866
2635
|
const existing = await this.getMenuItemById(id);
|
|
@@ -871,7 +2640,7 @@ function createFayzMenuProvider(options = {}) {
|
|
|
871
2640
|
row: itemUpdatePayload(data, existing ?? void 0)
|
|
872
2641
|
});
|
|
873
2642
|
const categoryMap = await getCategoryMap();
|
|
874
|
-
return
|
|
2643
|
+
return rowToItem2(row, row.category_id ? categoryMap.get(row.category_id)?.name : void 0);
|
|
875
2644
|
},
|
|
876
2645
|
async deleteMenuItem(id) {
|
|
877
2646
|
await client.data.updateRow({
|
|
@@ -924,7 +2693,7 @@ function createFayzMenuProvider(options = {}) {
|
|
|
924
2693
|
}
|
|
925
2694
|
|
|
926
2695
|
// src/index.ts
|
|
927
|
-
var MenuPage = React.lazy(() => import('./MenuPage-
|
|
2696
|
+
var MenuPage = React.lazy(() => import('./MenuPage-2LLWRJGN.js').then((m) => ({ default: m.MenuPage })));
|
|
928
2697
|
var DEFAULT_LABELS = {
|
|
929
2698
|
pageTitle: "Menu",
|
|
930
2699
|
menuManager: "Menu Manager",
|
|
@@ -945,7 +2714,10 @@ function resolveConfig(options) {
|
|
|
945
2714
|
function createMenuPlugin(options) {
|
|
946
2715
|
const config = resolveConfig(options);
|
|
947
2716
|
registerTranslations(menuLocales);
|
|
948
|
-
const provider = options?.dataProvider ??
|
|
2717
|
+
const provider = options?.dataProvider ?? createSafeDataProvider(
|
|
2718
|
+
() => createSupabaseMenuProvider({ currencyCode: config.currency.code }),
|
|
2719
|
+
() => createMockMenuProvider()
|
|
2720
|
+
);
|
|
949
2721
|
const store = createMenuStore(provider);
|
|
950
2722
|
const PageComponent = () => React.createElement(
|
|
951
2723
|
React.Suspense,
|
|
@@ -954,6 +2726,7 @@ function createMenuPlugin(options) {
|
|
|
954
2726
|
);
|
|
955
2727
|
return {
|
|
956
2728
|
id: "menu",
|
|
2729
|
+
defaultAgentRole: "operations",
|
|
957
2730
|
name: config.labels.pageTitle,
|
|
958
2731
|
icon: "UtensilsCrossed",
|
|
959
2732
|
version: "1.0.0",
|
|
@@ -965,9 +2738,18 @@ function createMenuPlugin(options) {
|
|
|
965
2738
|
scaffolds: ["saas", "ecommerce"],
|
|
966
2739
|
defaultEnabled: true,
|
|
967
2740
|
dependencies: [],
|
|
2741
|
+
declaredFeatures: [
|
|
2742
|
+
{ id: "menu", label: config.labels.pageTitle, group: config.labels.pageTitle }
|
|
2743
|
+
],
|
|
968
2744
|
declaredLimits: [
|
|
969
|
-
{ key: "menu_items", label: "Menu items", table:
|
|
2745
|
+
{ key: "menu_items", label: "Menu items", table: T.items }
|
|
970
2746
|
],
|
|
2747
|
+
migrations: MIGRATIONS.map((migration) => ({
|
|
2748
|
+
id: `menu-${migration.id}`,
|
|
2749
|
+
version: "1.0.0",
|
|
2750
|
+
sql: migration.sql,
|
|
2751
|
+
description: `plugin-menu ${migration.id}`
|
|
2752
|
+
})),
|
|
971
2753
|
navigation: [
|
|
972
2754
|
{
|
|
973
2755
|
section: options?.navSection ?? "main",
|
|
@@ -1006,6 +2788,19 @@ function createMenuPlugin(options) {
|
|
|
1006
2788
|
{ label: "Which items are sold out right now?" }
|
|
1007
2789
|
]
|
|
1008
2790
|
},
|
|
2791
|
+
{
|
|
2792
|
+
id: "menu.list-menus",
|
|
2793
|
+
name: "listMenus",
|
|
2794
|
+
description: "Lists the menus of the place (regular, executive lunch, weekend) with when each one is served.",
|
|
2795
|
+
icon: "UtensilsCrossed",
|
|
2796
|
+
mode: "read",
|
|
2797
|
+
category: "Menu",
|
|
2798
|
+
parameters: { type: "object", properties: {} },
|
|
2799
|
+
suggestions: [
|
|
2800
|
+
{ label: "Quais card\xE1pios existem?" },
|
|
2801
|
+
{ label: "Que card\xE1pio est\xE1 no ar agora?" }
|
|
2802
|
+
]
|
|
2803
|
+
},
|
|
1009
2804
|
{
|
|
1010
2805
|
id: "menu.toggle-availability",
|
|
1011
2806
|
name: "toggleMenuItemAvailability",
|
|
@@ -1033,7 +2828,6 @@ function createMenuPlugin(options) {
|
|
|
1033
2828
|
component: (() => {
|
|
1034
2829
|
const MenuSettingsTab = () => React.createElement(PluginSettingsPanel, {
|
|
1035
2830
|
title: "Menu Settings",
|
|
1036
|
-
subtitle: "Categories, allergens, and modifiers",
|
|
1037
2831
|
registries: menuRegistries,
|
|
1038
2832
|
routeBase: "/settings/menu"
|
|
1039
2833
|
});
|