@fayz-ai/plugin-reservations 0.1.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 (44) hide show
  1. package/LICENSE +21 -0
  2. package/dist/ReservationsPage-5RF5SYZT.js +1157 -0
  3. package/dist/ReservationsPage-5RF5SYZT.js.map +1 -0
  4. package/dist/chunk-7KXGSUHT.js +335 -0
  5. package/dist/chunk-7KXGSUHT.js.map +1 -0
  6. package/dist/components/CapacityStrip.d.ts +25 -0
  7. package/dist/components/CapacityStrip.d.ts.map +1 -0
  8. package/dist/components/ReservationModal.d.ts +23 -0
  9. package/dist/components/ReservationModal.d.ts.map +1 -0
  10. package/dist/components/SeatDialog.d.ts +16 -0
  11. package/dist/components/SeatDialog.d.ts.map +1 -0
  12. package/dist/context.d.ts +49 -0
  13. package/dist/context.d.ts.map +1 -0
  14. package/dist/data/core-supabase.d.ts +32 -0
  15. package/dist/data/core-supabase.d.ts.map +1 -0
  16. package/dist/data/mock.d.ts +3 -0
  17. package/dist/data/mock.d.ts.map +1 -0
  18. package/dist/data/types.d.ts +37 -0
  19. package/dist/data/types.d.ts.map +1 -0
  20. package/dist/index.d.ts +43 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +870 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/locales/en.d.ts +2 -0
  25. package/dist/locales/en.d.ts.map +1 -0
  26. package/dist/locales/index.d.ts +2 -0
  27. package/dist/locales/index.d.ts.map +1 -0
  28. package/dist/locales/pt-BR.d.ts +2 -0
  29. package/dist/locales/pt-BR.d.ts.map +1 -0
  30. package/dist/migrations/index.d.ts +6 -0
  31. package/dist/migrations/index.d.ts.map +1 -0
  32. package/dist/store.d.ts +48 -0
  33. package/dist/store.d.ts.map +1 -0
  34. package/dist/types.d.ts +174 -0
  35. package/dist/types.d.ts.map +1 -0
  36. package/dist/views/CapacitySettingsView.d.ts +3 -0
  37. package/dist/views/CapacitySettingsView.d.ts.map +1 -0
  38. package/dist/views/ReservationsListView.d.ts +5 -0
  39. package/dist/views/ReservationsListView.d.ts.map +1 -0
  40. package/dist/views/ReservationsPage.d.ts +27 -0
  41. package/dist/views/ReservationsPage.d.ts.map +1 -0
  42. package/dist/views/ServiceDayView.d.ts +15 -0
  43. package/dist/views/ServiceDayView.d.ts.map +1 -0
  44. package/package.json +62 -0
