@fayz-ai/plugin-agenda 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/chunk-DRZF6CV2.js +421 -0
  2. package/dist/chunk-DRZF6CV2.js.map +1 -0
  3. package/dist/chunk-LFUELUIT.cjs +423 -0
  4. package/dist/chunk-LFUELUIT.cjs.map +1 -0
  5. package/dist/data/mock.d.ts.map +1 -1
  6. package/dist/index.cjs +300 -421
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +297 -420
  10. package/dist/index.js.map +1 -1
  11. package/dist/migrations/index.d.ts +2 -0
  12. package/dist/migrations/index.d.ts.map +1 -0
  13. package/dist/public/BookingWidget.d.ts +11 -0
  14. package/dist/public/BookingWidget.d.ts.map +1 -0
  15. package/dist/public/components/BrandHeader.d.ts +6 -0
  16. package/dist/public/components/BrandHeader.d.ts.map +1 -0
  17. package/dist/public/components/ServiceCard.d.ts +5 -0
  18. package/dist/public/components/ServiceCard.d.ts.map +1 -0
  19. package/dist/public/components/SidebarExtras.d.ts +4 -0
  20. package/dist/public/components/SidebarExtras.d.ts.map +1 -0
  21. package/dist/public/components/StepRailItem.d.ts +4 -0
  22. package/dist/public/components/StepRailItem.d.ts.map +1 -0
  23. package/dist/public/components/SuccessPanel.d.ts +4 -0
  24. package/dist/public/components/SuccessPanel.d.ts.map +1 -0
  25. package/dist/public/components/SummaryRail.d.ts +27 -0
  26. package/dist/public/components/SummaryRail.d.ts.map +1 -0
  27. package/dist/public/components/defaults.d.ts +3 -0
  28. package/dist/public/components/defaults.d.ts.map +1 -0
  29. package/dist/public/context.d.ts +51 -0
  30. package/dist/public/context.d.ts.map +1 -0
  31. package/dist/public/createPublicBookingPlugin.d.ts +62 -0
  32. package/dist/public/createPublicBookingPlugin.d.ts.map +1 -0
  33. package/dist/public/data.d.ts +37 -0
  34. package/dist/public/data.d.ts.map +1 -0
  35. package/dist/public/data.supabase.d.ts +7 -0
  36. package/dist/public/data.supabase.d.ts.map +1 -0
  37. package/dist/public/format.d.ts +18 -0
  38. package/dist/public/format.d.ts.map +1 -0
  39. package/dist/public/hooks.d.ts +34 -0
  40. package/dist/public/hooks.d.ts.map +1 -0
  41. package/dist/public/index.cjs +1423 -0
  42. package/dist/public/index.cjs.map +1 -0
  43. package/dist/public/index.d.ts +20 -0
  44. package/dist/public/index.d.ts.map +1 -0
  45. package/dist/public/index.js +1402 -0
  46. package/dist/public/index.js.map +1 -0
  47. package/dist/public/steps/ContactStep.d.ts +30 -0
  48. package/dist/public/steps/ContactStep.d.ts.map +1 -0
  49. package/dist/public/steps/DateTimeStep.d.ts +23 -0
  50. package/dist/public/steps/DateTimeStep.d.ts.map +1 -0
  51. package/dist/public/steps/OverviewStep.d.ts +16 -0
  52. package/dist/public/steps/OverviewStep.d.ts.map +1 -0
  53. package/dist/public/steps/PaymentStep.d.ts +20 -0
  54. package/dist/public/steps/PaymentStep.d.ts.map +1 -0
  55. package/dist/public/types.d.ts +185 -0
  56. package/dist/public/types.d.ts.map +1 -0
  57. package/package.json +20 -14
  58. package/src/data/mock.ts +4 -1
  59. package/src/index.ts +14 -0
  60. package/src/integrations/google-calendar/integration.test.ts +1 -1
  61. package/src/migrations/001_public_booking.sql +278 -0
  62. package/src/migrations/index.ts +282 -0
  63. package/src/public/BookingWidget.tsx +315 -0
  64. package/src/public/components/BrandHeader.tsx +27 -0
  65. package/src/public/components/ServiceCard.tsx +39 -0
  66. package/src/public/components/SidebarExtras.tsx +30 -0
  67. package/src/public/components/StepRailItem.tsx +27 -0
  68. package/src/public/components/SuccessPanel.tsx +27 -0
  69. package/src/public/components/SummaryRail.tsx +90 -0
  70. package/src/public/components/defaults.ts +14 -0
  71. package/src/public/context.tsx +100 -0
  72. package/src/public/createPublicBookingPlugin.ts +265 -0
  73. package/src/public/data.supabase.ts +94 -0
  74. package/src/public/data.ts +99 -0
  75. package/src/public/format.ts +61 -0
  76. package/src/public/hooks.ts +105 -0
  77. package/src/public/index.ts +68 -0
  78. package/src/public/steps/ContactStep.tsx +179 -0
  79. package/src/public/steps/DateTimeStep.tsx +138 -0
  80. package/src/public/steps/OverviewStep.tsx +81 -0
  81. package/src/public/steps/PaymentStep.tsx +180 -0
  82. package/src/public/types.ts +229 -0
package/dist/index.cjs CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ var chunkLFUELUIT_cjs = require('./chunk-LFUELUIT.cjs');
3
4
  var chunkSYDBDI3Q_cjs = require('./chunk-SYDBDI3Q.cjs');
4
5
  var React12 = require('react');
5
6
  var core = require('@fayz-ai/core');
@@ -3886,10 +3887,10 @@ function createSupabaseAgendaProvider() {
3886
3887
  },
