@fayz-ai/plugin-menu 0.9.4 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/MenuPage-2LLWRJGN.js +1785 -0
  2. package/dist/MenuPage-2LLWRJGN.js.map +1 -0
  3. package/dist/chunk-C6W53TML.js +65 -0
  4. package/dist/chunk-C6W53TML.js.map +1 -0
  5. package/dist/components/DraftSectionCard.d.ts +18 -0
  6. package/dist/components/DraftSectionCard.d.ts.map +1 -0
  7. package/dist/components/ItemRailPage.d.ts +36 -0
  8. package/dist/components/ItemRailPage.d.ts.map +1 -0
  9. package/dist/components/MenuPreview.d.ts +17 -0
  10. package/dist/components/MenuPreview.d.ts.map +1 -0
  11. package/dist/components/MenuRailPage.d.ts +17 -0
  12. package/dist/components/MenuRailPage.d.ts.map +1 -0
  13. package/dist/components/ScheduleEditor.d.ts +8 -0
  14. package/dist/components/ScheduleEditor.d.ts.map +1 -0
  15. package/dist/components/SectionCard.d.ts +32 -0
  16. package/dist/components/SectionCard.d.ts.map +1 -0
  17. package/dist/data/legacy-adapter.d.ts +4 -0
  18. package/dist/data/legacy-adapter.d.ts.map +1 -0
  19. package/dist/data/mock.d.ts +2 -2
  20. package/dist/data/mock.d.ts.map +1 -1
  21. package/dist/data/registries.d.ts.map +1 -1
  22. package/dist/data/supabase.d.ts +7 -0
  23. package/dist/data/supabase.d.ts.map +1 -0
  24. package/dist/data/tables.d.ts +17 -0
  25. package/dist/data/tables.d.ts.map +1 -0
  26. package/dist/data/types.d.ts +36 -2
  27. package/dist/data/types.d.ts.map +1 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2174 -356
  30. package/dist/index.js.map +1 -1
  31. package/dist/lib/agent-surface.d.ts +46 -0
  32. package/dist/lib/agent-surface.d.ts.map +1 -0
  33. package/dist/lib/availability.d.ts +16 -0
  34. package/dist/lib/availability.d.ts.map +1 -0
  35. package/dist/lib/extract-parse.d.ts +16 -0
  36. package/dist/lib/extract-parse.d.ts.map +1 -0
  37. package/dist/lib/extract-transport.d.ts +10 -0
  38. package/dist/lib/extract-transport.d.ts.map +1 -0
  39. package/dist/lib/images.d.ts +7 -0
  40. package/dist/lib/images.d.ts.map +1 -0
  41. package/dist/locales/en.d.ts.map +1 -1
  42. package/dist/locales/pt-BR.d.ts.map +1 -1
  43. package/dist/migrations/index.d.ts +9 -0
  44. package/dist/migrations/index.d.ts.map +1 -0
  45. package/dist/store.d.ts +46 -10
  46. package/dist/store.d.ts.map +1 -1
  47. package/dist/types.d.ts +151 -2
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/views/MenuManagerView.d.ts.map +1 -1
  50. package/package.json +14 -7
  51. package/dist/MenuPage-ZNKKQJKD.js +0 -523
  52. 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 ?? 50;
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
- function seedCategories() {
25
- const t = "mock-tenant";
23
+ var TENANT = "mock-tenant";
24
+ function createMockMenuProvider() {
26
25
  const ts = now();
27
- return [
28
- { id: nextId("cat"), name: "Burgers", icon: "\u{1F354}", color: "#f59e0b", sortOrder: 0, isActive: true, itemCount: 3, tenantId: t, createdAt: ts, updatedAt: ts },
29
- { id: nextId("cat"), name: "Sides", icon: "\u{1F35F}", color: "#22c55e", sortOrder: 1, isActive: true, itemCount: 3, tenantId: t, createdAt: ts, updatedAt: ts },
30
- { id: nextId("cat"), name: "Drinks", icon: "\u{1F964}", color: "#3b82f6", sortOrder: 2, isActive: true, itemCount: 4, tenantId: t, createdAt: ts, updatedAt: ts },
31
- { id: nextId("cat"), name: "Desserts", icon: "\u{1F370}", color: "#ec4899", sortOrder: 3, isActive: true, itemCount: 2, tenantId: t, createdAt: ts, updatedAt: ts },
32
- { id: nextId("cat"), name: "Combos", icon: "\u2B50", color: "#8b5cf6", sortOrder: 4, isActive: true, itemCount: 0, tenantId: t, createdAt: ts, updatedAt: ts }
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
- function seedItems(categories) {
36
- const t = "mock-tenant";
37
- const ts = now();
38
- const [burgers, sides, drinks, desserts] = categories.map((c) => c.id);
39
- const item = (name, price, categoryId, categoryName, opts = {}) => ({
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
- tenantId: t,
82
+ allergens: [],
83
+ dietaryTags: [],
84
+ tenantId: TENANT,
53
85
  createdAt: ts,
54
86
  updatedAt: ts,
55
- ...opts
87
+ ...extra
56
88
  });
57
- let order = 0;
58
- return [
59
- item("Classic Burger", 32, burgers, "Burgers", { prepTimeMinutes: 12, sortOrder: order++, isFeatured: true }),
60
- item("Cheese Burger", 36, burgers, "Burgers", { prepTimeMinutes: 12, sortOrder: order++ }),
61
- item("BBQ Bacon", 42, burgers, "Burgers", { prepTimeMinutes: 15, sortOrder: order++, isFeatured: true }),
62
- item("Fries", 15, sides, "Sides", { prepTimeMinutes: 8, sortOrder: order++ }),
63
- item("Onion Rings", 18, sides, "Sides", { prepTimeMinutes: 8, sortOrder: order++ }),
64
- item("Coleslaw", 12, sides, "Sides", { prepTimeMinutes: 2, sortOrder: order++, availableForDelivery: false }),
65
- item("Soda", 8, drinks, "Drinks", { prepTimeMinutes: 1, sortOrder: order++ }),
66
- item("Juice", 12, drinks, "Drinks", { prepTimeMinutes: 3, sortOrder: order++ }),
67
- item("Water", 5, drinks, "Drinks", { prepTimeMinutes: 0, sortOrder: order++ }),
68
- item("Milkshake", 16, drinks, "Drinks", { prepTimeMinutes: 5, sortOrder: order++, isFeatured: true }),
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
- function seedModifierGroups() {
74
- const t = "mock-tenant";
75
- const ts = now();
76
- const g1Id = nextId("mg");
77
- const g2Id = nextId("mg");
78
- return [
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: g1Id,
81
- name: "Extra Toppings",
82
- minSelections: 0,
83
- maxSelections: 3,
84
- isRequired: false,
132
+ id: g1,
133
+ name: "Ponto da carne",
134
+ minSelections: 1,
135
+ maxSelections: 1,
136
+ isRequired: true,
85
137
  sortOrder: 0,
86
- tenantId: t,
138
+ tenantId: TENANT,
87
139
  createdAt: ts,
88
140
  items: [
89
- { id: nextId("mod"), groupId: g1Id, name: "Extra Cheese", priceAdjustment: 5, isActive: true, sortOrder: 0 },
90
- { id: nextId("mod"), groupId: g1Id, name: "Bacon", priceAdjustment: 7, isActive: true, sortOrder: 1 },
91
- { id: nextId("mod"), groupId: g1Id, name: "Jalape\xF1os", priceAdjustment: 4, isActive: true, sortOrder: 2 }
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: g2Id,
96
- name: "Drink Size",
97
- minSelections: 1,
98
- maxSelections: 1,
99
- isRequired: true,
147
+ id: g2,
148
+ name: "Acompanhamentos",
149
+ minSelections: 0,
150
+ maxSelections: 3,
151
+ isRequired: false,
100
152
  sortOrder: 1,
101
- tenantId: t,
153
+ tenantId: TENANT,
102
154
  createdAt: ts,
103
155
  items: [
104
- { id: nextId("mod"), groupId: g2Id, name: "Small", priceAdjustment: 0, isActive: true, sortOrder: 0 },
105
- { id: nextId("mod"), groupId: g2Id, name: "Medium", priceAdjustment: 3, isActive: true, sortOrder: 1 },
106
- { id: nextId("mod"), groupId: g2Id, name: "Large", priceAdjustment: 5, isActive: true, sortOrder: 2 }
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
- function createMockMenuProvider() {
112
- const tenantId = "mock-tenant";
113
- const categories = seedCategories();
114
- const items = seedItems(categories);
115
- const modifierGroups = seedModifierGroups();
116
- return {
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 cat = {
338
+ const created = {
125
339
  id: nextId("cat"),
126
340
  name: input.name,
127
341
  parentId: input.parentId,
@@ -129,257 +343,1686 @@ function createMockMenuProvider() {
129
343
  color: input.color,
130
344
  sortOrder: input.sortOrder ?? categories.length,
131
345
  isActive: true,
132
- itemCount: 0,
133
- tenantId,
346
+ tenantId: TENANT,
134
347
  createdAt: now(),
135
348
  updatedAt: now()
136
349
  };
137
- categories.push(cat);
138
- return cat;
350
+ categories.push(created);
351
+ return created;
139
352
  },
140
- async updateCategory(id, data) {
141
- const cat = categories.find((c) => c.id === id);
142
- if (!cat) throw new Error(`Category ${id} not found`);
143
- Object.assign(cat, data, { updatedAt: now() });
144
- return cat;
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 idx = categories.findIndex((c) => c.id === id);
148
- if (idx >= 0) categories.splice(idx, 1);
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, i) => {
152
- const cat = categories.find((c) => c.id === id);
153
- if (cat) cat.sortOrder = i;
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 results = [...items];
158
- if (query.categoryId) results = results.filter((i) => i.categoryId === query.categoryId);
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
- results = results.filter((i) => statuses.includes(i.status));
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
- results.sort((a, b) => a.sortOrder - b.sortOrder);
171
- return paginate(results, query.page, query.pageSize);
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 cat = input.categoryId ? categories.find((c) => c.id === input.categoryId) : void 0;
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
- tenantId,
200
- createdAt: now(),
201
- updatedAt: now()
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, data) {
208
- const item = items.find((i) => i.id === id);
209
- if (!item) throw new Error(`MenuItem ${id} not found`);
210
- Object.assign(item, data, { updatedAt: now() });
211
- return item;
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
- const idx = items.findIndex((i) => i.id === id);
215
- if (idx >= 0) {
216
- const [removed] = items.splice(idx, 1);
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 item = items.find((i) => i.id === id);
225
- if (!item) throw new Error(`MenuItem ${id} not found`);
226
- item[field] = value;
227
- item.updatedAt = now();
228
- return item;
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(categoryId, ids) {
231
- ids.forEach((id, i) => {
232
- const item = items.find((it) => it.id === id && it.categoryId === categoryId);
233
- if (item) item.sortOrder = i;
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 group = {
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(group);
252
- return group;
449
+ modifierGroups.push(created);
450
+ return created;
253
451
  },
254
- async updateModifierGroup(id, data) {
255
- const group = modifierGroups.find((g) => g.id === id);
256
- if (!group) throw new Error(`ModifierGroup ${id} not found`);
257
- Object.assign(group, data);
258
- return group;
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 idx = modifierGroups.findIndex((g) => g.id === id);
262
- if (idx >= 0) modifierGroups.splice(idx, 1);
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 available = items.filter((i) => i.status === "available");
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: soldOut.length,
273
- hiddenItems: hidden.length,
274
- categoryCount: categories.length,
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
- function createMenuStore(provider) {
282
- return createStore((set, get) => ({
283
- categories: [],
284
- categoriesLoading: false,
285
- menuItems: [],
286
- menuItemsTotal: 0,
287
- menuItemsLoading: false,
288
- menuItemQuery: {},
289
- modifierGroups: [],
290
- modifiersLoading: false,
291
- summary: null,
292
- summaryLoading: false,
293
- async fetchCategories() {
294
- return dedup("menu:categories", async () => {
295
- set({ categoriesLoading: true });
296
- const categories = await provider.getCategories();
297
- set({ categories, categoriesLoading: false });
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 fetchMenuItems(query) {
301
- return dedup("menu:items:" + JSON.stringify(query), async () => {
302
- set({ menuItemsLoading: true, menuItemQuery: query });
303
- const result = await provider.getMenuItems(query);
304
- set({ menuItems: result.data, menuItemsTotal: result.total, menuItemsLoading: false });
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 fetchModifierGroups() {
308
- return dedup("menu:modifiers", async () => {
309
- set({ modifiersLoading: true });
310
- const modifierGroups = await provider.getModifierGroups();
311
- set({ modifierGroups, modifiersLoading: false });
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 fetchSummary() {
315
- return dedup("menu:summary", async () => {
316
- set({ summaryLoading: true });
317
- const summary = await provider.getSummary();
318
- set({ summary, summaryLoading: false });
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
- try {
323
- const item = await provider.createMenuItem(input);
324
- const query = get().menuItemQuery;
325
- const [result, summary] = await Promise.all([provider.getMenuItems(query), provider.getSummary()]);
326
- set({ menuItems: result.data, menuItemsTotal: result.total, summary });
327
- toast.success("Menu item created");
328
- return item;
329
- } catch (err) {
330
- toast.error("Failed to create menu item", { description: err?.message });
331
- throw err;
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);
884
+ }
885
+ if (input.sectionId) {
886
+ await provider.placeItem({ sectionId: input.sectionId, itemId: product.id });
332
887
  }
888
+ return await provider.getMenuItemById(product.id);
333
889
  },
334
- async updateMenuItem(id, data) {
335
- try {
336
- const item = await provider.updateMenuItem(id, data);
337
- const query = get().menuItemQuery;
338
- const result = await provider.getMenuItems(query);
339
- set({ menuItems: result.data, menuItemsTotal: result.total });
340
- toast.success("Menu item updated");
341
- return item;
342
- } catch (err) {
343
- toast.error("Failed to update menu item", { description: err?.message });
344
- throw err;
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);
345
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);
915
+ }
916
+ return await provider.getMenuItemById(id);
346
917
  },
347
918
  async deleteMenuItem(id) {
348
- try {
349
- await provider.deleteMenuItem(id);
350
- const query = get().menuItemQuery;
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
- try {
361
- await provider.toggleAvailability(id, field, value);
362
- const query = get().menuItemQuery;
363
- const result = await provider.getMenuItems(query);
364
- set({ menuItems: result.data, menuItemsTotal: result.total });
365
- } catch (err) {
366
- toast.error("Failed to update availability", { description: err?.message });
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 createCategory(input) {
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 MIGRATION_004_THE_MENU_DECLARES_ITS_SPECIES = `-- ---------------------------------------------------------------------------
1360
+ -- 004_the_menu_declares_its_species.sql \u2014 the card\xE1pio's taxonomy becomes a row
1361
+ -- (#274, ADR 0017).
1362
+ --
1363
+ -- The base owns the FORM of a Category; this plugin owns the species of it that
1364
+ -- a restaurant has. 147 named this case explicitly and left the declaring here,
1365
+ -- which is the rule the registry exists to make enforceable: a species is
1366
+ -- written by a plugin, in a row, with a foreign key behind it \u2014 not chosen as a
1367
+ -- literal and hoped to be unique.
1368
+ -- ---------------------------------------------------------------------------
1369
+
1370
+ DO $$
1371
+ BEGIN
1372
+ IF to_regprocedure('public.register_kind(text, text, text, text[], jsonb, text)') IS NULL THEN
1373
+ RAISE NOTICE 'plugin-menu 004: register_kind missing (spine < 147) \u2014 skipped';
1374
+ RETURN;
1375
+ END IF;
1376
+
1377
+ PERFORM public.register_kind(
1378
+ 'category', 'menu_category', 'menu', NULL, NULL,
1379
+ 'Uma se\xE7\xE3o do card\xE1pio: entradas, pratos, bebidas, sobremesas.');
1380
+ END $$;
1381
+ `;
1382
+ var MIGRATIONS = [
1383
+ { id: "001_menu_base", sql: MIGRATION_001_MENU_BASE },
1384
+ { id: "002_menu_backfill", sql: MIGRATION_002_MENU_BACKFILL },
1385
+ { id: "003_menu_storage", sql: MIGRATION_003_MENU_STORAGE },
1386
+ { id: "004_the_menu_declares_its_species", sql: MIGRATION_004_THE_MENU_DECLARES_ITS_SPECIES }
1387
+ ];
1388
+
1389
+ // src/data/types.ts
1390
+ function supportsMenus(provider) {
1391
+ return typeof provider.getMenus === "function" && typeof provider.createSection === "function";
1392
+ }
1393
+
1394
+ // src/data/legacy-adapter.ts
1395
+ var LEGACY_MENU_ID = "__default__";
1396
+ var placementId = (itemId) => `legacy:${itemId}`;
1397
+ var itemIdOf = (placement) => placement.replace(/^legacy:/, "");
1398
+ function unsupported(what) {
1399
+ throw new Error(`[menu] This workspace's menu backend does not support ${what} yet.`);
1400
+ }
1401
+ function wrapLegacyProvider(provider) {
1402
+ async function allItems() {
1403
+ const result = await provider.getMenuItems({ pageSize: 1e3 });
1404
+ return result.data;
1405
+ }
1406
+ const legacyMenu = async () => {
1407
+ const [categories, items] = await Promise.all([provider.getCategories(), allItems()]);
1408
+ const first = items[0];
1409
+ return {
1410
+ id: LEGACY_MENU_ID,
1411
+ name: "Card\xE1pio principal",
1412
+ isActive: true,
1413
+ sortOrder: 0,
1414
+ channels: ["pos", "delivery", "digital"],
1415
+ availability: {},
1416
+ sectionCount: categories.length,
1417
+ itemCount: items.length,
1418
+ tenantId: first?.tenantId ?? "",
1419
+ createdAt: first?.createdAt ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1420
+ updatedAt: first?.updatedAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
1421
+ };
1422
+ };
1423
+ const overrides = {
1424
+ async getMenus() {
1425
+ return [await legacyMenu()];
1426
+ },
1427
+ async createMenu() {
1428
+ unsupported("multiple menus");
1429
+ },
1430
+ async updateMenu() {
1431
+ unsupported("multiple menus");
1432
+ },
1433
+ async deleteMenu() {
1434
+ unsupported("multiple menus");
1435
+ },
1436
+ async duplicateMenu() {
1437
+ unsupported("multiple menus");
1438
+ },
1439
+ async reorderMenus() {
1440
+ },
1441
+ async getSections() {
1442
+ const categories = await provider.getCategories();
1443
+ return categories.map((c) => ({
1444
+ id: c.id,
1445
+ menuId: LEGACY_MENU_ID,
1446
+ name: c.name,
1447
+ sortOrder: c.sortOrder,
1448
+ isActive: c.isActive,
1449
+ tenantId: c.tenantId,
1450
+ createdAt: c.createdAt,
1451
+ updatedAt: c.updatedAt
1452
+ }));
1453
+ },
1454
+ async createSection(input) {
1455
+ const category = await provider.createCategory({ name: input.name, sortOrder: input.sortOrder });
1456
+ return {
1457
+ id: category.id,
1458
+ menuId: LEGACY_MENU_ID,
1459
+ name: category.name,
1460
+ sortOrder: category.sortOrder,
1461
+ isActive: category.isActive,
1462
+ tenantId: category.tenantId,
1463
+ createdAt: category.createdAt,
1464
+ updatedAt: category.updatedAt
1465
+ };
1466
+ },
1467
+ async updateSection(id, data) {
1468
+ const category = await provider.updateCategory(id, {
1469
+ name: data.name,
1470
+ sortOrder: data.sortOrder,
1471
+ isActive: data.isActive
1472
+ });
1473
+ return {
1474
+ id: category.id,
1475
+ menuId: LEGACY_MENU_ID,
1476
+ name: category.name,
1477
+ sortOrder: category.sortOrder,
1478
+ isActive: category.isActive,
1479
+ tenantId: category.tenantId,
1480
+ createdAt: category.createdAt,
1481
+ updatedAt: category.updatedAt
1482
+ };
1483
+ },
1484
+ async deleteSection(id) {
1485
+ await provider.deleteCategory(id);
1486
+ },
1487
+ async reorderSections(_menuId, ids) {
1488
+ await provider.reorderCategories(ids);
1489
+ },
1490
+ async getPlacements() {
1491
+ const items = await allItems();
1492
+ return items.filter((item) => !!item.categoryId).map((item) => ({
1493
+ id: placementId(item.id),
1494
+ sectionId: item.categoryId,
1495
+ itemId: item.id,
1496
+ sortOrder: item.sortOrder ?? 0,
1497
+ isActive: item.status !== "hidden",
1498
+ tenantId: item.tenantId
1499
+ }));
1500
+ },
1501
+ async placeItem(input) {
1502
+ await provider.updateMenuItem(input.itemId, { categoryId: input.sectionId });
1503
+ return {
1504
+ id: placementId(input.itemId),
1505
+ sectionId: input.sectionId,
1506
+ itemId: input.itemId,
1507
+ sortOrder: input.sortOrder ?? 0,
1508
+ isActive: input.isActive ?? true,
1509
+ tenantId: ""
1510
+ };
1511
+ },
1512
+ async updatePlacement(id, data) {
1513
+ if (data.sectionId) await provider.updateMenuItem(itemIdOf(id), { categoryId: data.sectionId });
1514
+ return {
1515
+ id,
1516
+ sectionId: data.sectionId ?? "",
1517
+ itemId: itemIdOf(id),
1518
+ sortOrder: data.sortOrder ?? 0,
1519
+ isActive: data.isActive ?? true,
1520
+ tenantId: ""
1521
+ };
1522
+ },
1523
+ async removePlacement(id) {
1524
+ await provider.updateMenuItem(itemIdOf(id), { categoryId: void 0 });
1525
+ },
1526
+ async reorderPlacements(sectionId, placementIds) {
1527
+ await provider.reorderItems(sectionId, placementIds.map(itemIdOf));
1528
+ },
1529
+ async movePlacement(id, toSectionId) {
1530
+ await provider.updateMenuItem(itemIdOf(id), { categoryId: toSectionId });
1531
+ }
1532
+ };
1533
+ return new Proxy({}, {
1534
+ get: (_t, prop) => overrides[prop] ?? provider[prop]
1535
+ });
1536
+ }
1537
+
1538
+ // src/store.ts
1539
+ var optimisticSeq = 0;
1540
+ function createMenuStore(rawProvider) {
1541
+ const multiMenu = supportsMenus(rawProvider);
1542
+ const provider = multiMenu ? rawProvider : wrapLegacyProvider(rawProvider);
1543
+ return createStore((set, get) => {
1544
+ async function writeItemField(id, field, value) {
1545
+ const previous = get().menuItems;
1546
+ set({ menuItems: previous.map((i) => i.id === id ? { ...i, [field]: value } : i) });
371
1547
  try {
372
- const category = await provider.createCategory(input);
373
- const categories = await provider.getCategories();
374
- set({ categories });
375
- toast.success("Category created");
376
- return category;
1548
+ await provider.toggleAvailability(id, field, value);
377
1549
  } catch (err) {
378
- toast.error("Failed to create category", { description: err?.message });
379
- throw err;
1550
+ set({ menuItems: previous });
1551
+ toast.error(tl("Could not update the item", "N\xE3o foi poss\xEDvel atualizar o item"), { description: err?.message });
380
1552
  }
381
1553
  }
382
- }));
1554
+ return {
1555
+ multiMenu,
1556
+ menus: [],
1557
+ menusLoading: false,
1558
+ activeMenuId: null,
1559
+ sections: [],
1560
+ placements: [],
1561
+ menuLoading: false,
1562
+ pendingIds: [],
1563
+ menuItems: [],
1564
+ menuItemsTotal: 0,
1565
+ menuItemsLoading: false,
1566
+ menuItemQuery: {},
1567
+ modifierGroups: [],
1568
+ modifiersLoading: false,
1569
+ summary: null,
1570
+ summaryLoading: false,
1571
+ // --- Reads -------------------------------------------------------------
1572
+ async fetchMenus() {
1573
+ set({ menusLoading: true });
1574
+ try {
1575
+ const menus = await dedup("menu:menus", () => provider.getMenus());
1576
+ const active = get().activeMenuId && menus.some((m) => m.id === get().activeMenuId) ? get().activeMenuId : menus[0]?.id ?? null;
1577
+ set({ menus, activeMenuId: active, menusLoading: false });
1578
+ if (active) await get().fetchMenuContent(active);
1579
+ } catch (err) {
1580
+ set({ menusLoading: false });
1581
+ toast.error(tl("Could not load the menus", "N\xE3o foi poss\xEDvel carregar os card\xE1pios"), { description: err?.message });
1582
+ }
1583
+ },
1584
+ async openMenu(menuId) {
1585
+ set({ activeMenuId: menuId });
1586
+ await get().fetchMenuContent(menuId);
1587
+ },
1588
+ async fetchMenuContent(menuId) {
1589
+ set({ menuLoading: true });
1590
+ try {
1591
+ const [sections, placements] = await Promise.all([
1592
+ provider.getSections(menuId),
1593
+ provider.getPlacements(menuId)
1594
+ ]);
1595
+ set({ sections, placements, menuLoading: false });
1596
+ } catch (err) {
1597
+ set({ menuLoading: false });
1598
+ toast.error(tl("Could not load the menu", "N\xE3o foi poss\xEDvel carregar o card\xE1pio"), { description: err?.message });
1599
+ }
1600
+ },
1601
+ async fetchMenuItems(query = {}) {
1602
+ set({ menuItemsLoading: true, menuItemQuery: query });
1603
+ try {
1604
+ const result = await provider.getMenuItems(query);
1605
+ set({ menuItems: result.data, menuItemsTotal: result.total, menuItemsLoading: false });
1606
+ } catch (err) {
1607
+ set({ menuItemsLoading: false });
1608
+ toast.error(tl("Could not load the items", "N\xE3o foi poss\xEDvel carregar os itens"), { description: err?.message });
1609
+ }
1610
+ },
1611
+ async fetchModifierGroups() {
1612
+ set({ modifiersLoading: true });
1613
+ try {
1614
+ const modifierGroups = await dedup("menu:modifiers", () => provider.getModifierGroups());
1615
+ set({ modifierGroups, modifiersLoading: false });
1616
+ } catch {
1617
+ set({ modifiersLoading: false, modifierGroups: [] });
1618
+ }
1619
+ },
1620
+ async fetchSummary() {
1621
+ set({ summaryLoading: true });
1622
+ try {
1623
+ const summary = await dedup("menu:summary", () => provider.getSummary());
1624
+ set({ summary, summaryLoading: false });
1625
+ } catch {
1626
+ set({ summaryLoading: false });
1627
+ }
1628
+ },
1629
+ // --- Menus -------------------------------------------------------------
1630
+ async createMenu(input) {
1631
+ const menu = await provider.createMenu(input);
1632
+ set({ menus: [...get().menus, menu] });
1633
+ await get().openMenu(menu.id);
1634
+ toast.success(tl("Menu created", "Card\xE1pio criado"));
1635
+ return menu;
1636
+ },
1637
+ async updateMenu(id, data) {
1638
+ const updated = await provider.updateMenu(id, data);
1639
+ set({ menus: get().menus.map((m) => m.id === id ? { ...m, ...updated } : m) });
1640
+ },
1641
+ async deleteMenu(id) {
1642
+ await provider.deleteMenu(id);
1643
+ const menus = get().menus.filter((m) => m.id !== id);
1644
+ set({ menus });
1645
+ const next = menus[0]?.id ?? null;
1646
+ set({ activeMenuId: next, sections: [], placements: [] });
1647
+ if (next) await get().fetchMenuContent(next);
1648
+ toast.success(tl("Menu deleted", "Card\xE1pio exclu\xEDdo"));
1649
+ },
1650
+ async duplicateMenu(id, name) {
1651
+ const copy = await provider.duplicateMenu(id, name);
1652
+ await get().fetchMenus();
1653
+ await get().openMenu(copy.id);
1654
+ 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.") });
1655
+ return copy;
1656
+ },
1657
+ // --- Sections ----------------------------------------------------------
1658
+ async createSection(name, extra) {
1659
+ const menuId = get().activeMenuId;
1660
+ if (!menuId) return null;
1661
+ const tempId = `pending-${++optimisticSeq}`;
1662
+ const optimistic = {
1663
+ id: tempId,
1664
+ menuId,
1665
+ name,
1666
+ sortOrder: extra?.sortOrder ?? get().sections.length,
1667
+ isActive: extra?.isActive ?? true,
1668
+ availability: extra?.availability ?? {},
1669
+ tenantId: "",
1670
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1671
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1672
+ };
1673
+ set({
1674
+ sections: [...get().sections, optimistic],
1675
+ pendingIds: [...get().pendingIds, tempId]
1676
+ });
1677
+ try {
1678
+ const section = await provider.createSection({ menuId, name, ...extra });
1679
+ set({
1680
+ sections: get().sections.map((s) => s.id === tempId ? section : s),
1681
+ pendingIds: get().pendingIds.filter((id) => id !== tempId)
1682
+ });
1683
+ return section;
1684
+ } catch (err) {
1685
+ set({
1686
+ sections: get().sections.filter((s) => s.id !== tempId),
1687
+ pendingIds: get().pendingIds.filter((id) => id !== tempId)
1688
+ });
1689
+ toast.error(tl("Could not create the section", "N\xE3o foi poss\xEDvel criar a se\xE7\xE3o"), { description: err?.message });
1690
+ return null;
1691
+ }
1692
+ },
1693
+ async updateSection(id, data) {
1694
+ const previous = get().sections;
1695
+ set({ sections: previous.map((s) => s.id === id ? { ...s, ...data } : s) });
1696
+ try {
1697
+ await provider.updateSection(id, data);
1698
+ } catch (err) {
1699
+ set({ sections: previous });
1700
+ toast.error(tl("Could not save the section", "N\xE3o foi poss\xEDvel salvar a se\xE7\xE3o"), { description: err?.message });
1701
+ }
1702
+ },
1703
+ async deleteSection(id) {
1704
+ try {
1705
+ await provider.deleteSection(id);
1706
+ set({
1707
+ sections: get().sections.filter((s) => s.id !== id),
1708
+ placements: get().placements.filter((p) => p.sectionId !== id)
1709
+ });
1710
+ toast.success(tl("Section removed", "Se\xE7\xE3o removida"));
1711
+ } catch (err) {
1712
+ toast.error(tl("Could not remove the section", "N\xE3o foi poss\xEDvel remover a se\xE7\xE3o"), { description: err?.message });
1713
+ }
1714
+ },
1715
+ async reorderSections(orderedIds) {
1716
+ const menuId = get().activeMenuId;
1717
+ if (!menuId) return;
1718
+ const previous = get().sections;
1719
+ set({
1720
+ sections: orderedIds.map((id, index) => {
1721
+ const section = previous.find((s) => s.id === id);
1722
+ return section ? { ...section, sortOrder: index } : null;
1723
+ }).filter(Boolean)
1724
+ });
1725
+ try {
1726
+ await provider.reorderSections(menuId, orderedIds);
1727
+ } catch (err) {
1728
+ set({ sections: previous });
1729
+ toast.error(tl("Could not save the new order", "N\xE3o foi poss\xEDvel salvar a ordem"), { description: err?.message });
1730
+ }
1731
+ },
1732
+ // --- Items in the open menu -------------------------------------------
1733
+ async addItemToSection(sectionId, input) {
1734
+ const tempId = `pending-${++optimisticSeq}`;
1735
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
1736
+ const optimisticItem = {
1737
+ id: tempId,
1738
+ name: input.name,
1739
+ description: input.description,
1740
+ price: input.price ?? 0,
1741
+ currency: "",
1742
+ imageUrl: input.imageUrl,
1743
+ status: input.status ?? "available",
1744
+ isActive: true,
1745
+ availableForPos: input.availableForPos ?? true,
1746
+ availableForDelivery: input.availableForDelivery ?? true,
1747
+ deliveryPrice: input.deliveryPrice,
1748
+ prepTimeMinutes: input.prepTimeMinutes,
1749
+ allergens: input.allergens ?? [],
1750
+ dietaryTags: input.dietaryTags ?? [],
1751
+ sortOrder: 0,
1752
+ isFeatured: input.isFeatured ?? false,
1753
+ tenantId: "",
1754
+ createdAt: now2,
1755
+ updatedAt: now2
1756
+ };
1757
+ const optimisticPlacement = {
1758
+ id: `pending-placement-${optimisticSeq}`,
1759
+ sectionId,
1760
+ itemId: tempId,
1761
+ sortOrder: get().placements.filter((p) => p.sectionId === sectionId).length,
1762
+ isActive: true,
1763
+ tenantId: ""
1764
+ };
1765
+ set({
1766
+ menuItems: [...get().menuItems, optimisticItem],
1767
+ placements: [...get().placements, optimisticPlacement],
1768
+ pendingIds: [...get().pendingIds, optimisticPlacement.id]
1769
+ });
1770
+ try {
1771
+ const item = await provider.createMenuItem({ ...input, sectionId });
1772
+ const menuId = get().activeMenuId;
1773
+ set({ menuItems: get().menuItems.map((i) => i.id === tempId ? item : i) });
1774
+ if (menuId) await get().fetchMenuContent(menuId);
1775
+ set({ pendingIds: get().pendingIds.filter((id) => id !== optimisticPlacement.id) });
1776
+ void get().fetchSummary();
1777
+ return item;
1778
+ } catch (err) {
1779
+ set({
1780
+ menuItems: get().menuItems.filter((i) => i.id !== tempId),
1781
+ placements: get().placements.filter((p) => p.id !== optimisticPlacement.id),
1782
+ pendingIds: get().pendingIds.filter((id) => id !== optimisticPlacement.id)
1783
+ });
1784
+ toast.error(tl("Could not create the item", "N\xE3o foi poss\xEDvel criar o item"), { description: err?.message });
1785
+ return null;
1786
+ }
1787
+ },
1788
+ async placeExistingItem(sectionId, itemId) {
1789
+ try {
1790
+ const placement = await provider.placeItem({ sectionId, itemId });
1791
+ set({ placements: [...get().placements.filter((p) => p.id !== placement.id), placement] });
1792
+ } catch (err) {
1793
+ toast.error(tl("Could not add it to the menu", "N\xE3o foi poss\xEDvel adicionar ao card\xE1pio"), { description: err?.message });
1794
+ }
1795
+ },
1796
+ async removePlacement(placementId2) {
1797
+ const previous = get().placements;
1798
+ set({ placements: previous.filter((p) => p.id !== placementId2) });
1799
+ try {
1800
+ await provider.removePlacement(placementId2);
1801
+ } catch (err) {
1802
+ set({ placements: previous });
1803
+ toast.error(tl("Could not remove it from the menu", "N\xE3o foi poss\xEDvel remover do card\xE1pio"), { description: err?.message });
1804
+ }
1805
+ },
1806
+ async setPlacementPrice(placementId2, price) {
1807
+ const previous = get().placements;
1808
+ set({ placements: previous.map((p) => p.id === placementId2 ? { ...p, priceOverride: price } : p) });
1809
+ try {
1810
+ await provider.updatePlacement(placementId2, { priceOverride: price });
1811
+ } catch (err) {
1812
+ set({ placements: previous });
1813
+ toast.error(tl("Could not save the price", "N\xE3o foi poss\xEDvel salvar o pre\xE7o"), { description: err?.message });
1814
+ }
1815
+ },
1816
+ async reorderPlacements(sectionId, orderedIds) {
1817
+ const previous = get().placements;
1818
+ const reordered = previous.map((p) => {
1819
+ const index = orderedIds.indexOf(p.id);
1820
+ return index >= 0 ? { ...p, sortOrder: index } : p;
1821
+ });
1822
+ set({ placements: reordered });
1823
+ try {
1824
+ await provider.reorderPlacements(sectionId, orderedIds);
1825
+ } catch (err) {
1826
+ set({ placements: previous });
1827
+ toast.error(tl("Could not save the new order", "N\xE3o foi poss\xEDvel salvar a ordem"), { description: err?.message });
1828
+ }
1829
+ },
1830
+ async movePlacement(placementId2, toSectionId, toIndex) {
1831
+ const previous = get().placements;
1832
+ set({
1833
+ placements: previous.map((p) => p.id === placementId2 ? { ...p, sectionId: toSectionId, sortOrder: toIndex } : p)
1834
+ });
1835
+ try {
1836
+ await provider.movePlacement(placementId2, toSectionId, toIndex);
1837
+ } catch (err) {
1838
+ set({ placements: previous });
1839
+ toast.error(tl("Could not move the item", "N\xE3o foi poss\xEDvel mover o item"), { description: err?.message });
1840
+ }
1841
+ },
1842
+ // --- Items -------------------------------------------------------------
1843
+ async updateMenuItem(id, data) {
1844
+ const previous = get().menuItems;
1845
+ set({ menuItems: previous.map((i) => i.id === id ? { ...i, ...data } : i) });
1846
+ try {
1847
+ const updated = await provider.updateMenuItem(id, data);
1848
+ set({ menuItems: get().menuItems.map((i) => i.id === id ? updated : i) });
1849
+ } catch (err) {
1850
+ set({ menuItems: previous });
1851
+ toast.error(tl("Could not save the item", "N\xE3o foi poss\xEDvel salvar o item"), { description: err?.message });
1852
+ }
1853
+ },
1854
+ async deleteMenuItem(id) {
1855
+ try {
1856
+ await provider.deleteMenuItem(id);
1857
+ set({
1858
+ menuItems: get().menuItems.filter((i) => i.id !== id),
1859
+ placements: get().placements.filter((p) => p.itemId !== id)
1860
+ });
1861
+ void get().fetchSummary();
1862
+ toast.success(tl("Item removed", "Item removido"));
1863
+ } catch (err) {
1864
+ toast.error(tl("Could not remove the item", "N\xE3o foi poss\xEDvel remover o item"), { description: err?.message });
1865
+ }
1866
+ },
1867
+ async duplicateMenuItem(id, sectionId) {
1868
+ const source = get().menuItems.find((i) => i.id === id);
1869
+ if (!source) return;
1870
+ await get().addItemToSection(sectionId ?? get().placements.find((p) => p.itemId === id)?.sectionId ?? "", {
1871
+ name: `${source.name} (c\xF3pia)`,
1872
+ description: source.description,
1873
+ price: source.price,
1874
+ cost: source.cost,
1875
+ imageUrl: source.imageUrl,
1876
+ prepTimeMinutes: source.prepTimeMinutes,
1877
+ allergens: source.allergens,
1878
+ dietaryTags: source.dietaryTags,
1879
+ availableForPos: source.availableForPos,
1880
+ availableForDelivery: source.availableForDelivery,
1881
+ deliveryPrice: source.deliveryPrice
1882
+ });
1883
+ },
1884
+ async setItemStatus(id, status) {
1885
+ await writeItemField(id, "status", status);
1886
+ },
1887
+ async toggleAvailability(id, field, value) {
1888
+ await writeItemField(id, field, value);
1889
+ },
1890
+ async uploadImage(file, kind) {
1891
+ if (!provider.uploadImage) {
1892
+ toast.error(tl("This workspace does not store menu photos yet.", "Este espa\xE7o ainda n\xE3o guarda fotos do card\xE1pio."));
1893
+ return null;
1894
+ }
1895
+ try {
1896
+ return await provider.uploadImage(file, kind);
1897
+ } catch (err) {
1898
+ toast.error("N\xE3o foi poss\xEDvel enviar a foto", { description: err?.message });
1899
+ return null;
1900
+ }
1901
+ },
1902
+ // --- AI import ---------------------------------------------------------
1903
+ extracting: false,
1904
+ draft: null,
1905
+ async runExtraction(images, extract) {
1906
+ if (images.length === 0) return;
1907
+ set({ extracting: true });
1908
+ try {
1909
+ const result = await extract(images);
1910
+ const counts = countExtracted(result);
1911
+ if (counts.items === 0) {
1912
+ set({ extracting: false });
1913
+ toast.error(tl("I could not read any items in those photos", "N\xE3o consegui ler itens nessas fotos"), {
1914
+ description: tl("Try one photo per page, closer and without glare.", "Tente uma foto por p\xE1gina, mais perto e sem reflexo.")
1915
+ });
1916
+ return;
1917
+ }
1918
+ const existing = get().sections;
1919
+ let key = 0;
1920
+ const draft = {
1921
+ source: "photo",
1922
+ model: result.model,
1923
+ sections: result.sections.map((section) => ({
1924
+ key: `ds-${++key}`,
1925
+ name: section.name,
1926
+ targetSectionId: existing.find((s) => normalizeName(s.name) === normalizeName(section.name))?.id,
1927
+ items: section.items.map((item) => ({
1928
+ key: `di-${++key}`,
1929
+ name: item.name,
1930
+ price: item.price,
1931
+ description: item.description
1932
+ }))
1933
+ }))
1934
+ };
1935
+ set({ draft, extracting: false });
1936
+ } catch (err) {
1937
+ set({ extracting: false });
1938
+ toast.error(tl("I could not read the photos", "N\xE3o consegui ler as fotos"), { description: err?.message });
1939
+ }
1940
+ },
1941
+ updateDraftItem(sectionKey, itemKey, patch) {
1942
+ const draft = get().draft;
1943
+ if (!draft) return;
1944
+ set({
1945
+ draft: {
1946
+ ...draft,
1947
+ sections: draft.sections.map((section) => section.key !== sectionKey ? section : {
1948
+ ...section,
1949
+ items: section.items.map((item) => item.key === itemKey ? { ...item, ...patch } : item)
1950
+ })
1951
+ }
1952
+ });
1953
+ },
1954
+ removeDraftItem(sectionKey, itemKey) {
1955
+ const draft = get().draft;
1956
+ if (!draft) return;
1957
+ const sections = draft.sections.map((section) => section.key !== sectionKey ? section : {
1958
+ ...section,
1959
+ items: section.items.filter((item) => item.key !== itemKey)
1960
+ }).filter((section) => section.items.length > 0);
1961
+ set({ draft: sections.length > 0 ? { ...draft, sections } : null });
1962
+ },
1963
+ updateDraftSection(sectionKey, patch) {
1964
+ const draft = get().draft;
1965
+ if (!draft) return;
1966
+ set({
1967
+ draft: {
1968
+ ...draft,
1969
+ sections: draft.sections.map((section) => section.key === sectionKey ? { ...section, ...patch } : section)
1970
+ }
1971
+ });
1972
+ },
1973
+ removeDraftSection(sectionKey) {
1974
+ const draft = get().draft;
1975
+ if (!draft) return;
1976
+ const sections = draft.sections.filter((section) => section.key !== sectionKey);
1977
+ set({ draft: sections.length > 0 ? { ...draft, sections } : null });
1978
+ },
1979
+ discardDraft() {
1980
+ set({ draft: null });
1981
+ },
1982
+ async commitDraft() {
1983
+ const draft = get().draft;
1984
+ const menuId = get().activeMenuId;
1985
+ if (!draft || !menuId) return { sectionsCreated: 0, itemsCreated: 0, failed: 0 };
1986
+ const result = await get().applyImportPlan({
1987
+ menuId,
1988
+ sections: draft.sections.map((section) => ({
1989
+ sectionId: section.targetSectionId,
1990
+ name: section.name,
1991
+ items: section.items.map(({ name, price, description }) => ({ name, price, description }))
1992
+ }))
1993
+ });
1994
+ set({ draft: null });
1995
+ return result;
1996
+ },
1997
+ async applyImportPlan(plan) {
1998
+ const result = { sectionsCreated: 0, itemsCreated: 0, failed: 0 };
1999
+ for (const section of plan.sections) {
2000
+ let sectionId = section.sectionId;
2001
+ if (!sectionId) {
2002
+ const created = await provider.createSection({ menuId: plan.menuId, name: section.name });
2003
+ sectionId = created.id;
2004
+ result.sectionsCreated++;
2005
+ }
2006
+ for (const item of section.items) {
2007
+ try {
2008
+ await provider.createMenuItem({
2009
+ name: item.name,
2010
+ description: item.description,
2011
+ price: item.price,
2012
+ sectionId
2013
+ });
2014
+ result.itemsCreated++;
2015
+ } catch {
2016
+ result.failed++;
2017
+ }
2018
+ }
2019
+ }
2020
+ await get().fetchMenus();
2021
+ void get().fetchSummary();
2022
+ return result;
2023
+ }
2024
+ };
2025
+ });
383
2026
  }
384
2027
 
385
2028
  // src/data/registries.ts
@@ -394,7 +2037,7 @@ var allergenEntity = {
394
2037
  { key: "icon", label: "Icon", type: "text", showInTable: true },
395
2038
  { key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true }
396
2039
  ],
397
- data: { table: "menu_allergens", tenantScoped: true }
2040
+ data: { table: T.allergens, tenantScoped: true }
398
2041
  };
399
2042
  var dietaryTagEntity = {
400
2043
  name: "Dietary Tag",
@@ -407,7 +2050,7 @@ var dietaryTagEntity = {
407
2050
  { key: "color", label: "Color", type: "text", showInTable: true },
408
2051
  { key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true }
409
2052
  ],
410
- data: { table: "menu_dietary_tags", tenantScoped: true }
2053
+ data: { table: T.dietaryTags, tenantScoped: true }
411
2054
  };
412
2055
  var modifierGroupEntity = {
413
2056
  name: "Modifier Group",
@@ -421,7 +2064,7 @@ var modifierGroupEntity = {
421
2064
  { key: "maxSelections", label: "Max Selections", type: "number", showInTable: true, defaultValue: 1 },
422
2065
  { key: "isRequired", label: "Required", type: "boolean", showInTable: true, defaultValue: false }
423
2066
  ],
424
- data: { table: "menu_modifier_groups", tenantScoped: true }
2067
+ data: { table: T.modifierGroups, tenantScoped: true }
425
2068
  };
426
2069
  var menuRegistries = [
427
2070
  {
@@ -466,132 +2109,282 @@ var menuRegistries = [
466
2109
  var en = {
467
2110
  "menu.title": "Menu",
468
2111
  "menu.nav.manager": "Menu",
469
- "menu.nav.categories": "Categories",
470
- "menu.nav.items": "Items",
471
- "menu.nav.modifiers": "Modifiers",
472
- "menu.manager.title": "Menu Manager",
473
- "menu.manager.subtitle": "Manage your menu items and categories",
474
- "menu.manager.search": "Search menu...",
475
- "menu.manager.allCategories": "All Categories",
476
- "menu.manager.newItem": "New Item",
477
- "menu.manager.noItems": "No menu items yet",
478
- "menu.manager.noItemsDesc": "Add your first menu item to get started",
479
- "menu.manager.addFirst": "Add first item",
2112
+ // Manager
2113
+ "menu.manager.title": "Menu",
2114
+ "menu.manager.subtitle": "Build your menus, with sections and order",
2115
+ "menu.manager.search": "Search the menu...",
2116
+ "menu.manager.newMenu": "New menu",
2117
+ "menu.manager.menuActions": "Menu actions",
2118
+ "menu.manager.editMenu": "Edit menu",
2119
+ "menu.manager.duplicateMenu": "Duplicate menu",
2120
+ "menu.manager.activateMenu": "Activate menu",
2121
+ "menu.manager.deactivateMenu": "Deactivate menu",
2122
+ "menu.manager.deleteMenu": "Delete menu",
2123
+ "menu.manager.menuOff": "Off",
2124
+ "menu.manager.servingNow": "Serving now",
2125
+ "menu.manager.notServing": "Outside its hours",
2126
+ "menu.manager.addSection": "New section",
2127
+ "menu.manager.newSectionName": "New section",
2128
+ "menu.manager.editSection": "Turn section on/off",
2129
+ "menu.manager.deleteSection": "Delete section",
2130
+ "menu.manager.sectionActions": "Section actions",
2131
+ "menu.manager.sectionOff": "Section off",
2132
+ "menu.manager.emptySection": "Nothing here yet \u2014 click to add",
2133
+ "menu.manager.addItem": "Add item",
2134
+ "menu.manager.itemActions": "Item actions",
2135
+ "menu.manager.itemsCount": "{{count}} items",
2136
+ "menu.manager.edit": "Edit",
2137
+ "menu.manager.duplicate": "Duplicate",
2138
+ "menu.manager.markSoldOut": "Mark sold out",
2139
+ "menu.manager.markAvailable": "Mark available",
2140
+ "menu.manager.feature": "Feature",
2141
+ "menu.manager.unfeature": "Remove from featured",
2142
+ "menu.manager.removeFromMenu": "Remove from this menu",
2143
+ "menu.manager.deleteItem": "Delete item",
2144
+ "menu.manager.dragSection": "Drag section",
2145
+ "menu.manager.dragItem": "Drag item",
2146
+ "menu.manager.expand": "Expand",
2147
+ "menu.manager.collapse": "Collapse",
480
2148
  "menu.manager.available": "Available",
481
- "menu.manager.soldOut": "Sold Out",
2149
+ "menu.manager.soldOut": "Sold out",
482
2150
  "menu.manager.hidden": "Hidden",
483
2151
  "menu.manager.pos": "POS",
484
2152
  "menu.manager.delivery": "Delivery",
485
2153
  "menu.manager.featured": "Featured",
486
- "menu.manager.prepTime": "Prep time",
487
- "menu.manager.min": "min",
488
- "menu.manager.addCategory": "Add Category",
489
- "menu.manager.addProduct": "Add Product",
490
- "menu.manager.item": "Item",
491
- "menu.manager.price": "Price",
492
- "menu.manager.deliveryPrice": "Delivery $",
493
- "menu.manager.itemsCount": "{{count}} items",
494
- "menu.manager.noResults": "No categories or items match your search.",
495
- "menu.manager.noResultsDesc": "Try a different search term or category filter.",
496
- "menu.itemForm.newTitle": "New Menu Item",
497
- "menu.itemForm.editTitle": "Edit Menu Item",
498
- "menu.itemForm.name": "Item name",
2154
+ "menu.manager.overridden": "this menu",
2155
+ "menu.manager.overriddenHint": "This item costs a different price on this menu",
2156
+ "menu.manager.empty": "Your menu is still empty",
2157
+ "menu.manager.emptyHint": "Create the first section, or send photos of the printed menu and the assistant fills it in.",
2158
+ // Menu dialog
2159
+ "menu.menuDialog.newTitle": "New menu",
2160
+ "menu.menuDialog.editTitle": "Edit menu",
2161
+ "menu.menuDialog.name": "Name",
2162
+ "menu.menuDialog.namePlaceholder": "Lunch, Weekend, Delivery...",
2163
+ "menu.menuDialog.description": "Description",
2164
+ "menu.menuDialog.descriptionPlaceholder": "Weekday lunch",
2165
+ "menu.menuDialog.channels": "Where it shows",
2166
+ "menu.menuDialog.active": "Menu active",
2167
+ "menu.menuDialog.activeHint": "Turned off it leaves the floor and delivery without being deleted",
2168
+ "menu.channel.pos": "Dine-in / POS",
2169
+ "menu.channel.delivery": "Delivery",
2170
+ "menu.channel.digital": "Digital menu",
2171
+ // Schedule
2172
+ "menu.schedule.always": "Every day, all day",
2173
+ "menu.schedule.restrict": "Only on certain days and hours",
2174
+ "menu.schedule.restrictHint": "This is how a weekday lunch menu is born",
2175
+ "menu.schedule.days": "Days",
2176
+ "menu.schedule.from": "From",
2177
+ "menu.schedule.to": "To",
2178
+ "menu.schedule.overnightHint": "If the end is before the start, the menu crosses midnight (22:00\u201302:00).",
2179
+ "menu.schedule.presetWeekdays": "Weekdays",
2180
+ "menu.schedule.presetWeekend": "Weekend",
2181
+ "menu.schedule.presetEveryDay": "Every day",
2182
+ // Item form
2183
+ "menu.itemForm.newTitle": "New item",
2184
+ "menu.itemForm.editTitle": "Edit item",
2185
+ "menu.itemForm.name": "Name",
2186
+ "menu.itemForm.namePlaceholder": "Grilled picanha",
499
2187
  "menu.itemForm.description": "Description",
2188
+ "menu.itemForm.descriptionPlaceholder": "Serves 2, comes with farofa",
2189
+ "menu.itemForm.photo": "Photo",
2190
+ "menu.itemForm.photoHint": "Square, well lit. Shows on the digital menu and delivery.",
2191
+ "menu.itemForm.pricingGroup": "Pricing",
2192
+ "menu.itemForm.menuGroup": "On the menu",
2193
+ "menu.itemForm.posHint": "Shows for whoever is serving the floor",
2194
+ "menu.itemForm.deliveryHint": "Shows for orders from outside",
2195
+ "menu.itemForm.featuredHint": "Moves to the top of its section",
500
2196
  "menu.itemForm.price": "Price",
501
2197
  "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
2198
  "menu.itemForm.deliveryPrice": "Delivery price",
510
- "menu.itemForm.isFeatured": "Featured item",
511
- "menu.itemForm.save": "Save",
512
- "menu.itemForm.cancel": "Cancel",
513
- "menu.itemForm.saving": "Saving...",
514
- "menu.categories.title": "Categories",
515
- "menu.categories.newCategory": "New Category",
516
- "menu.categories.name": "Category name",
517
- "menu.settings.title": "Menu Settings",
518
- "menu.settings.general": "General",
519
- "menu.settings.taxRate": "Default tax rate",
520
- "menu.settings.currency": "Currency",
521
- "menu.form.categoryName": "Category name",
522
- "menu.form.itemName": "Item name",
523
- "menu.form.description": "Description",
524
- "menu.form.category": "Category",
525
- "menu.form.uncategorized": "Uncategorized",
526
- "menu.form.optional": "optional",
2199
+ "menu.itemForm.prepTime": "Prep (min)",
2200
+ "menu.itemForm.section": "Section",
2201
+ "menu.itemForm.status": "Status",
2202
+ "menu.itemForm.priceOverride": "Price on this menu only",
2203
+ "menu.itemForm.priceOverrideHint": "The same dish can cost less at lunch. Empty = the item price.",
2204
+ "menu.itemForm.clearOverride": "Use the item price",
2205
+ // AI import
2206
+ "menu.import.action": "Add with AI",
2207
+ "menu.import.uploadCta": "Upload a photo of the menu",
2208
+ "menu.import.uploadHint": "One photo per page, straight on, no glare. Several at once is fine.",
2209
+ "menu.import.askPrompt": "I want to add my menu from photos.",
2210
+ "menu.import.askHint": "Describe the menu, or ask to start from photos\u2026",
2211
+ "menu.import.askPersona": "Menu setup",
2212
+ "menu.import.askPitch": "Send me photos of the menu and I will type it in.",
2213
+ "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.",
2214
+ "menu.import.askSuggestion.0": "I will send photos of the printed menu",
2215
+ "menu.import.askSuggestion.1": "Create a weekday lunch menu",
2216
+ "menu.import.askSuggestion.2": "Add a desserts section",
2217
+ "menu.import.createSection": "Create new section",
2218
+ "menu.import.intoSection": "Add into: {{name}}",
2219
+ "menu.import.willAppend": "goes into {{name}}",
2220
+ "menu.import.dropItem": "Drop item",
2221
+ "menu.import.dropSection": "Drop section",
2222
+ "menu.import.draftCount": "{{items}} items read across {{sections}} sections",
2223
+ "menu.import.draftHint": "Check and fix them right here \u2014 nothing is saved yet.",
2224
+ "menu.import.draftMissingPrice": "{{count}} items came without a price. Fill them in before saving.",
2225
+ "menu.import.discard": "Discard",
2226
+ "menu.import.commit": "Save {{count}} items",
2227
+ "menu.import.done": "{{items}} items in {{sections}} sections created",
2228
+ "menu.import.someFailed": "{{count}} items could not be created",
2229
+ // Preview
2230
+ "menu.preview.action": "Preview",
2231
+ "menu.preview.title": "Menu",
2232
+ "menu.preview.notServingNow": "outside its hours right now",
2233
+ // Confirmations
2234
+ "menu.confirm.sectionTitle": "Delete the section {{name}}?",
2235
+ "menu.confirm.sectionDesc": "Its items leave this menu but stay registered.",
2236
+ "menu.confirm.itemTitle": "Delete the item {{name}}?",
2237
+ "menu.confirm.itemDesc": "It leaves every menu it appears in.",
2238
+ "menu.confirm.menuTitle": "Delete the menu {{name}}?",
2239
+ "menu.confirm.menuDesc": "Its sections go with it. The dishes stay registered.",
2240
+ "menu.confirm.delete": "Delete",
2241
+ // Shared form bits
527
2242
  "menu.form.save": "Save",
528
- "menu.form.cancel": "Cancel"
2243
+ "menu.form.saving": "Saving...",
2244
+ "menu.form.cancel": "Cancel",
2245
+ // Settings
2246
+ "menu.settings.title": "Menu settings"
529
2247
  };
530
2248
 
531
2249
  // src/locales/pt-BR.ts
532
2250
  var ptBR = {
533
2251
  "menu.title": "Card\xE1pio",
534
2252
  "menu.nav.manager": "Card\xE1pio",
535
- "menu.nav.categories": "Categorias",
536
- "menu.nav.items": "Itens",
537
- "menu.nav.modifiers": "Modificadores",
538
- "menu.manager.title": "Gerenciar Card\xE1pio",
539
- "menu.manager.subtitle": "Gerencie itens e categorias do card\xE1pio",
540
- "menu.manager.search": "Buscar no card\xE1pio...",
541
- "menu.manager.allCategories": "Todas as Categorias",
542
- "menu.manager.newItem": "Novo Item",
543
- "menu.manager.noItems": "Nenhum item no card\xE1pio",
544
- "menu.manager.noItemsDesc": "Adicione o primeiro item para come\xE7ar",
545
- "menu.manager.addFirst": "Adicionar primeiro item",
2253
+ // Manager
2254
+ "menu.manager.title": "Card\xE1pio",
2255
+ "menu.manager.subtitle": "Monte os card\xE1pios da casa, com se\xE7\xF5es e ordem",
2256
+ "menu.manager.search": "Buscar item no card\xE1pio...",
2257
+ "menu.manager.newMenu": "Novo card\xE1pio",
2258
+ "menu.manager.menuActions": "A\xE7\xF5es do card\xE1pio",
2259
+ "menu.manager.editMenu": "Editar card\xE1pio",
2260
+ "menu.manager.duplicateMenu": "Duplicar card\xE1pio",
2261
+ "menu.manager.activateMenu": "Ativar card\xE1pio",
2262
+ "menu.manager.deactivateMenu": "Desativar card\xE1pio",
2263
+ "menu.manager.deleteMenu": "Excluir card\xE1pio",
2264
+ "menu.manager.menuOff": "Desativado",
2265
+ "menu.manager.servingNow": "Servindo agora",
2266
+ "menu.manager.notServing": "Fora do hor\xE1rio",
2267
+ "menu.manager.addSection": "Nova se\xE7\xE3o",
2268
+ "menu.manager.newSectionName": "Nova se\xE7\xE3o",
2269
+ "menu.manager.editSection": "Ativar/desativar se\xE7\xE3o",
2270
+ "menu.manager.deleteSection": "Excluir se\xE7\xE3o",
2271
+ "menu.manager.sectionActions": "A\xE7\xF5es da se\xE7\xE3o",
2272
+ "menu.manager.sectionOff": "Se\xE7\xE3o desativada",
2273
+ "menu.manager.emptySection": "Nenhum item aqui ainda \u2014 clique para adicionar",
2274
+ "menu.manager.addItem": "Adicionar item",
2275
+ "menu.manager.itemActions": "A\xE7\xF5es do item",
2276
+ "menu.manager.itemsCount": "{{count}} itens",
2277
+ "menu.manager.edit": "Editar",
2278
+ "menu.manager.duplicate": "Duplicar",
2279
+ "menu.manager.markSoldOut": "Marcar como esgotado",
2280
+ "menu.manager.markAvailable": "Marcar como dispon\xEDvel",
2281
+ "menu.manager.feature": "Colocar em destaque",
2282
+ "menu.manager.unfeature": "Tirar do destaque",
2283
+ "menu.manager.removeFromMenu": "Tirar deste card\xE1pio",
2284
+ "menu.manager.deleteItem": "Excluir item",
2285
+ "menu.manager.dragSection": "Arrastar se\xE7\xE3o",
2286
+ "menu.manager.dragItem": "Arrastar item",
2287
+ "menu.manager.expand": "Expandir",
2288
+ "menu.manager.collapse": "Recolher",
546
2289
  "menu.manager.available": "Dispon\xEDvel",
547
2290
  "menu.manager.soldOut": "Esgotado",
548
2291
  "menu.manager.hidden": "Oculto",
549
2292
  "menu.manager.pos": "PDV",
550
2293
  "menu.manager.delivery": "Delivery",
551
2294
  "menu.manager.featured": "Destaque",
552
- "menu.manager.prepTime": "Tempo de preparo",
553
- "menu.manager.min": "min",
554
- "menu.manager.addCategory": "Adicionar Categoria",
555
- "menu.manager.addProduct": "Adicionar Produto",
556
- "menu.manager.item": "Item",
557
- "menu.manager.price": "Pre\xE7o",
558
- "menu.manager.deliveryPrice": "Delivery R$",
559
- "menu.manager.itemsCount": "{{count}} itens",
560
- "menu.manager.noResults": "Nenhuma categoria ou item encontrado.",
561
- "menu.manager.noResultsDesc": "Tente um termo diferente ou filtro de categoria.",
562
- "menu.itemForm.newTitle": "Novo Item do Card\xE1pio",
563
- "menu.itemForm.editTitle": "Editar Item do Card\xE1pio",
564
- "menu.itemForm.name": "Nome do item",
2295
+ "menu.manager.overridden": "pre\xE7o deste card\xE1pio",
2296
+ "menu.manager.overriddenHint": "Este item custa diferente neste card\xE1pio",
2297
+ "menu.manager.empty": "Seu card\xE1pio ainda est\xE1 vazio",
2298
+ "menu.manager.emptyHint": "Crie a primeira se\xE7\xE3o, ou mande fotos do card\xE1pio impresso e o assistente cadastra pra voc\xEA.",
2299
+ // Menu dialog
2300
+ "menu.menuDialog.newTitle": "Novo card\xE1pio",
2301
+ "menu.menuDialog.editTitle": "Editar card\xE1pio",
2302
+ "menu.menuDialog.name": "Nome",
2303
+ "menu.menuDialog.namePlaceholder": "Executivo, Fim de semana, Delivery...",
2304
+ "menu.menuDialog.description": "Descri\xE7\xE3o",
2305
+ "menu.menuDialog.descriptionPlaceholder": "Almo\xE7o de segunda a sexta",
2306
+ "menu.menuDialog.channels": "Onde aparece",
2307
+ "menu.menuDialog.active": "Card\xE1pio ativo",
2308
+ "menu.menuDialog.activeHint": "Desativado, ele some do sal\xE3o e do delivery sem ser apagado",
2309
+ "menu.channel.pos": "Sal\xE3o / PDV",
2310
+ "menu.channel.delivery": "Delivery",
2311
+ "menu.channel.digital": "Card\xE1pio digital",
2312
+ // Schedule
2313
+ "menu.schedule.always": "Todo dia, o dia inteiro",
2314
+ "menu.schedule.restrict": "S\xF3 em certos dias e hor\xE1rios",
2315
+ "menu.schedule.restrictHint": "\xC9 assim que nasce um executivo de segunda a sexta",
2316
+ "menu.schedule.days": "Dias",
2317
+ "menu.schedule.from": "Das",
2318
+ "menu.schedule.to": "At\xE9",
2319
+ "menu.schedule.overnightHint": "Se o fim for antes do come\xE7o, o card\xE1pio atravessa a meia-noite (22:00\u201302:00).",
2320
+ "menu.schedule.presetWeekdays": "Dias de semana",
2321
+ "menu.schedule.presetWeekend": "Fim de semana",
2322
+ "menu.schedule.presetEveryDay": "Todo dia",
2323
+ // Item form
2324
+ "menu.itemForm.newTitle": "Novo item",
2325
+ "menu.itemForm.editTitle": "Editar item",
2326
+ "menu.itemForm.name": "Nome",
2327
+ "menu.itemForm.namePlaceholder": "Picanha na chapa",
565
2328
  "menu.itemForm.description": "Descri\xE7\xE3o",
2329
+ "menu.itemForm.descriptionPlaceholder": "Serve 2 pessoas, acompanha farofa",
2330
+ "menu.itemForm.photo": "Foto",
2331
+ "menu.itemForm.photoHint": "Quadrada, boa luz. Aparece no card\xE1pio digital e no delivery.",
2332
+ "menu.itemForm.pricingGroup": "Pre\xE7os",
2333
+ "menu.itemForm.menuGroup": "No card\xE1pio",
2334
+ "menu.itemForm.posHint": "Aparece para quem atende no sal\xE3o",
2335
+ "menu.itemForm.deliveryHint": "Aparece para quem pede de fora",
2336
+ "menu.itemForm.featuredHint": "Sobe para o topo da se\xE7\xE3o",
566
2337
  "menu.itemForm.price": "Pre\xE7o",
567
2338
  "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
2339
  "menu.itemForm.deliveryPrice": "Pre\xE7o delivery",
576
- "menu.itemForm.isFeatured": "Item em destaque",
577
- "menu.itemForm.save": "Salvar",
578
- "menu.itemForm.cancel": "Cancelar",
579
- "menu.itemForm.saving": "Salvando...",
580
- "menu.categories.title": "Categorias",
581
- "menu.categories.newCategory": "Nova Categoria",
582
- "menu.categories.name": "Nome da categoria",
583
- "menu.settings.title": "Configura\xE7\xF5es do Card\xE1pio",
584
- "menu.settings.general": "Geral",
585
- "menu.settings.taxRate": "Taxa padr\xE3o",
586
- "menu.settings.currency": "Moeda",
587
- "menu.form.categoryName": "Nome da categoria",
588
- "menu.form.itemName": "Nome do item",
589
- "menu.form.description": "Descri\xE7\xE3o",
590
- "menu.form.category": "Categoria",
591
- "menu.form.uncategorized": "Sem categoria",
592
- "menu.form.optional": "opcional",
2340
+ "menu.itemForm.prepTime": "Preparo (min)",
2341
+ "menu.itemForm.section": "Se\xE7\xE3o",
2342
+ "menu.itemForm.status": "Situa\xE7\xE3o",
2343
+ "menu.itemForm.priceOverride": "Pre\xE7o s\xF3 neste card\xE1pio",
2344
+ "menu.itemForm.priceOverrideHint": "O mesmo prato pode custar menos no executivo. Em branco, vale o pre\xE7o do item.",
2345
+ "menu.itemForm.clearOverride": "Usar o pre\xE7o do item",
2346
+ // AI import
2347
+ "menu.import.action": "Inserir com IA",
2348
+ "menu.import.uploadCta": "Subir foto do card\xE1pio",
2349
+ "menu.import.uploadHint": "Uma foto por p\xE1gina, de frente e sem reflexo. Pode mandar v\xE1rias.",
2350
+ "menu.import.askPrompt": "Quero cadastrar meu card\xE1pio a partir de fotos.",
2351
+ "menu.import.askHint": "Descreva o card\xE1pio, ou pe\xE7a para come\xE7ar pelas fotos\u2026",
2352
+ "menu.import.askPersona": "Cadastro de card\xE1pio",
2353
+ "menu.import.askPitch": "Me manda as fotos do card\xE1pio que eu cadastro.",
2354
+ "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.",
2355
+ "menu.import.askSuggestion.0": "Vou mandar as fotos do card\xE1pio impresso",
2356
+ "menu.import.askSuggestion.1": "Cria um card\xE1pio executivo de segunda a sexta",
2357
+ "menu.import.askSuggestion.2": "Adiciona uma se\xE7\xE3o de sobremesas",
2358
+ "menu.import.createSection": "Criar se\xE7\xE3o nova",
2359
+ "menu.import.intoSection": "Somar em: {{name}}",
2360
+ "menu.import.willAppend": "entra em {{name}}",
2361
+ "menu.import.dropItem": "Descartar item",
2362
+ "menu.import.dropSection": "Descartar se\xE7\xE3o",
2363
+ "menu.import.draftCount": "{{items}} itens lidos em {{sections}} se\xE7\xF5es",
2364
+ "menu.import.draftHint": "Confira e ajuste aqui mesmo \u2014 nada foi gravado ainda.",
2365
+ "menu.import.draftMissingPrice": "{{count}} itens vieram sem pre\xE7o. Preencha antes de gravar.",
2366
+ "menu.import.discard": "Descartar",
2367
+ "menu.import.commit": "Gravar {{count}} itens",
2368
+ "menu.import.done": "{{items}} itens em {{sections}} se\xE7\xF5es cadastrados",
2369
+ "menu.import.someFailed": "{{count}} itens n\xE3o puderam ser criados",
2370
+ // Preview
2371
+ "menu.preview.action": "Ver como fica",
2372
+ "menu.preview.title": "Card\xE1pio",
2373
+ "menu.preview.notServingNow": "fora do hor\xE1rio agora",
2374
+ // Confirmations
2375
+ "menu.confirm.sectionTitle": "Excluir a se\xE7\xE3o {{name}}?",
2376
+ "menu.confirm.sectionDesc": "Os itens dela saem deste card\xE1pio, mas continuam cadastrados.",
2377
+ "menu.confirm.itemTitle": "Excluir o item {{name}}?",
2378
+ "menu.confirm.itemDesc": "Ele sai de todos os card\xE1pios em que aparece.",
2379
+ "menu.confirm.menuTitle": "Excluir o card\xE1pio {{name}}?",
2380
+ "menu.confirm.menuDesc": "As se\xE7\xF5es deste card\xE1pio somem. Os pratos continuam cadastrados.",
2381
+ "menu.confirm.delete": "Excluir",
2382
+ // Shared form bits
593
2383
  "menu.form.save": "Salvar",
594
- "menu.form.cancel": "Cancelar"
2384
+ "menu.form.saving": "Salvando...",
2385
+ "menu.form.cancel": "Cancelar",
2386
+ // Settings
2387
+ "menu.settings.title": "Configura\xE7\xF5es do Card\xE1pio"
595
2388
  };
596
2389
 
597
2390
  // src/locales/index.ts
@@ -613,7 +2406,7 @@ function menuMetadata(row) {
613
2406
  const raw = row.metadata?.[MENU_METADATA_KEY];
614
2407
  return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
615
2408
  }
616
- function rowToCategory(row, itemCount = 0) {
2409
+ function rowToCategory2(row, itemCount = 0) {
617
2410
  return {
618
2411
  id: row.id,
619
2412
  name: row.name,
@@ -628,7 +2421,7 @@ function rowToCategory(row, itemCount = 0) {
628
2421
  updatedAt: row.updated_at ?? row.created_at ?? nowIso()
629
2422
  };
630
2423
  }
631
- function rowToItem(row, categoryName) {
2424
+ function rowToItem2(row, categoryName) {
632
2425
  const metadata = menuMetadata(row);
633
2426
  const status = metadata.status ?? row.status ?? "available";
634
2427
  return {
@@ -782,7 +2575,7 @@ function createFayzMenuProvider(options = {}) {
782
2575
  }
783
2576
  async function getItems() {
784
2577
  const [rows, categoryMap] = await Promise.all([getItemsRaw(), getCategoryMap()]);
785
- return rows.map((row) => rowToItem(row, row.category_id ? categoryMap.get(row.category_id)?.name : void 0)).sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name));
2578
+ 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
2579
  }
787
2580
  return {
788
2581
  async getCategories() {
@@ -791,7 +2584,7 @@ function createFayzMenuProvider(options = {}) {
791
2584
  for (const item of items) {
792
2585
  if (item.category_id) counts.set(item.category_id, (counts.get(item.category_id) ?? 0) + 1);
793
2586
  }
794
- return categories.map((category) => rowToCategory(category, counts.get(category.id) ?? 0));
2587
+ return categories.map((category) => rowToCategory2(category, counts.get(category.id) ?? 0));
795
2588
  },
796
2589
  async getCategoryById(id) {
797
2590
  const response = await client.data.listRows({
@@ -800,7 +2593,7 @@ function createFayzMenuProvider(options = {}) {
800
2593
  filters: [{ column: "id", operator: "eq", value: id }],
801
2594
  limit: 1
802
2595
  });
803
- return response.rows[0] ? rowToCategory(response.rows[0]) : null;
2596
+ return response.rows[0] ? rowToCategory2(response.rows[0]) : null;
804
2597
  },
805
2598
  async createCategory(input) {
806
2599
  const row = await client.data.createRow({
@@ -808,7 +2601,7 @@ function createFayzMenuProvider(options = {}) {
808
2601
  table: categoriesTable,
809
2602
  row: categoryPayload(input, categoryKind)
810
2603
  });
811
- return rowToCategory(row);
2604
+ return rowToCategory2(row);
812
2605
  },
813
2606
  async updateCategory(id, data) {
814
2607
  const row = await client.data.updateRow({
@@ -824,7 +2617,7 @@ function createFayzMenuProvider(options = {}) {
824
2617
  is_active: data.isActive
825
2618
  }
826
2619
  });
827
- return rowToCategory(row);
2620
+ return rowToCategory2(row);
828
2621
  },
829
2622
  async deleteCategory(id) {
830
2623
  await client.data.updateRow({
@@ -851,7 +2644,7 @@ function createFayzMenuProvider(options = {}) {
851
2644
  if (!response.rows[0]) return null;
852
2645
  const categoryMap = await getCategoryMap();
853
2646
  const row = response.rows[0];
854
- return rowToItem(row, row.category_id ? categoryMap.get(row.category_id)?.name : void 0);
2647
+ return rowToItem2(row, row.category_id ? categoryMap.get(row.category_id)?.name : void 0);
855
2648
  },
856
2649
  async createMenuItem(input) {
857
2650
  const row = await client.data.createRow({
@@ -860,7 +2653,7 @@ function createFayzMenuProvider(options = {}) {
860
2653
  row: itemCreatePayload(input)
861
2654
  });
862
2655
  const categoryMap = await getCategoryMap();
863
- return rowToItem(row, row.category_id ? categoryMap.get(row.category_id)?.name : void 0);
2656
+ return rowToItem2(row, row.category_id ? categoryMap.get(row.category_id)?.name : void 0);
864
2657
  },
865
2658
  async updateMenuItem(id, data) {
866
2659
  const existing = await this.getMenuItemById(id);
@@ -871,7 +2664,7 @@ function createFayzMenuProvider(options = {}) {
871
2664
  row: itemUpdatePayload(data, existing ?? void 0)
872
2665
  });
873
2666
  const categoryMap = await getCategoryMap();
874
- return rowToItem(row, row.category_id ? categoryMap.get(row.category_id)?.name : void 0);
2667
+ return rowToItem2(row, row.category_id ? categoryMap.get(row.category_id)?.name : void 0);
875
2668
  },
876
2669
  async deleteMenuItem(id) {
877
2670
  await client.data.updateRow({
@@ -924,7 +2717,7 @@ function createFayzMenuProvider(options = {}) {
924
2717
  }
925
2718
 
926
2719
  // src/index.ts
927
- var MenuPage = React.lazy(() => import('./MenuPage-ZNKKQJKD.js').then((m) => ({ default: m.MenuPage })));
2720
+ var MenuPage = React.lazy(() => import('./MenuPage-2LLWRJGN.js').then((m) => ({ default: m.MenuPage })));
928
2721
  var DEFAULT_LABELS = {
929
2722
  pageTitle: "Menu",
930
2723
  menuManager: "Menu Manager",
@@ -945,7 +2738,10 @@ function resolveConfig(options) {
945
2738
  function createMenuPlugin(options) {
946
2739
  const config = resolveConfig(options);
947
2740
  registerTranslations(menuLocales);
948
- const provider = options?.dataProvider ?? createMockMenuProvider();
2741
+ const provider = options?.dataProvider ?? createSafeDataProvider(
2742
+ () => createSupabaseMenuProvider({ currencyCode: config.currency.code }),
2743
+ () => createMockMenuProvider()
2744
+ );
949
2745
  const store = createMenuStore(provider);
950
2746
  const PageComponent = () => React.createElement(
951
2747
  React.Suspense,
@@ -954,6 +2750,7 @@ function createMenuPlugin(options) {
954
2750
  );
955
2751
  return {
956
2752
  id: "menu",
2753
+ defaultAgentRole: "operations",
957
2754
  name: config.labels.pageTitle,
958
2755
  icon: "UtensilsCrossed",
959
2756
  version: "1.0.0",
@@ -965,9 +2762,18 @@ function createMenuPlugin(options) {
965
2762
  scaffolds: ["saas", "ecommerce"],
966
2763
  defaultEnabled: true,
967
2764
  dependencies: [],
2765
+ declaredFeatures: [
2766
+ { id: "menu", label: config.labels.pageTitle, group: config.labels.pageTitle }
2767
+ ],
968
2768
  declaredLimits: [
969
- { key: "menu_items", label: "Menu items", table: "menu_items" }
2769
+ { key: "menu_items", label: "Menu items", table: T.items }
970
2770
  ],
2771
+ migrations: MIGRATIONS.map((migration) => ({
2772
+ id: `menu-${migration.id}`,
2773
+ version: "1.0.0",
2774
+ sql: migration.sql,
2775
+ description: `plugin-menu ${migration.id}`
2776
+ })),
971
2777
  navigation: [
972
2778
  {
973
2779
  section: options?.navSection ?? "main",
@@ -1006,6 +2812,19 @@ function createMenuPlugin(options) {
1006
2812
  { label: "Which items are sold out right now?" }
1007
2813
  ]
1008
2814
  },
2815
+ {
2816
+ id: "menu.list-menus",
2817
+ name: "listMenus",
2818
+ description: "Lists the menus of the place (regular, executive lunch, weekend) with when each one is served.",
2819
+ icon: "UtensilsCrossed",
2820
+ mode: "read",
2821
+ category: "Menu",
2822
+ parameters: { type: "object", properties: {} },
2823
+ suggestions: [
2824
+ { label: "Quais card\xE1pios existem?" },
2825
+ { label: "Que card\xE1pio est\xE1 no ar agora?" }
2826
+ ]
2827
+ },
1009
2828
  {
1010
2829
  id: "menu.toggle-availability",
1011
2830
  name: "toggleMenuItemAvailability",
@@ -1033,7 +2852,6 @@ function createMenuPlugin(options) {
1033
2852
  component: (() => {
1034
2853
  const MenuSettingsTab = () => React.createElement(PluginSettingsPanel, {
1035
2854
  title: "Menu Settings",
1036
- subtitle: "Categories, allergens, and modifiers",
1037
2855
  registries: menuRegistries,
1038
2856
  routeBase: "/settings/menu"
1039
2857
  });