package/dist/index.js ADDED
@@ -0,0 +1,870 @@
1
+ import { DEFAULT_SETTINGS } from './chunk-7KXGSUHT.js';
2
+ export { DEFAULT_SETTINGS, createCoreReservationsProvider } from './chunk-7KXGSUHT.js';
3
+ import React from 'react';
4
+ import { createStore } from 'zustand/vanilla';
5
+ import { toast } from 'sonner';
6
+
7
+ // src/data/mock.ts
8
+ var TENANT = "mock-tenant";
9
+ function today(offset = 0) {
10
+ const d = /* @__PURE__ */ new Date();
11
+ d.setDate(d.getDate() + offset);
12
+ const m = String(d.getMonth() + 1).padStart(2, "0");
13
+ const day = String(d.getDate()).padStart(2, "0");
14
+ return `${d.getFullYear()}-${m}-${day}`;
15
+ }
16
+ var ZONES = [
17
+ { id: "z-salao", name: "Sal\xE3o", color: "#3b82f6" },
18
+ { id: "z-varanda", name: "Varanda", color: "#22c55e" }
19
+ ];
20
+ var TABLES = [
21
+ { id: "t-1", number: 1, seats: 2, zoneId: "z-salao", isOccupied: false },
22
+ { id: "t-2", number: 2, seats: 4, zoneId: "z-salao", isOccupied: true },
23
+ { id: "t-3", number: 3, seats: 4, zoneId: "z-salao", isOccupied: false },
24
+ { id: "t-7", number: 7, seats: 6, zoneId: "z-varanda", isOccupied: false },
25
+ { id: "t-8", number: 8, seats: 8, zoneId: "z-varanda", isOccupied: false }
26
+ ];
27
+ var settings = { ...DEFAULT_SETTINGS };
28
+ var rules = [
29
+ { id: "r-almoco", zoneId: "z-salao", zoneName: "Sal\xE3o", capacity: 50, startTime: "12:00", endTime: "15:00", weekdays: [1, 2, 3, 4, 5], isActive: true, tenantId: TENANT },
30
+ { id: "r-jantar", zoneId: "z-salao", zoneName: "Sal\xE3o", capacity: 80, startTime: "19:00", endTime: "23:00", weekdays: [0, 1, 2, 3, 4, 5, 6], isActive: true, tenantId: TENANT },
31
+ { id: "r-varanda", zoneId: "z-varanda", zoneName: "Varanda", capacity: 24, startTime: "19:00", endTime: "23:00", weekdays: [4, 5, 6], isActive: true, tenantId: TENANT }
32
+ ];
33
+ var exceptions = [];
34
+ var reservations = [
35
+ { id: "res-1", guestName: "Marina Costa", phone: "(11) 98888-1010", zoneId: "z-salao", zoneName: "Sal\xE3o", reservedOn: today(), startTime: "20:00", partySize: 4, occasion: "birthday", status: "confirmed", tenantId: TENANT, createdAt: "", updatedAt: "" },
36
+ { id: "res-2", guestName: "Eduardo Lima", phone: "(11) 97777-2020", zoneId: "z-salao", zoneName: "Sal\xE3o", reservedOn: today(), startTime: "20:30", partySize: 2, status: "pending", tenantId: TENANT, createdAt: "", updatedAt: "" },
37
+ { id: "res-3", guestName: "Fam\xEDlia Tanaka", phone: "(11) 96666-3030", zoneId: "z-varanda", zoneName: "Varanda", reservedOn: today(), startTime: "19:30", partySize: 6, occasion: "business", status: "seated", tableId: "t-2", tableNumber: 2, orderReference: "PED-000042", seatedAt: (/* @__PURE__ */ new Date()).toISOString(), tenantId: TENANT, createdAt: "", updatedAt: "" },
38
+ { id: "res-4", guestName: "Paulo Rezende", zoneId: "z-salao", zoneName: "Sal\xE3o", reservedOn: today(1), startTime: "12:30", partySize: 3, status: "pending", tenantId: TENANT, createdAt: "", updatedAt: "" }
39
+ ];
40
+ var nextId = 5;
41
+ function weekdayOf(date) {
42
+ const [y, m, d] = date.split("-").map(Number);
43
+ return new Date(y, m - 1, d).getDay();
44
+ }
45
+ function createMockReservationsProvider() {
46
+ const provider = {
47
+ async getReservations(query) {
48
+ let rows = [...reservations];
49
+ if (query?.from) rows = rows.filter((r) => r.reservedOn >= query.from);
50
+ if (query?.to) rows = rows.filter((r) => r.reservedOn <= query.to);
51
+ if (query?.zoneId) rows = rows.filter((r) => r.zoneId === query.zoneId);
52
+ if (query?.status) {
53
+ const wanted = Array.isArray(query.status) ? query.status : [query.status];
54
+ rows = rows.filter((r) => wanted.includes(r.status));
55
+ }
56
+ if (query?.search) {
57
+ const term = query.search.toLowerCase();
58
+ rows = rows.filter((r) => r.guestName.toLowerCase().includes(term) || (r.phone ?? "").includes(term));
59
+ }
60
+ return rows.sort((a, b) => (a.reservedOn + a.startTime).localeCompare(b.reservedOn + b.startTime));
61
+ },
62
+ async getReservation(id) {
63
+ return reservations.find((r) => r.id === id) ?? null;
64
+ },
65
+ async createReservation(input) {
66
+ const zone = ZONES.find((z) => z.id === input.zoneId);
67
+ const row = {
68
+ id: `res-${nextId++}`,
69
+ ...input,
70
+ zoneName: zone?.name,
71
+ zoneColor: zone?.color,
72
+ tableNumber: TABLES.find((t) => t.id === input.tableId)?.number,
73
+ status: input.status ?? (settings.autoConfirm ? "confirmed" : "pending"),
74
+ tenantId: TENANT,
75
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
76
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
77
+ };
78
+ reservations = [...reservations, row];
79
+ return row;
80
+ },
81
+ async updateReservation(id, data) {
82
+ reservations = reservations.map((r) => r.id === id ? { ...r, ...data } : r);
83
+ return reservations.find((r) => r.id === id);
84
+ },
85
+ async setStatus(id, status) {
86
+ reservations = reservations.map((r) => r.id === id ? { ...r, status } : r);
87
+ return reservations.find((r) => r.id === id);
88
+ },
89
+ async deleteReservation(id) {
90
+ reservations = reservations.filter((r) => r.id !== id);
91
+ },
92
+ async seatReservation(input) {
93
+ const table = TABLES.find((t) => t.id === input.tableId);
94
+ reservations = reservations.map((r) => r.id === input.reservationId ? {
95
+ ...r,
96
+ status: "seated",
97
+ tableId: input.tableId,
98
+ tableNumber: table?.number,
99
+ partySize: input.guests ?? r.partySize,
100
+ orderReference: `PED-${String(100 + nextId).padStart(6, "0")}`,
101
+ seatedAt: (/* @__PURE__ */ new Date()).toISOString()
102
+ } : r);
103
+ if (table) table.isOccupied = true;
104
+ return reservations.find((r) => r.id === input.reservationId);
105
+ },
106
+ async getDayCapacity(date) {
107
+ const dow = weekdayOf(date);
108
+ const overridden = new Set(
109
+ exceptions.filter((e) => date >= e.startsOn && date <= e.endsOn).map((e) => e.zoneId)
110
+ );
111
+ const fromExceptions = exceptions.filter((e) => date >= e.startsOn && date <= e.endsOn && e.capacity > 0).map((e) => ({
112
+ zoneId: e.zoneId,
113
+ zoneName: e.zoneName,
114
+ zoneColor: ZONES.find((z) => z.id === e.zoneId)?.color,
115
+ startTime: e.startTime ?? "00:00",
116
+ endTime: e.endTime ?? "23:59",
117
+ capacity: e.capacity,
118
+ booked: 0,
119
+ remaining: e.capacity,
120
+ isException: true
121
+ }));
122
+ const fromRules = rules.filter((r) => r.isActive && r.weekdays.includes(dow) && !overridden.has(r.zoneId)).map((r) => ({
123
+ zoneId: r.zoneId,
124
+ zoneName: r.zoneName,
125
+ zoneColor: ZONES.find((z) => z.id === r.zoneId)?.color,
126
+ startTime: r.startTime,
127
+ endTime: r.endTime,
128
+ capacity: r.capacity,
129
+ booked: 0,
130
+ remaining: r.capacity,
131
+ isException: false
132
+ }));
133
+ return [...fromExceptions, ...fromRules].map((slot) => {
134
+ const booked = reservations.filter((r) => r.reservedOn === date && r.status !== "cancelled").filter((r) => !slot.zoneId || r.zoneId === slot.zoneId).filter((r) => r.startTime >= slot.startTime && r.startTime < slot.endTime).reduce((sum, r) => sum + r.partySize, 0);
135
+ return { ...slot, booked, remaining: Math.max(slot.capacity - booked, 0) };
136
+ }).sort((a, b) => a.startTime.localeCompare(b.startTime));
137
+ },
138
+ async getCapacityRules() {
139
+ return [...rules];
140
+ },
141
+ async saveCapacityRule(rule) {
142
+ const zone = ZONES.find((z) => z.id === rule.zoneId);
143
+ if (rule.id) {
144
+ rules = rules.map((r) => r.id === rule.id ? { ...r, ...rule, zoneName: zone?.name } : r);
145
+ return rules.find((r) => r.id === rule.id);
146
+ }
147
+ const row = {
148
+ ...rule,
149
+ id: `r-${nextId++}`,
150
+ zoneName: zone?.name,
151
+ weekdays: rule.weekdays ?? [0, 1, 2, 3, 4, 5, 6],
152
+ isActive: rule.isActive !== false,
153
+ tenantId: TENANT
154
+ };
155
+ rules = [...rules, row];
156
+ return row;
157
+ },
158
+ async deleteCapacityRule(id) {
159
+ rules = rules.filter((r) => r.id !== id);
160
+ },
161
+ async getCapacityExceptions() {
162
+ return [...exceptions];
163
+ },
164
+ async saveCapacityException(ex) {
165
+ const zone = ZONES.find((z) => z.id === ex.zoneId);
166
+ if (ex.id) {
167
+ exceptions = exceptions.map((e) => e.id === ex.id ? { ...e, ...ex, zoneName: zone?.name } : e);
168
+ return exceptions.find((e) => e.id === ex.id);
169
+ }
170
+ const row = {
171
+ ...ex,
172
+ id: `e-${nextId++}`,
173
+ zoneName: zone?.name,
174
+ endsOn: ex.endsOn ?? ex.startsOn,
175
+ capacity: ex.capacity ?? 0,
176
+ tenantId: TENANT
177
+ };
178
+ exceptions = [...exceptions, row];
179
+ return row;
180
+ },
181
+ async deleteCapacityException(id) {
182
+ exceptions = exceptions.filter((e) => e.id !== id);
183
+ },
184
+ async getSettings() {
185
+ return { ...settings };
186
+ },
187
+ async saveSettings(next) {
188
+ settings = { ...next };
189
+ return settings;
190
+ },
191
+ async getZones() {
192
+ return [...ZONES];
193
+ },
194
+ async getTables(zoneId) {
195
+ return TABLES.filter((t) => !zoneId || t.zoneId === zoneId);
196
+ },
197
+ async getSummary(date) {
198
+ const rows = reservations.filter((r) => r.reservedOn === date);
199
+ const count = (s) => rows.filter((r) => r.status === s).length;
200
+ return {
201
+ total: rows.length,
202
+ pending: count("pending"),
203
+ confirmed: count("confirmed"),
204
+ seated: count("seated"),
205
+ noShow: count("no_show"),
206
+ cancelled: count("cancelled"),
207
+ expectedGuests: rows.filter((r) => !["cancelled", "no_show"].includes(r.status)).reduce((sum, r) => sum + r.partySize, 0)
208
+ };
209
+ }
210
+ };
211
+ return provider;
212
+ }
213
+ function todayISO() {
214
+ const d = /* @__PURE__ */ new Date();
215
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
216
+ }
217
+ function createReservationsStore(provider) {
218
+ return createStore((set, get) => ({
219
+ selectedDate: todayISO(),
220
+ slots: [],
221
+ slotsLoading: false,
222
+ slotsError: null,
223
+ dayReservations: [],
224
+ dayLoading: false,
225
+ reservations: [],
226
+ listLoading: false,
227
+ summary: null,
228
+ rules: [],
229
+ exceptions: [],
230
+ settings: null,
231
+ configLoading: false,
232
+ zones: [],
233
+ tables: [],
234
+ selectedId: null,
235
+ setSelectedDate(date) {
236
+ set({ selectedDate: date });
237
+ void get().fetchDay(date);
238
+ },
239
+ select(id) {
240
+ set({ selectedId: id });
241
+ },
242
+ async fetchDay(date) {
243
+ const day = date ?? get().selectedDate;
244
+ set({ slotsLoading: true, dayLoading: true, slotsError: null });
245
+ const [slots, rows, summary] = await Promise.allSettled([
246
+ provider.getDayCapacity(day),
247
+ provider.getReservations({ from: day, to: day }),
248
+ provider.getSummary(day)
249
+ ]);
250
+ set({
251
+ slots: slots.status === "fulfilled" ? slots.value : [],
252
+ slotsError: slots.status === "rejected" ? String(slots.reason?.message ?? slots.reason) : null,
253
+ slotsLoading: false,
254
+ dayReservations: rows.status === "fulfilled" ? rows.value : [],
255
+ dayLoading: false,
256
+ summary: summary.status === "fulfilled" ? summary.value : null
257
+ });
258
+ },
259
+ async fetchRange(from, to, query) {
260
+ set({ listLoading: true });
261
+ try {
262
+ set({ reservations: await provider.getReservations({ ...query, from, to }) });
263
+ } catch (e) {
264
+ toast.error(e?.message ?? "N\xE3o foi poss\xEDvel carregar as reservas");
265
+ } finally {
266
+ set({ listLoading: false });
267
+ }
268
+ },
269
+ async fetchConfig() {
270
+ set({ configLoading: true });
271
+ const [rules2, exceptions2, settings2] = await Promise.allSettled([
272
+ provider.getCapacityRules(),
273
+ provider.getCapacityExceptions(),
274
+ provider.getSettings()
275
+ ]);
276
+ set({
277
+ rules: rules2.status === "fulfilled" ? rules2.value : [],
278
+ exceptions: exceptions2.status === "fulfilled" ? exceptions2.value : [],
279
+ settings: settings2.status === "fulfilled" ? settings2.value : null,
280
+ configLoading: false
281
+ });
282
+ },
283
+ async fetchSalao() {
284
+ const [zones, tables] = await Promise.allSettled([provider.getZones(), provider.getTables()]);
285
+ set({
286
+ zones: zones.status === "fulfilled" ? zones.value : [],
287
+ tables: tables.status === "fulfilled" ? tables.value : []
288
+ });
289
+ },
290
+ async create(input) {
291
+ try {
292
+ const row = await provider.createReservation(input);
293
+ toast.success("Reserva registrada");
294
+ await get().fetchDay(input.reservedOn);
295
+ return row;
296
+ } catch (e) {
297
+ toast.error(e?.message ?? "N\xE3o foi poss\xEDvel registrar a reserva");
298
+ return null;
299
+ }
300
+ },
301
+ async update(id, data) {
302
+ try {
303
+ await provider.updateReservation(id, data);
304
+ toast.success("Reserva atualizada");
305
+ await get().fetchDay();
306
+ } catch (e) {
307
+ toast.error(e?.message ?? "N\xE3o foi poss\xEDvel atualizar a reserva");
308
+ }
309
+ },
310
+ async setStatus(id, status) {
311
+ try {
312
+ await provider.setStatus(id, status);
313
+ await get().fetchDay();
314
+ } catch (e) {
315
+ toast.error(e?.message ?? "N\xE3o foi poss\xEDvel mudar o estado");
316
+ }
317
+ },
318
+ async seat(reservationId, tableId, guests) {
319
+ try {
320
+ const row = await provider.seatReservation({ reservationId, tableId, guests });
321
+ toast.success(row.orderReference ? `Mesa ${row.tableNumber} aberta \u2014 comanda ${row.orderReference}` : `Mesa ${row.tableNumber} ocupada`);
322
+ await Promise.all([get().fetchDay(), get().fetchSalao()]);
323
+ } catch (e) {
324
+ toast.error(e?.message ?? "N\xE3o foi poss\xEDvel acomodar");
325
+ }
326
+ },
327
+ async remove(id) {
328
+ try {
329
+ await provider.deleteReservation(id);
330
+ toast.success("Reserva exclu\xEDda");
331
+ set({ selectedId: null });
332
+ await get().fetchDay();
333
+ } catch (e) {
334
+ toast.error(e?.message ?? "N\xE3o foi poss\xEDvel excluir");
335
+ }
336
+ },
337
+ async saveRule(rule) {
338
+ try {
339
+ await provider.saveCapacityRule(rule);
340
+ toast.success("Regra salva");
341
+ await Promise.all([get().fetchConfig(), get().fetchDay()]);
342
+ } catch (e) {
343
+ toast.error(e?.message ?? "N\xE3o foi poss\xEDvel salvar a regra");
344
+ }
345
+ },
346
+ async deleteRule(id) {
347
+ try {
348
+ await provider.deleteCapacityRule(id);
349
+ await Promise.all([get().fetchConfig(), get().fetchDay()]);
350
+ } catch (e) {
351
+ toast.error(e?.message ?? "N\xE3o foi poss\xEDvel excluir a regra");
352
+ }
353
+ },
354
+ async saveException(ex) {
355
+ try {
356
+ await provider.saveCapacityException(ex);
357
+ toast.success("Exce\xE7\xE3o salva");
358
+ await Promise.all([get().fetchConfig(), get().fetchDay()]);
359
+ } catch (e) {
360
+ toast.error(e?.message ?? "N\xE3o foi poss\xEDvel salvar a exce\xE7\xE3o");
361
+ }
362
+ },
363
+ async deleteException(id) {
364
+ try {
365
+ await provider.deleteCapacityException(id);
366
+ await Promise.all([get().fetchConfig(), get().fetchDay()]);
367
+ } catch (e) {
368
+ toast.error(e?.message ?? "N\xE3o foi poss\xEDvel excluir a exce\xE7\xE3o");
369
+ }
370
+ },
371
+ async saveSettings(settings2) {
372
+ try {
373
+ set({ settings: await provider.saveSettings(settings2) });
374
+ toast.success("Configura\xE7\xF5es salvas");
375
+ } catch (e) {
376
+ toast.error(e?.message ?? "N\xE3o foi poss\xEDvel salvar");
377
+ }
378
+ }
379
+ }));
380
+ }
381
+
382
+ // src/locales/en.ts
383
+ var en = {
384
+ "reservations.page.title": "Reservations",
385
+ "reservations.page.subtitle": "Tables held for later",
386
+ "reservations.action.new": "New reservation",
387
+ "reservations.status.pending": "Pending",
388
+ "reservations.status.confirmed": "Confirmed",
389
+ "reservations.status.seated": "Seated",
390
+ "reservations.status.no_show": "No show",
391
+ "reservations.status.cancelled": "Cancelled"
392
+ };
393
+
394
+ // src/locales/pt-BR.ts
395
+ var ptBR = {
396
+ "reservations.page.title": "Reservas",
397
+ "reservations.page.subtitle": "Mesas guardadas por hor\xE1rio",
398
+ "reservations.action.new": "Nova reserva",
399
+ "reservations.status.pending": "Pendente",
400
+ "reservations.status.confirmed": "Confirmada",
401
+ "reservations.status.seated": "Acomodada",
402
+ "reservations.status.no_show": "N\xE3o compareceu",
403
+ "reservations.status.cancelled": "Cancelada"
404
+ };
405
+
406
+ // src/locales/index.ts
407
+ var reservationsLocales = { en, "pt-BR": ptBR };
408
+
409
+ // src/types.ts
410
+ var RESERVATION_STATUSES = [
411
+ "pending",
412
+ "confirmed",
413
+ "seated",
414
+ "no_show",
415
+ "cancelled"
416
+ ];
417
+ var CAPACITY_MODES = ["fixed_duration", "per_slot_duration", "no_duration"];
418
+ var OCCASIONS = ["birthday", "date", "business", "wedding", "graduation", "other"];
419
+
420
+ // src/migrations/index.ts
421
+ var MIGRATION_001_RESERVATIONS = `-- plugin-reservations \u2014 a mesa guardada para mais tarde.
422
+ --
423
+ -- WHY THIS IS NOT THE AGENDA. \`plugin-agenda\` schedules a PERSON's time: a
424
+ -- column per professional, services with durations, an assignee. A restaurant
425
+ -- reservation has neither professional nor service \u2014 it spends the SEATS OF A
426
+ -- ROOM inside a time window. resto-saas shipped \`createAgendaPlugin({
427
+ -- bookingKind: 'reservation' })\` with the labels swapped, and the screen came
428
+ -- out with nine staff columns over the word "Reservas". A booking kind cannot
429
+ -- change which resource is scarce.
430
+ --
431
+ -- The V1 (~/dev/beautyplace/docs/v1-guide/restaurante/reservas) models this
432
+ -- correctly and we keep its model \u2014 local \xD7 faixa \xD7 dia da semana, with
433
+ -- exceptions that override the weekly grid \u2014 with three deliberate changes,
434
+ -- each written where it happens below.
435
+ --
436
+ -- Idempotent.
437
+
438
+ -- ---------------------------------------------------------------------------
439
+ -- Policy \u2014 one row per tenant.
440
+ -- ---------------------------------------------------------------------------
441
+ create table if not exists public.plg_reservations_settings (
442
+ tenant_id uuid primary key references public.tenants(id) on delete cascade,
443
+ -- V1's three modes, same words. \`no_duration\` is the V1 default and the one
444
+ -- its own legend explains best: "a capacidade \xE9 o teto de pessoas que pode
445
+ -- iniciar dentro da faixa, sem rolagem entre faixas" \u2014 the reservation spends
446
+ -- the slot it STARTS in.
447
+ capacity_mode text not null default 'no_duration'
448
+ check (capacity_mode in ('fixed_duration','per_slot_duration','no_duration')),
449
+ default_duration_minutes integer not null default 120 check (default_duration_minutes > 0),
450
+ -- CHANGE 2 of 3: the V1 asks for minimum notice in MINUTES (1440) and maximum
451
+ -- in DAYS (90), in the same pair of fields. Both are hours/days here and the
452
+ -- UI writes the equivalent out beside them.
453
+ min_advance_hours integer not null default 24 check (min_advance_hours >= 0),
454
+ max_advance_days integer not null default 90 check (max_advance_days > 0),
455
+ auto_confirm boolean not null default false,
456
+ require_phone boolean not null default true,
457
+ require_email boolean not null default false,
458
+ created_at timestamptz not null default now(),
459
+ updated_at timestamptz not null default now()
460
+ );
461
+
462
+ -- ---------------------------------------------------------------------------
463
+ -- The weekly grid \u2014 one row per zone \xD7 window \xD7 set of weekdays.
464
+ -- ---------------------------------------------------------------------------
465
+ create table if not exists public.plg_reservations_capacity_rules (
466
+ id uuid primary key default gen_random_uuid(),
467
+ tenant_id uuid not null references public.tenants(id) on delete cascade,
468
+ -- CHANGE 1 of 3: capacity is per PRA\xC7A, not per a cadastro of its own.
469
+ --
470
+ -- The V1 splits these: capacity lives on \`service_locations\` (Cadastros \u2192
471
+ -- Locais Operacionais) while the mesa lives on \`restaurant_sections\` (the
472
+ -- Sal\xE3o screen). Two cadastros with similar names, and its own document says
473
+ -- "nada na tela avisa" \u2014 whoever registered the pra\xE7as does not find those
474
+ -- names when configuring the lota\xE7\xE3o. We already own zones with mesas inside
475
+ -- them; a third cadastro would re-import the confusion and buy nothing.
476
+ --
477
+ -- Nullable = the house as a whole, for a restaurant with one room.
478
+ zone_id uuid references public.plg_tables_zones(id) on delete cascade,
479
+ capacity integer not null check (capacity > 0),
480
+ start_time time not null,
481
+ end_time time not null,
482
+ -- 0=Sunday..6=Saturday, matching Date#getDay and plugin-menu's availability.
483
+ weekdays smallint[] not null default '{0,1,2,3,4,5,6}',
484
+ -- Only read in \`per_slot_duration\` mode; null falls back to the default.
485
+ duration_minutes integer check (duration_minutes is null or duration_minutes > 0),
486
+ is_active boolean not null default true,
487
+ created_at timestamptz not null default now(),
488
+ updated_at timestamptz not null default now(),
489
+ check (end_time > start_time)
490
+ );
491
+
492
+ -- ---------------------------------------------------------------------------
493
+ -- Exceptions \u2014 "Exce\xE7\xF5es sobrep\xF5em a grade semanal para o(s) dia(s) cobertos
494
+ -- no mesmo local." (V1, literal). A row with capacity 0 closes the zone.
495
+ -- ---------------------------------------------------------------------------
496
+ create table if not exists public.plg_reservations_capacity_exceptions (
497
+ id uuid primary key default gen_random_uuid(),
498
+ tenant_id uuid not null references public.tenants(id) on delete cascade,
499
+ zone_id uuid references public.plg_tables_zones(id) on delete cascade,
500
+ starts_on date not null,
501
+ ends_on date not null,
502
+ -- Null start/end = the whole day. Zero capacity = closed.
503
+ start_time time,
504
+ end_time time,
505
+ capacity integer not null default 0 check (capacity >= 0),
506
+ reason text,
507
+ created_at timestamptz not null default now(),
508
+ updated_at timestamptz not null default now(),
509
+ check (ends_on >= starts_on)
510
+ );
511
+
512
+ -- ---------------------------------------------------------------------------
513
+ -- The reservations.
514
+ -- ---------------------------------------------------------------------------
515
+ create table if not exists public.plg_reservations (
516
+ id uuid primary key default gen_random_uuid(),
517
+ tenant_id uuid not null references public.tenants(id) on delete cascade,
518
+ person_id uuid references public.people(id) on delete set null,
519
+ -- Denormalised on purpose: a reservation taken over the phone from someone
520
+ -- who is not a client yet still has a name and a number to call back.
521
+ guest_name text not null,
522
+ phone text,
523
+ email text,
524
+ zone_id uuid references public.plg_tables_zones(id) on delete set null,
525
+ table_id uuid references public.plg_tables_tables(id) on delete set null,
526
+ reserved_on date not null,
527
+ start_time time not null,
528
+ end_time time,
529
+ party_size integer not null check (party_size > 0),
530
+ occasion text,
531
+ notes text,
532
+ -- V1's five states, same words: Pendente \xB7 Confirmada \xB7 Acomodada \xB7
533
+ -- N\xE3o compareceu \xB7 Cancelada.
534
+ status text not null default 'pending'
535
+ check (status in ('pending','confirmed','seated','no_show','cancelled')),
536
+ -- CHANGE 3 of 3: \`seated\` opens the mesa.
537
+ --
538
+ -- In the V1, reservation and comanda are separate universes \u2014 marking
539
+ -- Acomodada creates no comanda at all, and the night's money never knows a
540
+ -- reservation happened. Here \`seated\` is the moment the party becomes a
541
+ -- comanda, and this column is the join. Written by the app, not a trigger:
542
+ -- opening a comanda is the tables plugin's job and it numbers the order.
543
+ order_id uuid references public.orders(id) on delete set null,
544
+ seated_at timestamptz,
545
+ created_at timestamptz not null default now(),
546
+ updated_at timestamptz not null default now()
547
+ );
548
+
549
+ create index if not exists plg_reservations_day_idx
550
+ on public.plg_reservations (tenant_id, reserved_on, start_time);
551
+ create index if not exists plg_reservations_zone_day_idx
552
+ on public.plg_reservations (tenant_id, zone_id, reserved_on);
553
+ create index if not exists plg_reservations_rules_zone_idx
554
+ on public.plg_reservations_capacity_rules (tenant_id, zone_id);
555
+ create index if not exists plg_reservations_exceptions_day_idx
556
+ on public.plg_reservations_capacity_exceptions (tenant_id, starts_on, ends_on);
557
+
558
+ do $$ begin
559
+ create trigger plg_reservations_settings_updated_at before update on public.plg_reservations_settings
560
+ for each row execute function public.handle_updated_at();
561
+ exception when duplicate_object then null; end $$;
562
+ do $$ begin
563
+ create trigger plg_reservations_rules_updated_at before update on public.plg_reservations_capacity_rules
564
+ for each row execute function public.handle_updated_at();
565
+ exception when duplicate_object then null; end $$;
566
+ do $$ begin
567
+ create trigger plg_reservations_exceptions_updated_at before update on public.plg_reservations_capacity_exceptions
568
+ for each row execute function public.handle_updated_at();
569
+ exception when duplicate_object then null; end $$;
570
+ do $$ begin
571
+ create trigger plg_reservations_updated_at before update on public.plg_reservations
572
+ for each row execute function public.handle_updated_at();
573
+ exception when duplicate_object then null; end $$;
574
+
575
+ -- ---------------------------------------------------------------------------
576
+ -- RLS \u2014 the ACTIVE account, never the union.
577
+ --
578
+ -- \`user_tenant_ids()\` honours only a signed JWT claim; switching workspace
579
+ -- happens through the \`x-fayz-tenant\` header, so it does not see the switch and
580
+ -- answers with the UNION of the caller's accounts. plugin-tables learned this
581
+ -- the hard way \u2014 Artorius drew Papa L\xE9guas' mesas. \`app.current_tenant_id()\`
582
+ -- reads the header and validates it against app.memberships: it chooses among
583
+ -- accounts you already belong to and can never widen.
584
+ -- ---------------------------------------------------------------------------
585
+ alter table public.plg_reservations_settings enable row level security;
586
+ alter table public.plg_reservations_capacity_rules enable row level security;
587
+ alter table public.plg_reservations_capacity_exceptions enable row level security;
588
+ alter table public.plg_reservations enable row level security;
589
+
590
+ do $$
591
+ declare t text;
592
+ begin
593
+ foreach t in array array['plg_reservations_settings','plg_reservations_capacity_rules',
594
+ 'plg_reservations_capacity_exceptions','plg_reservations'] loop
595
+ execute format('drop policy if exists %I on public.%I', t || '_select', t);
596
+ execute format('drop policy if exists %I on public.%I', t || '_insert', t);
597
+ execute format('drop policy if exists %I on public.%I', t || '_update', t);
598
+ execute format('drop policy if exists %I on public.%I', t || '_delete', t);
599
+ execute format('create policy %I on public.%I for select using (tenant_id = (select app.current_tenant_id()))', t || '_select', t);
600
+ execute format('create policy %I on public.%I for insert with check (tenant_id = (select app.current_tenant_id()))', t || '_insert', t);
601
+ execute format('create policy %I on public.%I for update using (tenant_id = (select app.current_tenant_id()))', t || '_update', t);
602
+ execute format('create policy %I on public.%I for delete using (tenant_id = (select app.current_tenant_id()))', t || '_delete', t);
603
+ execute format('grant select, insert, update, delete on public.%I to authenticated', t);
604
+ end loop;
605
+ end $$;
606
+
607
+ -- ---------------------------------------------------------------------------
608
+ -- The reservation with the names already resolved. The list and the calendar
609
+ -- read this, so nobody joins four tables in TypeScript to render a row.
610
+ -- ---------------------------------------------------------------------------
611
+ create or replace view public.v_reservations with (security_invoker = true) as
612
+ select
613
+ r.id, r.tenant_id, r.person_id, r.guest_name, r.phone, r.email,
614
+ r.zone_id, z.name as zone_name, z.color as zone_color,
615
+ r.table_id, t.number as table_number,
616
+ r.reserved_on, r.start_time, r.end_time, r.party_size,
617
+ r.occasion, r.notes, r.status, r.order_id, r.seated_at,
618
+ o.reference_number as order_reference,
619
+ r.created_at, r.updated_at
620
+ from public.plg_reservations r
621
+ left join public.plg_tables_zones z on z.id = r.zone_id
622
+ left join public.plg_tables_tables t on t.id = r.table_id
623
+ left join public.orders o on o.id = r.order_id;
624
+
625
+ grant select on public.v_reservations to authenticated;
626
+
627
+ -- ---------------------------------------------------------------------------
628
+ -- The slots of one day, with what is left in each.
629
+ --
630
+ -- THIS IS THE FUNCTION THE V1 NEVER GOT RIGHT. With three rules covering all
631
+ -- seven weekdays, its "Capacidade do dia" answered "Nenhuma grade configurada"
632
+ -- on every date tested (V1-041), and its reservation form never called the
633
+ -- availability function at all \u2014 the lota\xE7\xE3o check, if it existed, did not
634
+ -- happen while anyone was deciding. Both are the reason this screen exists.
635
+ --
636
+ -- Returns one row per zone \xD7 window that applies to \`p_date\`, with the seats
637
+ -- already spent by non-cancelled reservations STARTING inside that window \u2014
638
+ -- which is what "sem rolagem entre faixas" means in the V1's own legend.
639
+ --
640
+ -- Exceptions OVERRIDE, they do not add: a zone with an exception covering the
641
+ -- date returns the exception's windows and none of the weekly ones. A capacity
642
+ -- of 0 therefore closes the zone for the day without deleting the grid.
643
+ -- ---------------------------------------------------------------------------
644
+ create or replace function public.reservations_day_capacity(p_date date)
645
+ returns table (
646
+ zone_id uuid,
647
+ zone_name text,
648
+ zone_color text,
649
+ start_time time,
650
+ end_time time,
651
+ capacity integer,
652
+ booked integer,
653
+ remaining integer,
654
+ is_exception boolean
655
+ )
656
+ language sql
657
+ stable
658
+ security invoker
659
+ set search_path = public
660
+ as $$
661
+ with tenant as (select app.current_tenant_id() as id),
662
+ -- Which zones had their day overridden. Checked per zone, so a holiday in the
663
+ -- Varanda does not silence the grid of the Sal\xE3o.
664
+ overridden as (
665
+ select distinct e.zone_id
666
+ from public.plg_reservations_capacity_exceptions e, tenant
667
+ where e.tenant_id = tenant.id
668
+ and p_date between e.starts_on and e.ends_on
669
+ ),
670
+ slots as (
671
+ select e.zone_id, e.start_time, e.end_time, e.capacity, true as is_exception
672
+ from public.plg_reservations_capacity_exceptions e, tenant
673
+ where e.tenant_id = tenant.id
674
+ and p_date between e.starts_on and e.ends_on
675
+ and e.capacity > 0
676
+ -- A whole-day exception still has to draw SOMETHING, so it borrows the
677
+ -- house's widest window rather than a made-up one.
678
+ and coalesce(e.start_time, time '00:00') < coalesce(e.end_time, time '23:59')
679
+ union all
680
+ select r.zone_id, r.start_time, r.end_time, r.capacity, false
681
+ from public.plg_reservations_capacity_rules r, tenant
682
+ where r.tenant_id = tenant.id
683
+ and r.is_active
684
+ -- extract(dow) is 0=Sunday..6=Saturday, the same axis \`weekdays\` uses.
685
+ and extract(dow from p_date)::smallint = any (r.weekdays)
686
+ and (r.zone_id is null or r.zone_id not in (select o.zone_id from overridden o where o.zone_id is not null))
687
+ )
688
+ select
689
+ s.zone_id,
690
+ z.name as zone_name,
691
+ z.color as zone_color,
692
+ coalesce(s.start_time, time '00:00') as start_time,
693
+ coalesce(s.end_time, time '23:59') as end_time,
694
+ s.capacity,
695
+ coalesce(b.booked, 0)::integer as booked,
696
+ greatest(s.capacity - coalesce(b.booked, 0), 0)::integer as remaining,
697
+ s.is_exception
698
+ from slots s
699
+ left join public.plg_tables_zones z on z.id = s.zone_id
700
+ left join lateral (
701
+ select sum(v.party_size)::integer as booked
702
+ from public.plg_reservations v, tenant
703
+ where v.tenant_id = tenant.id
704
+ and v.reserved_on = p_date
705
+ and v.status <> 'cancelled'
706
+ and (s.zone_id is null or v.zone_id is not distinct from s.zone_id)
707
+ -- The reservation spends the window it STARTS in.
708
+ and v.start_time >= coalesce(s.start_time, time '00:00')
709
+ and v.start_time < coalesce(s.end_time, time '23:59')
710
+ ) b on true
711
+ order by coalesce(s.start_time, time '00:00'), z.name nulls first;
712
+ $$;
713
+
714
+ grant execute on function public.reservations_day_capacity(date) to authenticated;
715
+ `;
716
+ var MIGRATIONS = [
717
+ { id: "001_reservations", sql: MIGRATION_001_RESERVATIONS }
718
+ ];
719
+
720
+ // src/index.ts
721
+ var ReservationsPage = React.lazy(() => import('./ReservationsPage-5RF5SYZT.js').then((m) => ({ default: m.ReservationsPage })));
722
+ var ReservationsSettingsPage = React.lazy(() => import('./ReservationsPage-5RF5SYZT.js').then((m) => ({ default: m.ReservationsSettingsPage })));
723
+ var DEFAULT_LABELS = {
724
+ pageTitle: "Reservas",
725
+ pageSubtitle: "Mesas guardadas por hor\xE1rio",
726
+ newReservation: "Nova reserva"
727
+ };
728
+ var DEFAULT_STATUSES = [
729
+ { value: "pending", label: "Pendente", color: "#f59e0b" },
730
+ { value: "confirmed", label: "Confirmada", color: "#3b82f6" },
731
+ { value: "seated", label: "Acomodada", color: "#22c55e" },
732
+ { value: "no_show", label: "N\xE3o compareceu", color: "#ef4444" },
733
+ { value: "cancelled", label: "Cancelada", color: "#6b7280" }
734
+ ];
735
+ var DEFAULT_OCCASIONS = [
736
+ { value: "birthday", label: "Anivers\xE1rio" },
737
+ { value: "date", label: "Namoro" },
738
+ { value: "business", label: "Neg\xF3cios" },
739
+ { value: "wedding", label: "Casamento" },
740
+ { value: "graduation", label: "Formatura" },
741
+ { value: "other", label: "Outro" }
742
+ ];
743
+ function resolveConfig(options) {
744
+ return {
745
+ labels: { ...DEFAULT_LABELS, ...options?.labels },
746
+ statuses: options?.statuses ?? DEFAULT_STATUSES,
747
+ occasions: options?.occasions ?? DEFAULT_OCCASIONS,
748
+ dayWindow: options?.dayWindow ?? { start: "11:00", end: "23:59" },
749
+ clientKind: options?.clientKind ?? "customer",
750
+ contactLookup: options?.contactLookup,
751
+ clientEntityDef: options?.clientEntityDef
752
+ };
753
+ }
754
+ function createReservationsPlugin(options) {
755
+ const config = resolveConfig(options);
756
+ const provider = options?.dataProvider ?? createMockReservationsProvider();
757
+ const store = createReservationsStore(provider);
758
+ const PageComponent = () => React.createElement(
759
+ React.Suspense,
760
+ { fallback: null },
761
+ React.createElement(ReservationsPage, { config, provider, store })
762
+ );
763
+ const SettingsComponent = () => React.createElement(
764
+ React.Suspense,
765
+ { fallback: null },
766
+ React.createElement(ReservationsSettingsPage, { config, provider, store })
767
+ );
768
+ return {
769
+ id: "reservations",
770
+ defaultAgentRole: "operations",
771
+ name: config.labels.pageTitle,
772
+ icon: "CalendarClock",
773
+ version: "1.0.0",
774
+ scope: options?.scope ?? "vertical",
775
+ verticalId: options?.verticalId,
776
+ defaultEnabled: true,
777
+ // Dependência DURA, e o comentário que estava aqui dizia o contrário.
778
+ //
779
+ // A intenção era boa — "uma casa sem planta de salão ainda aceita reservas,
780
+ // numa grade da casa inteira; as praças só afinam a grade" — mas o schema
781
+ // nunca a sustentou: `plg_reservations_capacity`, `_capacity_exceptions` e
782
+ // `plg_reservations` têm FK para `plg_tables_zones`/`_tables`, e as views
783
+ // fazem join nelas. Sem o plugin de mesas instalado a migração não aplica,
784
+ // então a promessa de opcionalidade falhava no primeiro `apply`, não numa
785
+ // tela vazia.
786
+ //
787
+ // Declarar é o que torna a ordem da corrente (ver packages/db/chain.json)
788
+ // uma consequência do manifesto em vez de uma coincidência de nomes.
789
+ // Tornar a dependência de fato opcional é outro trabalho: exigiria trocar
790
+ // as FKs por um evento `zone.deleted` do dono e as views por joins
791
+ // tolerantes — e é isso, não um comentário, que reabriria a porta.
792
+ dependencies: ["tables"],
793
+ navigation: [
794
+ {
795
+ section: options?.navSection ?? "main",
796
+ position: options?.navPosition ?? 13,
797
+ label: config.labels.pageTitle,
798
+ route: "/reservations",
799
+ icon: "CalendarClock",
800
+ permission: { feature: "reservations", action: "read" }
801
+ }
802
+ ],
803
+ routes: [
804
+ {
805
+ path: "/reservations",
806
+ component: PageComponent,
807
+ permission: { feature: "reservations", action: "read" }
808
+ }
809
+ ],
810
+ widgets: [],
811
+ aiTools: [
812
+ {
813
+ id: "reservations.availability",
814
+ name: "getReservationAvailability",
815
+ description: "How many seats are still free in each time window of a given day.",
816
+ icon: "CalendarClock",
817
+ mode: "read",
818
+ category: "Reservations",
819
+ parameters: {
820
+ type: "object",
821
+ properties: {
822
+ date: { type: "string", description: "ISO date (YYYY-MM-DD)" }
823
+ },
824
+ required: ["date"]
825
+ },
826
+ suggestions: [
827
+ { label: "Tenho mesa para 6 pessoas sexta \xE0 noite?" },
828
+ { label: "Quantas pessoas esperamos hoje?" }
829
+ ]
830
+ },
831
+ {
832
+ id: "reservations.book",
833
+ name: "createReservation",
834
+ description: "Registers a reservation for a party on a date and time.",
835
+ icon: "CalendarPlus",
836
+ mode: "persist",
837
+ category: "Reservations",
838
+ parameters: {
839
+ type: "object",
840
+ properties: {
841
+ guestName: { type: "string" },
842
+ date: { type: "string", description: "ISO date (YYYY-MM-DD)" },
843
+ startTime: { type: "string", description: "HH:MM" },
844
+ partySize: { type: "number" },
845
+ phone: { type: "string" }
846
+ },
847
+ required: ["guestName", "date", "startTime", "partySize"]
848
+ },
849
+ permission: { feature: "reservations", action: "create" }
850
+ }
851
+ ],
852
+ settings: [
853
+ {
854
+ id: "reservations",
855
+ label: config.labels.pageTitle,
856
+ icon: "CalendarClock",
857
+ component: SettingsComponent,
858
+ order: 22,
859
+ // Reading the grid is not the same as writing it: reception opens this
860
+ // to see why a window is closed; only a manager changes the lotação.
861
+ permission: { feature: "reservations", action: "read" }
862
+ }
863
+ ],
864
+ locales: reservationsLocales
865
+ };
866
+ }
867
+
868
+ export { CAPACITY_MODES, OCCASIONS, RESERVATION_STATUSES, createMockReservationsProvider, createReservationsPlugin, MIGRATIONS as reservationsMigrations };
869
+ //# sourceMappingURL=index.js.map
870
+ //# sourceMappingURL=index.js.map