@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.
- package/LICENSE +21 -0
- package/dist/ReservationsPage-5RF5SYZT.js +1157 -0
- package/dist/ReservationsPage-5RF5SYZT.js.map +1 -0
- package/dist/chunk-7KXGSUHT.js +335 -0
- package/dist/chunk-7KXGSUHT.js.map +1 -0
- package/dist/components/CapacityStrip.d.ts +25 -0
- package/dist/components/CapacityStrip.d.ts.map +1 -0
- package/dist/components/ReservationModal.d.ts +23 -0
- package/dist/components/ReservationModal.d.ts.map +1 -0
- package/dist/components/SeatDialog.d.ts +16 -0
- package/dist/components/SeatDialog.d.ts.map +1 -0
- package/dist/context.d.ts +49 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/data/core-supabase.d.ts +32 -0
- package/dist/data/core-supabase.d.ts.map +1 -0
- package/dist/data/mock.d.ts +3 -0
- package/dist/data/mock.d.ts.map +1 -0
- package/dist/data/types.d.ts +37 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +870 -0
- package/dist/index.js.map +1 -0
- package/dist/locales/en.d.ts +2 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/index.d.ts +2 -0
- package/dist/locales/index.d.ts.map +1 -0
- package/dist/locales/pt-BR.d.ts +2 -0
- package/dist/locales/pt-BR.d.ts.map +1 -0
- package/dist/migrations/index.d.ts +6 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/store.d.ts +48 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/types.d.ts +174 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/views/CapacitySettingsView.d.ts +3 -0
- package/dist/views/CapacitySettingsView.d.ts.map +1 -0
- package/dist/views/ReservationsListView.d.ts +5 -0
- package/dist/views/ReservationsListView.d.ts.map +1 -0
- package/dist/views/ReservationsPage.d.ts +27 -0
- package/dist/views/ReservationsPage.d.ts.map +1 -0
- package/dist/views/ServiceDayView.d.ts +15 -0
- package/dist/views/ServiceDayView.d.ts.map +1 -0
- 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
|