3887
3888
  async getConfirmationsPending(daysAhead = 2) {
3888
3889
  const { pub } = getClients();
3889
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
3890
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3890
3891
  const cutoff = /* @__PURE__ */ new Date();
3891
3892
  cutoff.setDate(cutoff.getDate() + daysAhead);
3892
- const { data, error } = await pub.from("v_bookings").select("*").eq("status", "scheduled").gte("starts_at", now2).lte("starts_at", cutoff.toISOString()).order("starts_at");
3893
+ const { data, error } = await pub.from("v_bookings").select("*").eq("status", "scheduled").gte("starts_at", now).lte("starts_at", cutoff.toISOString()).order("starts_at");
3893
3894
  if (error) throw error;
3894
3895
  return (data ?? []).map(mapBooking);
3895
3896
  },
@@ -3914,423 +3915,6 @@ function createSupabaseAgendaProvider() {
3914
3915
  }
3915
3916
  };
3916
3917
  }
3917
-
3918
- // src/data/mock.ts
3919
- var nextId = 1;
3920
- function uid() {
3921
- return `mock-${nextId++}`;
3922
- }
3923
- function now() {
3924
- return (/* @__PURE__ */ new Date()).toISOString();
3925
- }
3926
- function addMinutes2(date, minutes) {
3927
- const d = new Date(date);
3928
- d.setMinutes(d.getMinutes() + minutes);
3929
- return d.toISOString();
3930
- }
3931
- function timeOverlaps(aStart, aEnd, bStart, bEnd) {
3932
- return aStart < bEnd && aEnd > bStart;
3933
- }
3934
- var SAMPLE_PROFESSIONALS = [
3935
- { id: "prof-1", name: "Ana Silva", avatarUrl: null, locationId: "loc-1", locationName: "Main Branch", isActive: true },
3936
- { id: "prof-2", name: "Carlos Santos", avatarUrl: null, locationId: "loc-1", locationName: "Main Branch", isActive: true },
3937
- { id: "prof-3", name: "Maria Oliveira", avatarUrl: null, locationId: "loc-1", locationName: "Main Branch", isActive: true }
3938
- ];
3939
- function createSampleBookings() {
3940
- const today = /* @__PURE__ */ new Date();
3941
- const year = today.getFullYear();
3942
- const month = today.getMonth();
3943
- const day = today.getDate();
3944
- function dt(hour, minute = 0) {
3945
- return new Date(year, month, day, hour, minute).toISOString();
3946
- }
3947
- return [
3948
- {
3949
- id: "bk-1",
3950
- tenantId: "mock",
3951
- kind: "appointment",
3952
- startsAt: dt(9, 0),
3953
- endsAt: dt(10, 30),
3954
- status: "confirmed",
3955
- notes: null,
3956
- orderId: "ord-1",
3957
- locationId: "loc-1",
3958
- metadata: {},
3959
- clientId: "cl-1",
3960
- clientName: "Sarah Johnson",
3961
- clientPhone: "(11) 99999-1111",
3962
- clientEmail: "sarah@email.com",
3963
- clientAvatarUrl: null,
3964
- professionalId: "prof-1",
3965
- professionalName: "Ana Silva",
3966
- professionalAvatarUrl: null,
3967
- locationName: "Main Branch",
3968
- orderTotal: 250,
3969
- orderStatus: "draft",
3970
- services: [{ id: "bi-1", serviceId: "svc-1", name: "Balayage", durationMinutes: 90, price: 250, assigneeId: "prof-1" }],
3971
- totalDurationMinutes: 90,
3972
- createdAt: now(),
3973
- updatedAt: now()
3974
- },
3975
- {
3976
- id: "bk-2",
3977
- tenantId: "mock",
3978
- kind: "appointment",
3979
- startsAt: dt(10, 0),
3980
- endsAt: dt(11, 0),
3981
- status: "scheduled",
3982
- notes: null,
3983
- orderId: "ord-2",
3984
- locationId: "loc-1",
3985
- metadata: {},
3986
- clientId: "cl-2",
3987
- clientName: "Emily Carter",
3988
- clientPhone: "(11) 99999-2222",
3989
- clientEmail: "emily@email.com",
3990
- clientAvatarUrl: null,
3991
- professionalId: "prof-2",
3992
- professionalName: "Carlos Santos",
3993
- professionalAvatarUrl: null,
3994
- locationName: "Main Branch",
3995
- orderTotal: 80,
3996
- orderStatus: "draft",
3997
- services: [{ id: "bi-2", serviceId: "svc-2", name: "Haircut", durationMinutes: 60, price: 80, assigneeId: "prof-2" }],
3998
- totalDurationMinutes: 60,
3999
- createdAt: now(),
4000
- updatedAt: now()
4001
- },
4002
- {
4003
- id: "bk-3",
4004
- tenantId: "mock",
4005
- kind: "appointment",
4006
- startsAt: dt(14, 0),
4007
- endsAt: dt(15, 0),
4008
- status: "confirmed",
4009
- notes: "First visit",
4010
- orderId: "ord-3",
4011
- locationId: "loc-1",
4012
- metadata: {},
4013
- clientId: "cl-3",
4014
- clientName: "Rachel Kim",
4015
- clientPhone: "(11) 99999-3333",
4016
- clientEmail: null,
4017
- clientAvatarUrl: null,
4018
- professionalId: "prof-3",
4019
- professionalName: "Maria Oliveira",
4020
- professionalAvatarUrl: null,
4021
- locationName: "Main Branch",
4022
- orderTotal: 120,
4023
- orderStatus: "draft",
4024
- services: [{ id: "bi-3", serviceId: "svc-3", name: "Facial Treatment", durationMinutes: 60, price: 120, assigneeId: "prof-3" }],
4025
- totalDurationMinutes: 60,
4026
- createdAt: now(),
4027
- updatedAt: now()
4028
- },
4029
- {
4030
- id: "bk-4",
4031
- tenantId: "mock",
4032
- kind: "appointment",
4033
- startsAt: dt(11, 0),
4034
- endsAt: dt(12, 0),
4035
- status: "scheduled",
4036
- notes: null,
4037
- orderId: "ord-4",
4038
- locationId: "loc-1",
4039
- metadata: {},
4040
- clientId: "cl-4",
4041
- clientName: "Jessica Lima",
4042
- clientPhone: "(11) 99999-4444",
4043
- clientEmail: null,
4044
- clientAvatarUrl: null,
4045
- professionalId: "prof-1",
4046
- professionalName: "Ana Silva",
4047
- professionalAvatarUrl: null,
4048
- locationName: "Main Branch",
4049
- orderTotal: 60,
4050
- orderStatus: "draft",
4051
- services: [{ id: "bi-4", serviceId: "svc-4", name: "Manicure", durationMinutes: 60, price: 60, assigneeId: "prof-1" }],
4052
- totalDurationMinutes: 60,
4053
- createdAt: now(),
4054
- updatedAt: now()
4055
- },
4056
- {
4057
- id: "bk-5",
4058
- tenantId: "mock",
4059
- kind: "appointment",
4060
- startsAt: dt(15, 30),
4061
- endsAt: dt(17, 0),
4062
- status: "confirmed",
4063
- notes: null,
4064
- orderId: "ord-5",
4065
- locationId: "loc-1",
4066
- metadata: {},
4067
- clientId: "cl-5",
4068
- clientName: "Amanda Wright",
4069
- clientPhone: "(11) 99999-5555",
4070
- clientEmail: "amanda@email.com",
4071
- clientAvatarUrl: null,
4072
- professionalId: "prof-2",
4073
- professionalName: "Carlos Santos",
4074
- professionalAvatarUrl: null,
4075
- locationName: "Main Branch",
4076
- orderTotal: 350,
4077
- orderStatus: "draft",
4078
- services: [
4079
- { id: "bi-5", serviceId: "svc-1", name: "Highlights", durationMinutes: 60, price: 200, assigneeId: "prof-2" },
4080
- { id: "bi-6", serviceId: "svc-2", name: "Haircut", durationMinutes: 30, price: 150, assigneeId: "prof-2" }
4081
- ],
4082
- totalDurationMinutes: 90,
4083
- createdAt: now(),
4084
- updatedAt: now()
4085
- }
4086
- ];
4087
- }
4088
- function createSampleSchedules() {
4089
- const schedules = [];
4090
- for (const prof of SAMPLE_PROFESSIONALS) {
4091
- for (let day = 1; day <= 5; day++) {
4092
- schedules.push({
4093
- id: uid(),
4094
- tenantId: "mock",
4095
- kind: "working_hours",
4096
- assigneeId: prof.id,
4097
- locationId: prof.locationId,
4098
- dayOfWeek: day,
4099
- specificDate: null,
4100
- startsAt: "08:00",
4101
- endsAt: "12:00",
4102
- isActive: true,
4103
- metadata: {}
4104
- });
4105
- schedules.push({
4106
- id: uid(),
4107
- tenantId: "mock",
4108
- kind: "working_hours",
4109
- assigneeId: prof.id,
4110
- locationId: prof.locationId,
4111
- dayOfWeek: day,
4112
- specificDate: null,
4113
- startsAt: "13:00",
4114
- endsAt: "18:00",
4115
- isActive: true,
4116
- metadata: {}
4117
- });
4118
- }
4119
- schedules.push({
4120
- id: uid(),
4121
- tenantId: "mock",
4122
- kind: "working_hours",
4123
- assigneeId: prof.id,
4124
- locationId: prof.locationId,
4125
- dayOfWeek: 6,
4126
- specificDate: null,
4127
- startsAt: "08:00",
4128
- endsAt: "14:00",
4129
- isActive: true,
4130
- metadata: {}
4131
- });
4132
- }
4133
- return schedules;
4134
- }
4135
- function createMockAgendaProvider(options) {
4136
- const seed = options?.seed;
4137
- const bookings = seed?.bookings ? [...seed.bookings] : createSampleBookings();
4138
- const schedules = seed?.schedules ? [...seed.schedules] : createSampleSchedules();
4139
- const professionals = seed?.professionals ? [...seed.professionals] : [...SAMPLE_PROFESSIONALS];
4140
- return {
4141
- async getLocations() {
4142
- return [];
4143
- },
4144
- async getBookings(query) {
4145
- return bookings.filter((b) => {
4146
- if (query.kind && b.kind !== query.kind) return false;
4147
- if (query.statuses?.length && !query.statuses.includes(b.status)) return false;
4148
- if (query.professionalIds?.length && !query.professionalIds.includes(b.professionalId ?? "")) return false;
4149
- if (query.locationId && b.locationId !== query.locationId) return false;
4150
- if (query.dateRange) {
4151
- if (b.startsAt > query.dateRange.end || (b.endsAt ?? b.startsAt) < query.dateRange.start) return false;
4152
- }
4153
- return true;
4154
- });
4155
- },
4156
- async getBookingById(id) {
4157
- return bookings.find((b) => b.id === id) ?? null;
4158
- },
4159
- async createBooking(input) {
4160
- const totalDuration = input.services.reduce((sum, s) => sum + s.durationMinutes, 0);
4161
- const totalPrice = input.services.reduce((sum, s) => sum + s.price, 0);
4162
- const booking = {
4163
- id: uid(),
4164
- tenantId: "mock",
4165
- kind: input.kind ?? "appointment",
4166
- startsAt: input.startsAt,
4167
- endsAt: addMinutes2(input.startsAt, totalDuration || 30),
4168
- status: "scheduled",
4169
- notes: input.notes ?? null,
4170
- orderId: input.services.length > 0 ? uid() : null,
4171
- locationId: input.locationId ?? null,
4172
- metadata: {},
4173
- clientId: input.clientId,
4174
- clientName: input.title?.trim() || "New Client",
4175
- clientPhone: null,
4176
- clientEmail: null,
4177
- clientAvatarUrl: null,
4178
- professionalId: input.professionalId,
4179
- professionalName: professionals.find((p) => p.id === input.professionalId)?.name ?? "Unknown",
4180
- professionalAvatarUrl: null,
4181
- locationName: null,
4182
- orderTotal: totalPrice,
4183
- orderStatus: "draft",
4184
- services: input.services.map((s) => ({
4185
- id: uid(),
4186
- serviceId: s.serviceId,
4187
- name: s.name,
4188
- durationMinutes: s.durationMinutes,
4189
- price: s.price,
4190
- assigneeId: s.assigneeId ?? input.professionalId
4191
- })),
4192
- totalDurationMinutes: totalDuration,
4193
- createdAt: now(),
4194
- updatedAt: now()
4195
- };
4196
- bookings.push(booking);
4197
- return booking;
4198
- },
4199
- async updateBooking(id, data) {
4200
- const idx = bookings.findIndex((b) => b.id === id);
4201
- if (idx === -1) throw new Error("Booking not found");
4202
- const existing = bookings[idx];
4203
- const serviceUpdates = {};
4204
- if (data.services) {
4205
- serviceUpdates.services = data.services.map((s) => ({
4206
- id: uid(),
4207
- serviceId: s.serviceId,
4208
- name: s.name,
4209
- durationMinutes: s.durationMinutes,
4210
- price: s.price,
4211
- assigneeId: s.assigneeId ?? existing.professionalId ?? ""
4212
- }));
4213
- serviceUpdates.totalDurationMinutes = data.services.reduce((sum, s) => sum + s.durationMinutes, 0);
4214
- serviceUpdates.orderTotal = data.services.reduce((sum, s) => sum + s.price, 0);
4215
- }
4216
- const updated = {
4217
- ...existing,
4218
- ...data.startsAt && { startsAt: data.startsAt },
4219
- ...data.endsAt && { endsAt: data.endsAt },
4220
- ...data.title !== void 0 && { clientName: data.title },
4221
- ...data.status && { status: data.status },
4222
- ...data.notes !== void 0 && { notes: data.notes ?? null },
4223
- ...data.professionalId && { professionalId: data.professionalId },
4224
- ...data.clientId && { clientId: data.clientId },
4225
- ...data.locationId && { locationId: data.locationId },
4226
- ...serviceUpdates,
4227
- updatedAt: now()
4228
- };
4229
- bookings[idx] = updated;
4230
- return updated;
4231
- },
4232
- async deleteBooking(id) {
4233
- const idx = bookings.findIndex((b) => b.id === id);
4234
- if (idx !== -1) bookings.splice(idx, 1);
4235
- },
4236
- async updateBookingStatus(id, status) {
4237
- const idx = bookings.findIndex((b) => b.id === id);
4238
- if (idx === -1) throw new Error("Booking not found");
4239
- bookings[idx] = { ...bookings[idx], status, updatedAt: now() };
4240
- return bookings[idx];
4241
- },
4242
- async checkConflict(params) {
4243
- return bookings.some((b) => {
4244
- if (params.excludeBookingId && b.id === params.excludeBookingId) return false;
4245
- if (b.professionalId !== params.assigneeId) return false;
4246
- if (b.status === "cancelled" || b.status === "no_show") return false;
4247
- return timeOverlaps(b.startsAt, b.endsAt ?? b.startsAt, params.startsAt, params.endsAt);
4248
- });
4249
- },
4250
- async getAvailableSlots(params) {
4251
- const date = new Date(params.date);
4252
- const dayOfWeek = date.getDay();
4253
- const interval = params.slotInterval ?? 30;
4254
- const daySchedules = schedules.filter(
4255
- (s) => s.assigneeId === params.assigneeId && s.isActive && (s.dayOfWeek === dayOfWeek || s.specificDate === params.date)
4256
- );
4257
- const slots = [];
4258
- for (const sched of daySchedules) {
4259
- const [startH, startM] = sched.startsAt.split(":").map(Number);
4260
- const [endH, endM] = sched.endsAt.split(":").map(Number);
4261
- const schedStart = new Date(date);
4262
- schedStart.setHours(startH, startM, 0, 0);
4263
- const schedEnd = new Date(date);
4264
- schedEnd.setHours(endH, endM, 0, 0);
4265
- let slotStart = new Date(schedStart);
4266
- while (true) {
4267
- const slotEnd = new Date(slotStart.getTime() + params.durationMinutes * 6e4);
4268
- if (slotEnd > schedEnd) break;
4269
- const startIso = slotStart.toISOString();
4270
- const endIso = slotEnd.toISOString();
4271
- const hasConflict = bookings.some((b) => {
4272
- if (b.professionalId !== params.assigneeId) return false;
4273
- if (b.status === "cancelled" || b.status === "no_show") return false;
4274
- return timeOverlaps(b.startsAt, b.endsAt ?? b.startsAt, startIso, endIso);
4275
- });
4276
- if (!hasConflict) {
4277
- slots.push({ start: startIso, end: endIso });
4278
- }
4279
- slotStart = new Date(slotStart.getTime() + interval * 6e4);
4280
- }
4281
- }
4282
- return slots;
4283
- },
4284
- async getSchedules(professionalId) {
4285
- if (professionalId) return schedules.filter((s) => s.assigneeId === professionalId);
4286
- return [...schedules];
4287
- },
4288
- async saveSchedule(input) {
4289
- const schedule = {
4290
- id: uid(),
4291
- tenantId: "mock",
4292
- kind: "working_hours",
4293
- assigneeId: input.assigneeId,
4294
- locationId: input.locationId ?? null,
4295
- dayOfWeek: input.dayOfWeek ?? null,
4296
- specificDate: input.specificDate ?? null,
4297
- startsAt: input.startsAt,
4298
- endsAt: input.endsAt,
4299
- isActive: input.isActive !== false,
4300
- metadata: input.metadata ?? {}
4301
- };
4302
- schedules.push(schedule);
4303
- return schedule;
4304
- },
4305
- async deleteSchedule(id) {
4306
- const idx = schedules.findIndex((s) => s.id === id);
4307
- if (idx !== -1) schedules.splice(idx, 1);
4308
- },
4309
- async getProfessionals() {
4310
- return [...professionals];
4311
- },
4312
- async getConfirmationsPending(daysAhead = 2) {
4313
- const cutoff = /* @__PURE__ */ new Date();
4314
- cutoff.setDate(cutoff.getDate() + daysAhead);
4315
- return bookings.filter(
4316
- (b) => b.status === "scheduled" && new Date(b.startsAt) <= cutoff && new Date(b.startsAt) >= /* @__PURE__ */ new Date()
4317
- );
4318
- },
4319
- async sendConfirmation(bookingId, channel) {
4320
- const booking = bookings.find((b) => b.id === bookingId);
4321
- if (!booking) throw new Error("Booking not found");
4322
- const confirmations = booking.metadata.confirmations ?? [];
4323
- confirmations.push({ channel, sentAt: now(), status: "sent" });
4324
- booking.metadata = { ...booking.metadata, confirmations };
4325
- },
4326
- async completeBooking(bookingId) {
4327
- const idx = bookings.findIndex((b) => b.id === bookingId);
4328
- if (idx === -1) throw new Error("Booking not found");
4329
- bookings[idx] = { ...bookings[idx], status: "completed", updatedAt: now() };
4330
- return { booking: bookings[idx], orderId: bookings[idx].orderId ?? "" };
4331
- }
4332
- };
4333
- }
4334
3918
  function getCurrentBookingQuery(state, dateRange = state.dateRange) {
4335
3919
  return {
4336
3920
  dateRange,
@@ -5079,6 +4663,287 @@ var agendaLocales = {
5079
4663
  en,
5080
4664
  "pt-BR": ptBR
5081
4665
  };
4666
+
4667
+ // src/migrations/index.ts
4668
+ var MIGRATION_001_PUBLIC_BOOKING = `-- ============================================================================
4669
+ -- plugin-agenda 001: booking read model + public (anon) booking surface
4670
+ --
4671
+ -- Ships the objects the agenda providers reference but the spine (packages/db
4672
+ -- migrations 001\u2013008) does not define:
4673
+ -- \xA71 order_items columns the admin provider writes (duration/assignee)
4674
+ -- \xA72 public.v_bookings \u2014 admin read model (authenticated, RLS applies)
4675
+ -- \xA73 public.v_public_services \u2014 anon-readable service catalog
4676
+ -- \xA74 public.get_available_slots \u2014 anon RPC (mirrors the mock algorithm)
4677
+ -- \xA75 public.create_public_booking \u2014 anon RPC (validated, race-safe write)
4678
+ --
4679
+ -- Anon-access design: public booking pages run with the publishable key and no
4680
+ -- session, so the canonical tenant RLS yields nothing. Anon access is granted
4681
+ -- ONLY via the column-whitelisted view (\xA73) and SECURITY DEFINER RPCs (\xA74, \xA75)
4682
+ -- with tight input validation and hardcoded statuses.
4683
+ -- ============================================================================
4684
+
4685
+ -- \xA71 \u2014 columns the agenda provider writes but 004_archetypes lacks
4686
+ ALTER TABLE saas_core.order_items
4687
+ ADD COLUMN IF NOT EXISTS duration_minutes integer,
4688
+ ADD COLUMN IF NOT EXISTS assignee_id uuid REFERENCES saas_core.persons(id) ON DELETE SET NULL;
4689
+
4690
+ -- Upsert key for public customers (per tenant, by normalized phone)
4691
+ CREATE UNIQUE INDEX IF NOT EXISTS persons_tenant_customer_phone
4692
+ ON saas_core.persons(tenant_id, phone)
4693
+ WHERE kind = 'customer' AND phone IS NOT NULL;
4694
+
4695
+ -- Slot-conflict scan
4696
+ CREATE INDEX IF NOT EXISTS bookings_assignee_time
4697
+ ON saas_core.bookings(assignee_id, starts_at, ends_at)
4698
+ WHERE status NOT IN ('cancelled', 'no_show');
4699
+
4700
+ -- \xA72 \u2014 admin read model (security_invoker \u2192 underlying RLS applies; anon sees 0 rows)
4701
+ CREATE OR REPLACE VIEW public.v_bookings
4702
+ WITH (security_invoker = true) AS
4703
+ SELECT
4704
+ b.id, b.tenant_id, b.kind, b.starts_at, b.ends_at, b.status, b.notes,
4705
+ b.order_id, b.location_id, b.metadata, b.created_at, b.updated_at,
4706
+ b.party_id AS client_id,
4707
+ COALESCE(pc.name, b.metadata->>'title') AS client_name,
4708
+ pc.phone AS client_phone,
4709
+ pc.email AS client_email,
4710
+ pc.avatar_url AS client_avatar_url,
4711
+ b.assignee_id AS professional_id,
4712
+ ps.name AS professional_name,
4713
+ ps.avatar_url AS professional_avatar_url,
4714
+ l.name AS location_name,
4715
+ o.total AS order_total,
4716
+ o.status AS order_status,
4717
+ (EXTRACT(EPOCH FROM (COALESCE(b.ends_at, b.starts_at) - b.starts_at))::int / 60)
4718
+ AS total_duration_minutes
4719
+ FROM saas_core.bookings b
4720
+ LEFT JOIN saas_core.persons pc ON pc.id = b.party_id
4721
+ LEFT JOIN saas_core.persons ps ON ps.id = b.assignee_id
4722
+ LEFT JOIN saas_core.locations l ON l.id = b.location_id
4723
+ LEFT JOIN saas_core.orders o ON o.id = b.order_id;
4724
+
4725
+ GRANT SELECT ON public.v_bookings TO authenticated, service_role;
4726
+ REVOKE SELECT ON public.v_bookings FROM anon;
4727
+
4728
+ -- \xA73 \u2014 anon-readable public catalog (owner-rights view = deliberate, whitelisted)
4729
+ CREATE OR REPLACE VIEW public.v_public_services AS
4730
+ SELECT
4731
+ s.id, s.tenant_id, s.name, s.description, s.price, s.currency,
4732
+ COALESCE(s.duration_minutes, 30) AS duration_minutes,
4733
+ s.image_url,
4734
+ COALESCE((s.metadata->>'sort_order')::int, 0) AS sort_order
4735
+ FROM saas_core.services s
4736
+ WHERE s.is_active AND s.status = 'active';
4737
+
4738
+ GRANT SELECT ON public.v_public_services TO anon, authenticated, service_role;
4739
+
4740
+ -- \xA74 \u2014 availability RPC (mirrors plugin-agenda/src/data/mock.ts getAvailableSlots)
4741
+ CREATE OR REPLACE FUNCTION public.get_available_slots(
4742
+ p_tenant_id uuid,
4743
+ p_date date,
4744
+ p_duration_minutes integer,
4745
+ p_assignee_id uuid DEFAULT NULL,
4746
+ p_slot_interval integer DEFAULT 30
4747
+ ) RETURNS TABLE (slot_start timestamptz, slot_end timestamptz)
4748
+ LANGUAGE plpgsql STABLE SECURITY DEFINER
4749
+ SET search_path = saas_core, public
4750
+ AS $$
4751
+ DECLARE
4752
+ v_tz text;
4753
+ BEGIN
4754
+ -- tight validation: this function is anon-callable
4755
+ IF p_duration_minutes IS NULL OR p_duration_minutes NOT BETWEEN 5 AND 480 THEN
4756
+ RAISE EXCEPTION 'invalid duration';
4757
+ END IF;
4758
+ IF p_slot_interval IS NULL OR p_slot_interval NOT BETWEEN 5 AND 240 THEN
4759
+ RAISE EXCEPTION 'invalid interval';
4760
+ END IF;
4761
+ IF p_date IS NULL OR p_date < current_date OR p_date > current_date + 90 THEN
4762
+ RETURN;
4763
+ END IF;
4764
+
4765
+ SELECT COALESCE(t.settings->>'timezone', 'America/Sao_Paulo') INTO v_tz
4766
+ FROM tenants t WHERE t.id = p_tenant_id;
4767
+ IF v_tz IS NULL THEN RETURN; END IF; -- unknown tenant \u2192 empty, not an error
4768
+
4769
+ RETURN QUERY
4770
+ WITH day_schedules AS (
4771
+ SELECT sc.assignee_id, sc.starts_at AS win_start, sc.ends_at AS win_end
4772
+ FROM schedules sc
4773
+ WHERE sc.tenant_id = p_tenant_id
4774
+ AND sc.kind = 'working_hours'
4775
+ AND sc.is_active
4776
+ AND (p_assignee_id IS NULL OR sc.assignee_id = p_assignee_id)
4777
+ AND (
4778
+ sc.specific_date = p_date
4779
+ OR (sc.specific_date IS NULL AND sc.day_of_week = EXTRACT(dow FROM p_date)::smallint)
4780
+ )
4781
+ ),
4782
+ candidate AS (
4783
+ SELECT
4784
+ ds.assignee_id,
4785
+ gs AS s_start,
4786
+ gs + make_interval(mins => p_duration_minutes) AS s_end
4787
+ FROM day_schedules ds
4788
+ CROSS JOIN LATERAL generate_series(
4789
+ (p_date + ds.win_start) AT TIME ZONE v_tz,
4790
+ ((p_date + ds.win_end) AT TIME ZONE v_tz) - make_interval(mins => p_duration_minutes),
4791
+ make_interval(mins => p_slot_interval)
4792
+ ) AS gs
4793
+ )
4794
+ SELECT DISTINCT c.s_start, c.s_end
4795
+ FROM candidate c
4796
+ WHERE NOT EXISTS (
4797
+ SELECT 1 FROM bookings b
4798
+ WHERE b.tenant_id = p_tenant_id
4799
+ AND b.assignee_id = c.assignee_id
4800
+ AND b.status NOT IN ('cancelled', 'no_show')
4801
+ AND b.starts_at < c.s_end
4802
+ AND COALESCE(b.ends_at, b.starts_at) > c.s_start
4803
+ )
4804
+ ORDER BY 1;
4805
+ END;
4806
+ $$;
4807
+
4808
+ REVOKE ALL ON FUNCTION public.get_available_slots(uuid, date, integer, uuid, integer) FROM public;
4809
+ GRANT EXECUTE ON FUNCTION public.get_available_slots(uuid, date, integer, uuid, integer)
4810
+ TO anon, authenticated, service_role;
4811
+
4812
+ -- \xA75 \u2014 public booking write RPC (validated, race-safe, hardcoded statuses)
4813
+ CREATE OR REPLACE FUNCTION public.create_public_booking(
4814
+ p_tenant_id uuid,
4815
+ p_service_id uuid,
4816
+ p_starts_at timestamptz,
4817
+ p_name text,
4818
+ p_phone text,
4819
+ p_email text DEFAULT NULL,
4820
+ p_notes text DEFAULT NULL
4821
+ ) RETURNS TABLE (booking_id uuid, starts_at timestamptz, ends_at timestamptz)
4822
+ LANGUAGE plpgsql SECURITY DEFINER
4823
+ SET search_path = saas_core, public
4824
+ AS $$
4825
+ DECLARE
4826
+ v_service record;
4827
+ v_person_id uuid;
4828
+ v_assignee uuid;
4829
+ v_ends timestamptz;
4830
+ v_order uuid;
4831
+ v_booking uuid;
4832
+ v_phone text;
4833
+ v_tz text;
4834
+ v_notes text;
4835
+ BEGIN
4836
+ -- validation: anon-callable, everything tight
4837
+ IF p_name IS NULL OR length(trim(p_name)) NOT BETWEEN 2 AND 120 THEN
4838
+ RAISE EXCEPTION 'invalid name';
4839
+ END IF;
4840
+ v_phone := regexp_replace(COALESCE(p_phone, ''), '\\D', '', 'g');
4841
+ IF length(v_phone) NOT BETWEEN 8 AND 15 THEN
4842
+ RAISE EXCEPTION 'invalid phone';
4843
+ END IF;
4844
+ IF p_email IS NOT NULL AND (length(p_email) > 254 OR p_email !~ '^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$') THEN
4845
+ RAISE EXCEPTION 'invalid email';
4846
+ END IF;
4847
+ v_notes := left(p_notes, 500);
4848
+ IF p_starts_at IS NULL OR p_starts_at <= now() OR p_starts_at > now() + interval '90 days' THEN
4849
+ RAISE EXCEPTION 'invalid start';
4850
+ END IF;
4851
+
4852
+ SELECT s.id, s.name, s.price, COALESCE(s.duration_minutes, 30) AS duration_minutes
4853
+ INTO v_service
4854
+ FROM services s
4855
+ WHERE s.id = p_service_id AND s.tenant_id = p_tenant_id
4856
+ AND s.is_active AND s.status = 'active';
4857
+ IF NOT FOUND THEN RAISE EXCEPTION 'unknown service'; END IF;
4858
+
4859
+ v_ends := p_starts_at + make_interval(mins => v_service.duration_minutes);
4860
+ SELECT COALESCE(t.settings->>'timezone', 'America/Sao_Paulo') INTO v_tz
4861
+ FROM tenants t WHERE t.id = p_tenant_id;
4862
+ IF v_tz IS NULL THEN RAISE EXCEPTION 'unknown tenant'; END IF;
4863
+
4864
+ -- pick the assignee whose working hours cover the slot
4865
+ SELECT sc.assignee_id INTO v_assignee
4866
+ FROM schedules sc
4867
+ WHERE sc.tenant_id = p_tenant_id
4868
+ AND sc.kind = 'working_hours'
4869
+ AND sc.is_active
4870
+ AND (
4871
+ sc.specific_date = (p_starts_at AT TIME ZONE v_tz)::date
4872
+ OR (sc.specific_date IS NULL
4873
+ AND sc.day_of_week = EXTRACT(dow FROM p_starts_at AT TIME ZONE v_tz)::smallint)
4874
+ )
4875
+ AND (p_starts_at AT TIME ZONE v_tz)::time >= sc.starts_at
4876
+ AND (v_ends AT TIME ZONE v_tz)::time <= sc.ends_at
4877
+ ORDER BY sc.assignee_id
4878
+ LIMIT 1;
4879
+ IF v_assignee IS NULL THEN RAISE EXCEPTION 'slot outside working hours'; END IF;
4880
+
4881
+ -- serialize per (tenant, assignee) to kill the double-book race
4882
+ PERFORM pg_advisory_xact_lock(hashtextextended(p_tenant_id::text || v_assignee::text, 0));
4883
+
4884
+ IF EXISTS (
4885
+ SELECT 1 FROM bookings b
4886
+ WHERE b.tenant_id = p_tenant_id
4887
+ AND b.assignee_id = v_assignee
4888
+ AND b.status NOT IN ('cancelled', 'no_show')
4889
+ AND b.starts_at < v_ends
4890
+ AND COALESCE(b.ends_at, b.starts_at) > p_starts_at
4891
+ ) THEN
4892
+ RAISE EXCEPTION 'slot no longer available';
4893
+ END IF;
4894
+
4895
+ -- anti-spam: cap open future public bookings per phone per tenant
4896
+ IF (
4897
+ SELECT count(*) FROM bookings b
4898
+ JOIN persons p ON p.id = b.party_id
4899
+ WHERE b.tenant_id = p_tenant_id
4900
+ AND p.phone = v_phone
4901
+ AND b.starts_at > now()
4902
+ AND b.status = 'scheduled'
4903
+ ) >= 3 THEN
4904
+ RAISE EXCEPTION 'too many open bookings for this phone';
4905
+ END IF;
4906
+
4907
+ -- upsert the customer by (tenant, phone) \u2014 matches persons_tenant_customer_phone
4908
+ INSERT INTO persons (tenant_id, kind, name, phone, email, metadata)
4909
+ VALUES (p_tenant_id, 'customer', trim(p_name), v_phone, p_email,
4910
+ jsonb_build_object('source', 'public_booking'))
4911
+ ON CONFLICT (tenant_id, phone) WHERE kind = 'customer' AND phone IS NOT NULL
4912
+ DO UPDATE SET
4913
+ name = EXCLUDED.name,
4914
+ email = COALESCE(EXCLUDED.email, persons.email),
4915
+ updated_at = now()
4916
+ RETURNING id INTO v_person_id;
4917
+
4918
+ -- order + booking + order_item (statuses hardcoded \u2014 caller cannot set)
4919
+ INSERT INTO orders (tenant_id, kind, status, party_id, assignee_id, subtotal, total, notes, metadata)
4920
+ VALUES (p_tenant_id, 'appointment', 'scheduled', v_person_id, v_assignee,
4921
+ v_service.price, v_service.price, v_notes,
4922
+ jsonb_build_object('source', 'public_booking',
4923
+ 'serviceNames', v_service.name,
4924
+ 'contactName', trim(p_name)))
4925
+ RETURNING id INTO v_order;
4926
+
4927
+ INSERT INTO bookings (tenant_id, kind, party_id, assignee_id, order_id,
4928
+ starts_at, ends_at, status, notes, metadata)
4929
+ VALUES (p_tenant_id, 'appointment', v_person_id, v_assignee, v_order,
4930
+ p_starts_at, v_ends, 'scheduled', v_notes,
4931
+ jsonb_build_object('source', 'public_booking', 'serviceNames', v_service.name))
4932
+ RETURNING id INTO v_booking;
4933
+
4934
+ INSERT INTO order_items (order_id, service_id, name, quantity, unit_price, total,
4935
+ sort_order, duration_minutes, assignee_id)
4936
+ VALUES (v_order, v_service.id, v_service.name, 1, v_service.price, v_service.price,
4937
+ 0, v_service.duration_minutes, v_assignee);
4938
+
4939
+ RETURN QUERY SELECT v_booking, p_starts_at, v_ends;
4940
+ END;
4941
+ $$;
4942
+
4943
+ REVOKE ALL ON FUNCTION public.create_public_booking(uuid, uuid, timestamptz, text, text, text, text) FROM public;
4944
+ GRANT EXECUTE ON FUNCTION public.create_public_booking(uuid, uuid, timestamptz, text, text, text, text)
4945
+ TO anon, authenticated, service_role;
4946
+ `;
5082
4947
  function computeCommissionAmount(total, ratePercent) {
5083
4948
  const base = Number(total) || 0;
5084
4949
  const rate = Number(ratePercent) || 0;
@@ -5494,7 +5359,7 @@ function createAgendaPlugin(options) {
5494
5359
  core.registerTranslations(agendaLocales);
5495
5360
  const provider2 = options?.dataProvider ?? core.createSafeDataProvider(
5496
5361
  () => createSupabaseAgendaProvider(),
5497
- () => createMockAgendaProvider()
5362
+ () => chunkLFUELUIT_cjs.createMockAgendaProvider()
5498
5363
  );
5499
5364
  const store = createAgendaStore(provider2, options?.financialBridge);
5500
5365
  const registries = [...agendaRegistries, ...options?.settingsRegistries ?? []];
@@ -5521,6 +5386,9 @@ function createAgendaPlugin(options) {
5521
5386
  version: "1.0.0",
5522
5387
  scope: options?.scope ?? "universal",
5523
5388
  verticalId: options?.verticalId,
5389
+ // Booking/scheduling is business-ops (clinic, salon, services) — targets
5390
+ // the saas/admin Panel world, not the ecommerce storefront.
5391
+ scaffolds: ["saas"],
5524
5392
  defaultEnabled: true,
5525
5393
  dependencies: [],
5526
5394
  declaredFeatures: [
@@ -5635,17 +5503,28 @@ function createAgendaPlugin(options) {
5635
5503
  permission: { feature: "appointments", action: "read" }
5636
5504
  }
5637
5505
  ],
5506
+ migrations: [
5507
+ {
5508
+ id: "agenda-001-public-booking",
5509
+ version: "0.3.0",
5510
+ sql: MIGRATION_001_PUBLIC_BOOKING,
5511
+ description: "Booking read model (v_bookings) + anon public booking surface (v_public_services, get_available_slots, create_public_booking) + order_items duration/assignee columns"
5512
+ }
5513
+ ],
5638
5514
  locales: agendaLocales
5639
5515
  };
5640
5516
  }
5641
5517
 
5518
+ Object.defineProperty(exports, "createMockAgendaProvider", {
5519
+ enumerable: true,
5520
+ get: function () { return chunkLFUELUIT_cjs.createMockAgendaProvider; }
5521
+ });
5642
5522
  exports.bookingToEvent = bookingToEvent2;
5643
5523
  exports.computeCommissionAmount = computeCommissionAmount;
5644
5524
  exports.createAgendaPlugin = createAgendaPlugin;
5645
5525
  exports.createFinancialBridge = createFinancialBridge;
5646
5526
  exports.createGoogleCalendarPlugin = createGoogleCalendarPlugin;
5647
5527
  exports.createGoogleCalendarProvider = createGoogleCalendarProvider;
5648
- exports.createMockAgendaProvider = createMockAgendaProvider;
5649
5528
  exports.eventToBookingPatch = eventToBookingPatch;
5650
5529
  exports.googleCalendarConnector = googleCalendarConnector;
5651
5530
  //# sourceMappingURL=index.cjs.map