@fayz-ai/plugin-agenda 0.1.8 → 0.2.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 (68) hide show
  1. package/dist/chunk-GAJVIEXM.cjs +870 -0
  2. package/dist/chunk-GAJVIEXM.cjs.map +1 -0
  3. package/dist/chunk-U6VTLOHN.js +867 -0
  4. package/dist/chunk-U6VTLOHN.js.map +1 -0
  5. package/dist/components/AgendaMobileList.d.ts +10 -0
  6. package/dist/components/AgendaMobileList.d.ts.map +1 -0
  7. package/dist/components/AppointmentModal.d.ts +4 -2
  8. package/dist/components/AppointmentModal.d.ts.map +1 -1
  9. package/dist/components/AppointmentPopover.d.ts.map +1 -1
  10. package/dist/components/EventContextMenu.d.ts.map +1 -1
  11. package/dist/config.d.ts +62 -1
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/data/index.d.ts +1 -0
  14. package/dist/data/index.d.ts.map +1 -1
  15. package/dist/data/mock.d.ts +15 -1
  16. package/dist/data/mock.d.ts.map +1 -1
  17. package/dist/data/supabase.d.ts.map +1 -1
  18. package/dist/index.cjs +611 -1157
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +445 -994
  23. package/dist/index.js.map +1 -1
  24. package/dist/locales/en.d.ts.map +1 -1
  25. package/dist/locales/pt-BR.d.ts.map +1 -1
  26. package/dist/public/BookingWidget.d.ts +9 -0
  27. package/dist/public/BookingWidget.d.ts.map +1 -0
  28. package/dist/public/context.d.ts +37 -0
  29. package/dist/public/context.d.ts.map +1 -0
  30. package/dist/public/createPublicBookingPlugin.d.ts +45 -0
  31. package/dist/public/createPublicBookingPlugin.d.ts.map +1 -0
  32. package/dist/public/hooks.d.ts +32 -0
  33. package/dist/public/hooks.d.ts.map +1 -0
  34. package/dist/public/index.cjs +1020 -0
  35. package/dist/public/index.cjs.map +1 -0
  36. package/dist/public/index.d.ts +9 -0
  37. package/dist/public/index.d.ts.map +1 -0
  38. package/dist/public/index.js +1012 -0
  39. package/dist/public/index.js.map +1 -0
  40. package/dist/public/types.d.ts +51 -0
  41. package/dist/public/types.d.ts.map +1 -0
  42. package/dist/store.d.ts +3 -0
  43. package/dist/store.d.ts.map +1 -1
  44. package/dist/types.d.ts +8 -0
  45. package/dist/types.d.ts.map +1 -1
  46. package/dist/views/CalendarView.d.ts.map +1 -1
  47. package/package.json +11 -8
  48. package/src/components/AgendaMobileList.tsx +103 -0
  49. package/src/components/AppointmentModal.tsx +86 -9
  50. package/src/components/AppointmentPopover.tsx +20 -10
  51. package/src/components/EventContextMenu.tsx +3 -0
  52. package/src/config.ts +104 -5
  53. package/src/data/index.ts +1 -0
  54. package/src/data/mock.ts +26 -6
  55. package/src/data/supabase.ts +13 -2
  56. package/src/index.ts +11 -3
  57. package/src/integrations/google-calendar/integration.test.ts +1 -1
  58. package/src/locales/en.ts +12 -0
  59. package/src/locales/pt-BR.ts +12 -0
  60. package/src/public/BookingWidget.tsx +892 -0
  61. package/src/public/context.tsx +43 -0
  62. package/src/public/createPublicBookingPlugin.ts +155 -0
  63. package/src/public/hooks.ts +117 -0
  64. package/src/public/index.ts +15 -0
  65. package/src/public/types.ts +63 -0
  66. package/src/store.ts +4 -2
  67. package/src/types.ts +8 -0
  68. package/src/views/CalendarView.tsx +220 -54
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
- import { setAgendaTenantId, getAgendaTenantId } from './chunk-3LJKZQK4.js';
2
- import React11, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
3
- import { getSupabaseClientOptional, registerTranslations, createSafeDataProvider, usePluginRuntimeOptional, useTranslation, getActiveTenantId, getCurrentLocale } from '@fayz-ai/core';
1
+ import { createSupabaseAgendaProvider, createMockAgendaProvider } from './chunk-U6VTLOHN.js';
2
+ export { createMockAgendaProvider } from './chunk-U6VTLOHN.js';
3
+ import { setAgendaTenantId } from './chunk-3LJKZQK4.js';
4
+ import React12, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
5
+ import { getSupabaseClientOptional, registerTranslations, createSafeDataProvider, usePluginRuntimeOptional, useTranslation, getCurrentLocale, formatCurrency } from '@fayz-ai/core';
4
6
  import { createPluginContext, setScheduleBlockConfig, getScheduleBlockConfig, PluginSettingsPanel, dedup, SettingsGroup, ToggleRow, SelectRow, usePluginPrefs, PersonLink, CrudFormPage, CrudDetailPage } from '@fayz-ai/saas';
5
7
  import FullCalendar from '@fullcalendar/react';
6
8
  import dayGridPlugin from '@fullcalendar/daygrid';
@@ -67,23 +69,47 @@ var DEFAULT_BOOKING_TYPES = [
67
69
  requiresClient: false
68
70
  }
69
71
  ];
72
+ var SIMPLE_EVENT_BOOKING_TYPES = [
73
+ {
74
+ id: "event",
75
+ label: "Event",
76
+ icon: "Calendar",
77
+ color: "#6366f1",
78
+ fields: { title: true, client: false, professional: false, services: false, location: false, status: false },
79
+ requiresServices: false,
80
+ requiresClient: false
81
+ }
82
+ ];
70
83
  function resolveConfig(options) {
84
+ const simple = options?.eventMode === "simple";
85
+ const bookingTypes = options?.bookingTypes ?? (simple ? SIMPLE_EVENT_BOOKING_TYPES : DEFAULT_BOOKING_TYPES);
86
+ const capabilities = {
87
+ title: bookingTypes.some((bt) => bt.fields.title === true),
88
+ client: bookingTypes.some((bt) => bt.fields.client),
89
+ professional: bookingTypes.some((bt) => bt.fields.professional),
90
+ services: bookingTypes.some((bt) => bt.fields.services),
91
+ location: bookingTypes.some((bt) => bt.fields.location),
92
+ status: bookingTypes.some((bt) => bt.fields.status)
93
+ };
71
94
  return {
72
95
  modules: {
73
- workingHours: options?.modules?.workingHours !== false,
74
- confirmations: options?.modules?.confirmations !== false,
75
- conflictDetection: options?.modules?.conflictDetection !== false,
96
+ // Off-unless-explicit in simple mode (`?? !simple`); default-on in full.
97
+ workingHours: options?.modules?.workingHours ?? !simple,
98
+ confirmations: options?.modules?.confirmations ?? !simple,
99
+ conflictDetection: options?.modules?.conflictDetection ?? !simple,
76
100
  dragAndDrop: options?.modules?.dragAndDrop !== false,
77
101
  locationSelection: options?.modules?.locationSelection === true,
78
102
  financial: options?.modules?.financial ?? !!options?.financialBridge
79
103
  },
104
+ simpleMode: simple,
105
+ capabilities,
80
106
  labels: { ...DEFAULT_LABELS, ...options?.labels },
81
107
  currency: { ...DEFAULT_CURRENCY, ...options?.currency },
82
108
  bookingKind: options?.bookingKind ?? "appointment",
83
109
  orderKind: options?.orderKind ?? "service_order",
84
110
  scheduleKind: options?.scheduleKind ?? "working_hours",
85
111
  statuses: options?.statuses ?? DEFAULT_STATUSES,
86
- bookingTypes: options?.bookingTypes ?? DEFAULT_BOOKING_TYPES,
112
+ bookingTypes,
87
113
  businessHours: options?.businessHours ?? { startTime: "07:00", endTime: "21:00" },
88
114
  slotDuration: options?.slotDuration ?? 30,
89
115
  professionalKind: options?.professionalKind ?? "staff",
@@ -91,6 +117,8 @@ function resolveConfig(options) {
91
117
  autoCreateOrder: options?.autoCreateOrder !== false,
92
118
  locations: options?.locations ?? [],
93
119
  confirmationChannels: options?.confirmationChannels ?? [],
120
+ cancellationDetails: options?.cancellationDetails,
121
+ onBookingCreated: options?.onBookingCreated,
94
122
  contactLookup: options?.contactLookup,
95
123
  clientEntityDef: options?.clientEntityDef,
96
124
  serviceLookup: options?.serviceLookup,
@@ -418,7 +446,7 @@ function toDateLocale(locale) {
418
446
  }
419
447
  function MiniCalendar({ selectedDate, onDateSelect, onMonthChange }) {
420
448
  const locale = getCurrentLocale();
421
- const [viewMonth, setViewMonth] = React11.useState(() => new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1));
449
+ const [viewMonth, setViewMonth] = React12.useState(() => new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1));
422
450
  const monthLabel = viewMonth.toLocaleDateString(toDateLocale(locale), { month: "long", year: "numeric" });
423
451
  const dayLabels = DAY_LABELS[locale] ?? DAY_LABELS.en;
424
452
  const weeks = useMemo(() => {
@@ -707,7 +735,7 @@ var ICON_COLORS = {
707
735
  bank_transfer: "text-warning",
708
736
  check: "text-muted-foreground"
709
737
  };
710
- function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPaymentChange, formatCurrency, inline }) {
738
+ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPaymentChange, formatCurrency: formatCurrency2, inline }) {
711
739
  const t = useTranslation();
712
740
  const config = useAgendaConfig();
713
741
  const bridge = config.financialBridge;
@@ -789,7 +817,7 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
789
817
  !loading && isPaid && justPaid && /* @__PURE__ */ jsxs("div", { className: "text-center py-8 space-y-2 animate-in fade-in zoom-in-95 duration-300", children: [
790
818
  /* @__PURE__ */ jsx("div", { className: "flex h-14 w-14 mx-auto items-center justify-center rounded-full bg-success/10 animate-bounce", style: { animationDuration: "600ms", animationIterationCount: "2" }, children: /* @__PURE__ */ jsx(Check, { className: "h-7 w-7 text-success" }) }),
791
819
  /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-success", children: t("agenda.payment.checkoutComplete") }),
792
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: formatCurrency(detail.totalAmount) })
820
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: formatCurrency2(detail.totalAmount) })
793
821
  ] }),
794
822
  !loading && detail && detail.status !== "none" && !justPaid && /* @__PURE__ */ jsxs("div", { className: "space-y-3 animate-in fade-in duration-300", children: [
795
823
  detail.movements.length > 0 ? /* @__PURE__ */ jsx("div", { className: "space-y-1.5", children: detail.movements.map((mov) => {
@@ -821,21 +849,21 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
821
849
  ] }),
822
850
  isPartial && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-warning/10 px-2 py-0.5 text-[10px] font-medium text-warning", children: [
823
851
  "! ",
824
- formatCurrency(mov.paidAmount),
852
+ formatCurrency2(mov.paidAmount),
825
853
  "/",
826
- formatCurrency(mov.amount)
854
+ formatCurrency2(mov.amount)
827
855
  ] }),
828
856
  isOverdue && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-destructive/10 px-2 py-0.5 text-[10px] font-medium text-destructive", children: [
829
857
  "! ",
830
858
  t("agenda.payment.overdue")
831
859
  ] }),
832
860
  mov.status === "pending" && /* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground", children: t("agenda.payment.pending") }),
833
- /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold", children: formatCurrency(mov.paidAmount || mov.amount) })
861
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold", children: formatCurrency2(mov.paidAmount || mov.amount) })
834
862
  ] })
835
863
  ] }, mov.id);
836
864
  }) }) : /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-sm px-1", children: [
837
865
  /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("agenda.payment.total") }),
838
- /* @__PURE__ */ jsx("span", { className: "font-semibold", children: formatCurrency(detail.totalAmount) })
866
+ /* @__PURE__ */ jsx("span", { className: "font-semibold", children: formatCurrency2(detail.totalAmount) })
839
867
  ] }),
840
868
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
841
869
  detail.referenceNumber && /* @__PURE__ */ jsxs("span", { className: "text-[10px] font-mono text-muted-foreground", children: [
@@ -860,12 +888,12 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
860
888
  /* @__PURE__ */ jsx("p", { className: "text-[11px] font-medium text-muted-foreground mb-2", children: t("agenda.payment.serviceSummary") }),
861
889
  /* @__PURE__ */ jsx("div", { className: "space-y-0.5", children: services.map((svc, i) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-sm py-1", children: [
862
890
  /* @__PURE__ */ jsx("span", { className: "truncate", children: svc.name }),
863
- /* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground", children: formatCurrency(svc.price) })
891
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted-foreground", children: formatCurrency2(svc.price) })
864
892
  ] }, i)) })
865
893
  ] }),
866
894
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-sm font-semibold pt-2 border-t", children: [
867
895
  /* @__PURE__ */ jsx("span", { children: t("agenda.payment.total") }),
868
- /* @__PURE__ */ jsx("span", { children: formatCurrency(orderTotal) })
896
+ /* @__PURE__ */ jsx("span", { children: formatCurrency2(orderTotal) })
869
897
  ] }),
870
898
  /* @__PURE__ */ jsxs("div", { children: [
871
899
  /* @__PURE__ */ jsx("p", { className: "text-[11px] font-medium text-muted-foreground mb-2", children: t("agenda.payment.paymentMethod") }),
@@ -940,7 +968,7 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
940
968
  children: Array.from({ length: 12 }, (_, i) => i + 1).map((n) => /* @__PURE__ */ jsxs("option", { value: n, children: [
941
969
  n,
942
970
  "x ",
943
- n > 1 ? formatCurrency(orderTotal / n) : ""
971
+ n > 1 ? formatCurrency2(orderTotal / n) : ""
944
972
  ] }, n))
945
973
  }
946
974
  )
@@ -969,7 +997,7 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
969
997
  onClick: handleCheckout,
970
998
  disabled: processing || !selectedTypeId,
971
999
  className: "w-full rounded-lg bg-primary border border-primary py-2.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 shadow-button-primary active:shadow-button-inset disabled:opacity-40 transition-colors",
972
- children: processing ? t("agenda.payment.checkingOut") : `${t("agenda.payment.checkout")} ${formatCurrency(orderTotal)}`
1000
+ children: processing ? t("agenda.payment.checkingOut") : `${t("agenda.payment.checkout")} ${formatCurrency2(orderTotal)}`
973
1001
  }
974
1002
  )
975
1003
  ] })
@@ -1232,7 +1260,7 @@ function LocationSelect({ value, onChange, locations }) {
1232
1260
  )) })
1233
1261
  ] });
1234
1262
  }
1235
- function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose, onCreated }) {
1263
+ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, externalSource, onClose, onCreated }) {
1236
1264
  const t = useTranslation();
1237
1265
  const locale = getCurrentLocale();
1238
1266
  const config = useAgendaConfig();
@@ -1245,8 +1273,10 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
1245
1273
  const agendaDateRange = useAgendaStore((s) => s.dateRange);
1246
1274
  const professionals = useAgendaStore((s) => s.professionals);
1247
1275
  const storeLocations = useAgendaStore((s) => s.locations);
1276
+ const defaultBookingType = config.bookingTypes[0]?.id ?? "appointment";
1248
1277
  const [loading, setLoading] = useState(mode === "edit");
1249
- const [bookingType, setBookingType] = useState(prefill?.kind ?? "appointment");
1278
+ const [bookingType, setBookingType] = useState(prefill?.kind ?? defaultBookingType);
1279
+ const [title, setTitle] = useState("");
1250
1280
  const [clientId, setClientId] = useState(prefill?.clientId ?? "");
1251
1281
  const [clientSearch, setClientSearch] = useState("");
1252
1282
  const [clientPhone, setClientPhone] = useState("");
@@ -1291,6 +1321,10 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
1291
1321
  const [clientData, setClientData] = useState(null);
1292
1322
  const [clientDataLoading, setClientDataLoading] = useState(false);
1293
1323
  const [clientEditing, setClientEditing] = useState(false);
1324
+ const [cancellationReasons, setCancellationReasons] = useState([]);
1325
+ const [cancellationReasonId, setCancellationReasonId] = useState("");
1326
+ const [cancellationNotes, setCancellationNotes] = useState("");
1327
+ const [cancellationReasonsLoading, setCancellationReasonsLoading] = useState(false);
1294
1328
  const activeType = config.bookingTypes.find((bt) => bt.id === bookingType) ?? config.bookingTypes[0];
1295
1329
  useEffect(() => {
1296
1330
  if (!open) return;
@@ -1311,7 +1345,8 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
1311
1345
  setNewClientPhone("");
1312
1346
  setNewClientEmail("");
1313
1347
  setCreatingClient(false);
1314
- setBookingType(prefill?.kind ?? "appointment");
1348
+ setBookingType(prefill?.kind ?? defaultBookingType);
1349
+ setTitle("");
1315
1350
  setEditingDate(false);
1316
1351
  setEditingTime(false);
1317
1352
  setConfirmingDelete(false);
@@ -1323,6 +1358,9 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
1323
1358
  setClientData(null);
1324
1359
  setClientDataLoading(false);
1325
1360
  setClientEditing(false);
1361
+ setCancellationReasonId("");
1362
+ setCancellationNotes("");
1363
+ setCancellationReasonsLoading(false);
1326
1364
  setModalTab(initialTab || "appointment");
1327
1365
  if (!prefill?.professionalId && professionals.length === 1) setProfessionalId(professionals[0].id);
1328
1366
  setDate(() => {
@@ -1345,7 +1383,8 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
1345
1383
  setLoading(false);
1346
1384
  return;
1347
1385
  }
1348
- setBookingType(b.kind ?? "appointment");
1386
+ setBookingType(b.kind ?? defaultBookingType);
1387
+ setTitle(b.clientName ?? "");
1349
1388
  setClientId(b.clientId ?? "");
1350
1389
  setClientSearch(b.clientName ?? "");
1351
1390
  setProfessionalId(b.professionalId ?? "");
@@ -1367,6 +1406,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
1367
1406
  setEditPaymentStatus(b.paymentStatus ?? "none");
1368
1407
  const svcSnap = b.services ? b.services.map((s) => ({ id: s.serviceId ?? "", dur: s.durationMinutes, price: s.price })) : [];
1369
1408
  initialSnapshot.current = JSON.stringify({
1409
+ title: b.clientName ?? "",
1370
1410
  clientId: b.clientId ?? "",
1371
1411
  professionalId: b.professionalId ?? "",
1372
1412
  locationId: b.locationId ?? "",
@@ -1374,6 +1414,8 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
1374
1414
  startTime: `${String(new Date(b.startsAt).getHours()).padStart(2, "0")}:${String(new Date(b.startsAt).getMinutes()).padStart(2, "0")}`,
1375
1415
  status: b.status,
1376
1416
  notes: b.notes ?? "",
1417
+ cancellationReasonId: "",
1418
+ cancellationNotes: "",
1377
1419
  services: svcSnap
1378
1420
  });
1379
1421
  setLoading(false);
@@ -1398,6 +1440,11 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
1398
1440
  setClientDataLoading(false);
1399
1441
  }).catch(() => setClientDataLoading(false));
1400
1442
  }, [modalTab, clientId]);
1443
+ useEffect(() => {
1444
+ if (!open || status !== "cancelled" || !config.cancellationDetails || cancellationReasons.length > 0) return;
1445
+ setCancellationReasonsLoading(true);
1446
+ config.cancellationDetails.listReasons().then((reasons) => setCancellationReasons(reasons)).catch(() => setCancellationReasons([])).finally(() => setCancellationReasonsLoading(false));
1447
+ }, [open, status, config.cancellationDetails, cancellationReasons.length]);
1401
1448
  useEffect(() => {
1402
1449
  if (!open || !config.serviceLookup) return;
1403
1450
  const lookup = config.serviceLookup;
@@ -1497,10 +1544,11 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
1497
1544
  locationId: locationId || void 0,
1498
1545
  startsAt,
1499
1546
  notes: notes || void 0,
1547
+ title: activeType.fields.title ? title.trim() : void 0,
1500
1548
  services: services.map((s) => ({ serviceId: s.serviceId, name: s.name, durationMinutes: s.durationMinutes, price: s.price }))
1501
1549
  });
1502
1550
  onClose();
1503
- onCreated?.(newBooking.id);
1551
+ onCreated?.(newBooking.id, externalSource);
1504
1552
  } else if (bookingId) {
1505
1553
  await updateBooking(bookingId, {
1506
1554
  clientId,
@@ -1509,8 +1557,17 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
1509
1557
  startsAt,
1510
1558
  notes: notes || void 0,
1511
1559
  status,
1560
+ title: activeType.fields.title ? title.trim() : void 0,
1512
1561
  services: services.map((s) => ({ serviceId: s.serviceId, name: s.name, durationMinutes: s.durationMinutes, price: s.price }))
1513
1562
  });
1563
+ if (status === "cancelled" && config.cancellationDetails && cancellationReasonId) {
1564
+ await config.cancellationDetails.saveCancellationDetails({
1565
+ bookingId,
1566
+ reasonId: cancellationReasonId,
1567
+ notes: cancellationNotes.trim() || void 0,
1568
+ cancelledAt: (/* @__PURE__ */ new Date()).toISOString()
1569
+ });
1570
+ }
1514
1571
  onClose();
1515
1572
  }
1516
1573
  } catch {
@@ -1528,12 +1585,15 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
1528
1585
  setDeleting(false);
1529
1586
  }
1530
1587
  const isPaidBooking = editPaymentStatus === "paid";
1588
+ const selectedCancellationReason = cancellationReasons.find((reason) => reason.id === cancellationReasonId);
1589
+ const needsCancellationReason = mode === "edit" && status === "cancelled" && !!config.cancellationDetails;
1590
+ const needsCancellationNotes = needsCancellationReason && !!selectedCancellationReason?.requiresNotes;
1531
1591
  const formSnapshot = useMemo(
1532
- () => JSON.stringify({ clientId, professionalId, locationId, date, startTime, status, notes, services: services.map((s) => ({ id: s.serviceId, dur: s.durationMinutes, price: s.price })) }),
1533
- [clientId, professionalId, locationId, date, startTime, status, notes, services]
1592
+ () => JSON.stringify({ title, clientId, professionalId, locationId, date, startTime, status, notes, cancellationReasonId, cancellationNotes, services: services.map((s) => ({ id: s.serviceId, dur: s.durationMinutes, price: s.price })) }),
1593
+ [title, clientId, professionalId, locationId, date, startTime, status, notes, cancellationReasonId, cancellationNotes, services]
1534
1594
  );
1535
1595
  const isDirty = mode === "create" || formSnapshot !== initialSnapshot.current;
1536
- const canSubmit = (!activeType.requiresClient || clientId) && (!activeType.fields.professional || professionalId) && (!activeType.requiresServices || services.length > 0) && !conflict && !saving;
1596
+ const canSubmit = (!activeType.fields.title || title.trim()) && (!activeType.requiresClient || clientId) && (!activeType.fields.professional || professionalId) && (!activeType.requiresServices || services.length > 0) && (!needsCancellationReason || !!cancellationReasonId) && (!needsCancellationNotes || !!cancellationNotes.trim()) && !conflict && !saving;
1537
1597
  const showTabs = config.bookingTypes.length > 1;
1538
1598
  return /* @__PURE__ */ jsx(Modal, { open, onOpenChange: (v) => {
1539
1599
  if (!v) onClose();
@@ -1654,8 +1714,8 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
1654
1714
  onCancel: () => setClientEditing(false),
1655
1715
  onSubmit: async (data) => {
1656
1716
  try {
1657
- const { getSupabaseClientOptional: getSupabaseClientOptional4 } = await import('@fayz-ai/core');
1658
- const supabase = getSupabaseClientOptional4();
1717
+ const { getSupabaseClientOptional: getSupabaseClientOptional3 } = await import('@fayz-ai/core');
1718
+ const supabase = getSupabaseClientOptional3();
1659
1719
  if (!supabase) return;
1660
1720
  const dbData = {};
1661
1721
  for (const [k, v] of Object.entries(data)) {
@@ -1708,6 +1768,20 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
1708
1768
  /* @__PURE__ */ jsx("div", { className: "h-8 flex-1 rounded-lg bg-muted animate-pulse" })
1709
1769
  ] }, i)) }),
1710
1770
  !loading && /* @__PURE__ */ jsxs(Fragment, { children: [
1771
+ activeType.fields.title && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1", children: [
1772
+ /* @__PURE__ */ jsx(Calendar, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
1773
+ /* @__PURE__ */ jsx(
1774
+ "input",
1775
+ {
1776
+ type: "text",
1777
+ value: title,
1778
+ onChange: (e) => setTitle(e.target.value),
1779
+ placeholder: t("agenda.appointment.addTitle"),
1780
+ autoFocus: mode === "create",
1781
+ className: "w-full flex-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-ring"
1782
+ }
1783
+ )
1784
+ ] }),
1711
1785
  activeType.fields.client && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1", children: [
1712
1786
  /* @__PURE__ */ jsx(User, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
1713
1787
  /* @__PURE__ */ jsx("div", { className: "flex-1", children: clientId && !quickCreate ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-md bg-muted/30 px-2.5 py-1.5 text-sm", children: [
@@ -1802,10 +1876,10 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
1802
1876
  onClick: async () => {
1803
1877
  setCreatingClient(true);
1804
1878
  try {
1805
- const { getSupabaseClientOptional: getSupabaseClientOptional4 } = await import('@fayz-ai/core');
1806
- const { getAgendaTenantId: getAgendaTenantId2 } = await import('./tenant-LAUWVUYB.js');
1807
- const supabase = getSupabaseClientOptional4();
1808
- const tenantId = getAgendaTenantId2();
1879
+ const { getSupabaseClientOptional: getSupabaseClientOptional3 } = await import('@fayz-ai/core');
1880
+ const { getAgendaTenantId } = await import('./tenant-LAUWVUYB.js');
1881
+ const supabase = getSupabaseClientOptional3();
1882
+ const tenantId = getAgendaTenantId();
1809
1883
  if (!supabase || !tenantId) throw new Error("Not initialized");
1810
1884
  const { data: personRow, error: personError } = await supabase.schema("saas_core").from("persons").insert({
1811
1885
  tenant_id: tenantId,
@@ -1920,6 +1994,37 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
1920
1994
  /* @__PURE__ */ jsx(CircleDot, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
1921
1995
  /* @__PURE__ */ jsx(StatusSelect, { value: status, onChange: setStatus, statuses: config.statuses, bookingDate: date })
1922
1996
  ] }),
1997
+ needsCancellationReason && /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 py-1", children: [
1998
+ /* @__PURE__ */ jsx(Ban, { className: "h-4 w-4 mt-2 text-muted-foreground shrink-0" }),
1999
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-2", children: [
2000
+ /* @__PURE__ */ jsxs(
2001
+ "select",
2002
+ {
2003
+ value: cancellationReasonId,
2004
+ onChange: (event) => setCancellationReasonId(event.target.value),
2005
+ disabled: cancellationReasonsLoading,
2006
+ className: "w-full rounded-input border border-input bg-card px-2.5 py-1.5 text-sm shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] focus:outline-none focus:ring-2 focus:ring-ring",
2007
+ children: [
2008
+ /* @__PURE__ */ jsx("option", { value: "", children: cancellationReasonsLoading ? t("agenda.cancellation.loadingReasons") : t("agenda.cancellation.selectReason") }),
2009
+ cancellationReasons.map((reason) => /* @__PURE__ */ jsxs("option", { value: reason.id, children: [
2010
+ reason.label,
2011
+ reason.requiresNotes ? " *" : ""
2012
+ ] }, reason.id))
2013
+ ]
2014
+ }
2015
+ ),
2016
+ /* @__PURE__ */ jsx(
2017
+ "textarea",
2018
+ {
2019
+ value: cancellationNotes,
2020
+ onChange: (event) => setCancellationNotes(event.target.value),
2021
+ placeholder: needsCancellationNotes ? t("agenda.cancellation.notesRequired") : t("agenda.cancellation.notesOptional"),
2022
+ rows: 2,
2023
+ className: "w-full rounded-input border border-input bg-card px-2.5 py-1.5 text-sm resize-none shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] focus:outline-none focus:ring-2 focus:ring-ring"
2024
+ }
2025
+ )
2026
+ ] })
2027
+ ] }),
1923
2028
  !showNotes ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 py-1", children: [
1924
2029
  /* @__PURE__ */ jsx(FileText, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
1925
2030
  /* @__PURE__ */ jsx(
@@ -2234,7 +2339,7 @@ function AppointmentPopover({ booking, position, onClose, onEdit }) {
2234
2339
  /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
2235
2340
  /* @__PURE__ */ jsx("span", { className: "mt-1.5 h-3 w-3 rounded shrink-0", style: { backgroundColor: color } }),
2236
2341
  /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
2237
- /* @__PURE__ */ jsx(PersonLink, { personId: booking.clientId, name: booking.clientName ?? "Unknown", className: "text-base" }),
2342
+ config.capabilities.client ? /* @__PURE__ */ jsx(PersonLink, { personId: booking.clientId, name: booking.clientName ?? "Unknown", className: "text-base" }) : /* @__PURE__ */ jsx("p", { className: "text-base font-medium truncate", children: booking.clientName || t("agenda.appointment.untitled") }),
2238
2343
  /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground mt-0.5", children: [
2239
2344
  fmtDate(booking.startsAt),
2240
2345
  " \xB7 ",
@@ -2254,7 +2359,7 @@ function AppointmentPopover({ booking, position, onClose, onEdit }) {
2254
2359
  ] })
2255
2360
  ] }, svc.id)) })
2256
2361
  ] }),
2257
- booking.professionalName && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
2362
+ config.capabilities.professional && booking.professionalName && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
2258
2363
  /* @__PURE__ */ jsx(User, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
2259
2364
  /* @__PURE__ */ jsx("span", { className: "text-sm", children: booking.professionalName })
2260
2365
  ] }),
@@ -2305,7 +2410,7 @@ function AppointmentPopover({ booking, position, onClose, onEdit }) {
2305
2410
  )
2306
2411
  ] });
2307
2412
  })(),
2308
- /* @__PURE__ */ jsx("div", { className: "pt-2 border-t fayz-glass-divider", children: /* @__PURE__ */ jsx(
2413
+ config.capabilities.status && /* @__PURE__ */ jsx("div", { className: "pt-2 border-t fayz-glass-divider", children: /* @__PURE__ */ jsx(
2309
2414
  PopoverStatusSelect,
2310
2415
  {
2311
2416
  value: booking.status,
@@ -2422,7 +2527,7 @@ function EventContextMenu({ booking, position, onClose, onEdit }) {
2422
2527
  }
2423
2528
  )
2424
2529
  ] }),
2425
- /* @__PURE__ */ jsxs("div", { className: "border-t px-4 py-3", children: [
2530
+ config.capabilities.status && /* @__PURE__ */ jsxs("div", { className: "border-t px-4 py-3", children: [
2426
2531
  /* @__PURE__ */ jsx("p", { className: "text-[11px] font-medium text-muted-foreground mb-2", children: t("agenda.contextMenu.changeStatus") }),
2427
2532
  /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1.5", children: config.statuses.map((s) => {
2428
2533
  const available = isStatusAvailable(s, booking.startsAt);
@@ -2446,6 +2551,66 @@ function EventContextMenu({ booking, position, onClose, onEdit }) {
2446
2551
  ] })
2447
2552
  ] }) });
2448
2553
  }
2554
+ function startOfDay(d) {
2555
+ const x = new Date(d);
2556
+ x.setHours(0, 0, 0, 0);
2557
+ return x;
2558
+ }
2559
+ function timeLabel(iso) {
2560
+ const d = new Date(iso);
2561
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
2562
+ }
2563
+ function AgendaMobileList({ bookings, statusColors, currencyCode, onSelect }) {
2564
+ const t = useTranslation();
2565
+ const locale = getCurrentLocale() === "pt-BR" ? "pt-BR" : "en";
2566
+ const groups = useMemo(() => {
2567
+ const todayStart = startOfDay(/* @__PURE__ */ new Date()).getTime();
2568
+ const sorted = bookings.filter((b) => !!b.startsAt).slice().sort((a, b) => a.startsAt < b.startsAt ? -1 : a.startsAt > b.startsAt ? 1 : 0);
2569
+ const map = /* @__PURE__ */ new Map();
2570
+ for (const b of sorted) {
2571
+ const day = startOfDay(new Date(b.startsAt));
2572
+ if (day.getTime() < todayStart) continue;
2573
+ const key = day.toISOString().slice(0, 10);
2574
+ const list = map.get(key) ?? [];
2575
+ list.push(b);
2576
+ map.set(key, list);
2577
+ }
2578
+ return Array.from(map.entries()).map(([key, items]) => ({ key, date: /* @__PURE__ */ new Date(key + "T00:00:00"), items }));
2579
+ }, [bookings]);
2580
+ function dayLabel(date) {
2581
+ const today = startOfDay(/* @__PURE__ */ new Date());
2582
+ const diff = Math.round((startOfDay(date).getTime() - today.getTime()) / 864e5);
2583
+ if (diff === 0) return t("agenda.list.today");
2584
+ if (diff === 1) return t("agenda.list.tomorrow");
2585
+ return date.toLocaleDateString(locale, { weekday: "long", day: "numeric", month: "long" });
2586
+ }
2587
+ if (groups.length === 0) {
2588
+ return /* @__PURE__ */ jsx("div", { className: "flex-1 flex flex-col items-center justify-center px-6 py-16 text-center text-muted-foreground", children: /* @__PURE__ */ jsx("p", { className: "text-sm", children: t("agenda.list.empty") }) });
2589
+ }
2590
+ return /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto px-3 py-3 space-y-5", children: groups.map((g) => /* @__PURE__ */ jsxs("div", { children: [
2591
+ /* @__PURE__ */ jsx("h3", { className: "px-1 pb-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: dayLabel(g.date) }),
2592
+ /* @__PURE__ */ jsx("div", { className: "space-y-1.5", children: g.items.map((b) => {
2593
+ const color = statusColors[b.status] ?? "#6b7280";
2594
+ const hasAmount = typeof b.orderTotal === "number" && b.orderTotal > 0;
2595
+ return /* @__PURE__ */ jsxs(
2596
+ "button",
2597
+ {
2598
+ onClick: () => onSelect(b),
2599
+ className: "flex w-full items-center gap-3 rounded-lg border bg-card px-3 py-2.5 text-left shadow-sm transition-colors active:bg-muted/50",
2600
+ children: [
2601
+ /* @__PURE__ */ jsx("span", { className: "h-2.5 w-2.5 shrink-0 rounded-full", style: { backgroundColor: color } }),
2602
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
2603
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium text-foreground", children: b.clientName ?? "\u2014" }),
2604
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: timeLabel(b.startsAt) })
2605
+ ] }),
2606
+ hasAmount && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm font-semibold tabular-nums text-foreground", children: formatCurrency(b.orderTotal, currencyCode) })
2607
+ ]
2608
+ },
2609
+ b.id
2610
+ );
2611
+ }) })
2612
+ ] }, g.key)) });
2613
+ }
2449
2614
  function bookingToEvent(booking, statusColors) {
2450
2615
  const color = statusColors[booking.status] ?? "#6b7280";
2451
2616
  const serviceNames = booking.services?.map((s) => s.name).join(", ") ?? "";
@@ -2568,11 +2733,37 @@ function CalendarSkeleton({ profCount = 2 }) {
2568
2733
  ] }, h)) })
2569
2734
  ] });
2570
2735
  }
2571
- var VIEW_OPTIONS = [
2736
+ var RESOURCE_VIEW_OPTIONS = [
2572
2737
  { key: "resourceTimeGridDay", labelKey: "agenda.calendar.viewDay" },
2573
2738
  { key: "resourceTimeGridWeek", labelKey: "agenda.calendar.viewWeek" },
2574
2739
  { key: "dayGridMonth", labelKey: "agenda.calendar.viewMonth" }
2575
2740
  ];
2741
+ var PLAIN_VIEW_OPTIONS = [
2742
+ { key: "timeGridDay", labelKey: "agenda.calendar.viewDay" },
2743
+ { key: "timeGridWeek", labelKey: "agenda.calendar.viewWeek" },
2744
+ { key: "dayGridMonth", labelKey: "agenda.calendar.viewMonth" },
2745
+ { key: "listMonth", labelKey: "agenda.calendar.viewList" }
2746
+ ];
2747
+ var MOBILE_LIST_VIEW = "__mobileList__";
2748
+ function viewForNoResources(view) {
2749
+ return view.startsWith("resource") ? "dayGridMonth" : view;
2750
+ }
2751
+ function viewForResources(view) {
2752
+ if (view === "timeGridWeek") return "resourceTimeGridWeek";
2753
+ if (view === "timeGridDay") return "resourceTimeGridDay";
2754
+ return view;
2755
+ }
2756
+ function useIsMobile() {
2757
+ const [isMobile, setIsMobile] = useState(() => typeof window !== "undefined" && window.matchMedia("(max-width: 767px)").matches);
2758
+ useEffect(() => {
2759
+ if (typeof window === "undefined") return;
2760
+ const mq = window.matchMedia("(max-width: 767px)");
2761
+ const onChange = () => setIsMobile(mq.matches);
2762
+ mq.addEventListener("change", onChange);
2763
+ return () => mq.removeEventListener("change", onChange);
2764
+ }, []);
2765
+ return isMobile;
2766
+ }
2576
2767
  function CalendarView() {
2577
2768
  injectCalendarStyles();
2578
2769
  const config = useAgendaConfig();
@@ -2605,6 +2796,18 @@ function CalendarView() {
2605
2796
  const _openModal = useAgendaStore((s) => s.openAppointmentModal);
2606
2797
  const closeModal = useAgendaStore((s) => s.closeAppointmentModal);
2607
2798
  const modalState = useAgendaStore((s) => s.appointmentModal);
2799
+ const isMobile = useIsMobile();
2800
+ const resourceCapable = professionals.length > 0;
2801
+ const viewOptions = resourceCapable ? RESOURCE_VIEW_OPTIONS : PLAIN_VIEW_OPTIONS;
2802
+ const [mobileView, setMobileView] = useState(MOBILE_LIST_VIEW);
2803
+ const mobileViewOptions = useMemo(
2804
+ () => [
2805
+ ...viewOptions.filter((o) => o.key !== "listMonth"),
2806
+ { key: MOBILE_LIST_VIEW, labelKey: "agenda.calendar.viewList" }
2807
+ ],
2808
+ [viewOptions]
2809
+ );
2810
+ const mobileGridActive = isMobile && mobileView !== MOBILE_LIST_VIEW;
2608
2811
  const [selectedBooking, setSelectedBooking] = useState(null);
2609
2812
  const [popoverAnchor, setPopoverAnchor] = useState(null);
2610
2813
  const [popoverVisible, _setPopoverVisible] = useState(false);
@@ -2644,6 +2847,24 @@ function CalendarView() {
2644
2847
  fetchSchedules();
2645
2848
  if (config.modules.locationSelection) fetchLocations();
2646
2849
  }, []);
2850
+ useEffect(() => {
2851
+ const api = calendarRef.current?.getApi();
2852
+ if (!api) return;
2853
+ if (!resourceCapable && currentView.startsWith("resource")) {
2854
+ api.changeView("dayGridMonth");
2855
+ } else if (resourceCapable && (currentView === "timeGridWeek" || currentView === "timeGridDay")) {
2856
+ api.changeView(viewForResources(currentView));
2857
+ }
2858
+ }, [resourceCapable, currentView]);
2859
+ useEffect(() => {
2860
+ if (!isMobile) return;
2861
+ const start = /* @__PURE__ */ new Date();
2862
+ start.setHours(0, 0, 0, 0);
2863
+ start.setDate(start.getDate() - 1);
2864
+ const end = new Date(start);
2865
+ end.setDate(end.getDate() + 91);
2866
+ fetchBookings({ start: start.toISOString(), end: end.toISOString() });
2867
+ }, [isMobile, fetchBookings]);
2647
2868
  const resources = useMemo(() => {
2648
2869
  const filtered = selectedProfIds.length > 0 ? professionals.filter((p) => selectedProfIds.includes(p.id)) : professionals;
2649
2870
  return filtered.map((p) => ({
@@ -2668,6 +2889,8 @@ function CalendarView() {
2668
2889
  [schedules, professionals, visibleRange, effectiveBusinessHours, locationMap]
2669
2890
  );
2670
2891
  const events = useMemo(() => [...bookingEvents, ...availabilityEvents], [bookingEvents, availabilityEvents]);
2892
+ const storedView = prefs.get("calendarView");
2893
+ const effectiveInitialView = resourceCapable ? viewForResources(storedView) : viewForNoResources(storedView);
2671
2894
  const handleDatesSet = useCallback((arg) => {
2672
2895
  setCalendarTitle(arg.view.title);
2673
2896
  setView(arg.view.type);
@@ -2784,12 +3007,12 @@ function CalendarView() {
2784
3007
  const locId = schedule?.locationId ?? schedule?.metadata?.locationId;
2785
3008
  openModal("create", { prefill: { startsAt: arg.startStr, endsAt: arg.endStr, professionalId, locationId: locId || void 0 } });
2786
3009
  };
2787
- if (!schedule) {
3010
+ if (config.modules.workingHours && !schedule) {
2788
3011
  setScheduleConfirm({ onConfirm: proceed });
2789
3012
  return;
2790
3013
  }
2791
3014
  proceed();
2792
- }, [openModal, findScheduleForSlot]);
3015
+ }, [openModal, findScheduleForSlot, config.modules.workingHours]);
2793
3016
  const handleEventDrop = useCallback(async (arg) => {
2794
3017
  const booking = arg.event.extendedProps.booking;
2795
3018
  try {
@@ -2802,6 +3025,12 @@ function CalendarView() {
2802
3025
  calendarRef.current?.getApi().changeView(v);
2803
3026
  prefs.set("calendarView", v);
2804
3027
  }, [prefs]);
3028
+ const handleMobileViewChange = useCallback((v) => {
3029
+ setMobileView(v);
3030
+ if (v === MOBILE_LIST_VIEW) return;
3031
+ setView(v);
3032
+ calendarRef.current?.getApi()?.changeView(v);
3033
+ }, [setView]);
2805
3034
  const handlePrev = useCallback(() => calendarRef.current?.getApi().prev(), []);
2806
3035
  const handleNext = useCallback(() => calendarRef.current?.getApi().next(), []);
2807
3036
  const handleToday = useCallback(() => {
@@ -2887,8 +3116,21 @@ function CalendarView() {
2887
3116
  }, [applyResourceStriping, currentView, visibleRange, bookings]);
2888
3117
  useEffect(() => {
2889
3118
  function handleOpenBooking(e) {
2890
- const bookingId = e.detail?.bookingId;
2891
- if (bookingId) openModal("edit", { bookingId });
3119
+ const detail = e.detail ?? {};
3120
+ const externalSource = detail.source ? { type: String(detail.source), id: detail.waitlistId, waitlistId: detail.waitlistId } : void 0;
3121
+ if (detail.bookingId) {
3122
+ openModal("edit", { bookingId: detail.bookingId, externalSource });
3123
+ return;
3124
+ }
3125
+ openModal("create", {
3126
+ prefill: detail.prefill ?? {
3127
+ clientId: detail.clientId,
3128
+ professionalId: detail.professionalId,
3129
+ locationId: detail.locationId,
3130
+ startsAt: detail.startsAt
3131
+ },
3132
+ externalSource
3133
+ });
2892
3134
  }
2893
3135
  window.addEventListener("agenda:open-booking", handleOpenBooking);
2894
3136
  return () => window.removeEventListener("agenda:open-booking", handleOpenBooking);
@@ -2962,110 +3204,135 @@ function CalendarView() {
2962
3204
  }
2963
3205
  ),
2964
3206
  /* @__PURE__ */ jsx(MiniCalendar, { selectedDate, onDateSelect: handleMiniDateSelect }),
2965
- /* @__PURE__ */ jsx("div", { className: "h-px bg-border my-4" }),
2966
- /* @__PURE__ */ jsxs(
2967
- "button",
2968
- {
2969
- onClick: () => setProfSectionOpen((p) => !p),
2970
- className: "flex items-center justify-between w-full text-xs font-medium text-muted-foreground hover:text-foreground py-1 transition-colors",
2971
- children: [
2972
- /* @__PURE__ */ jsx("span", { children: "Agendas" }),
2973
- /* @__PURE__ */ jsx(ChevronDown, { className: `h-3.5 w-3.5 transition-transform ${profSectionOpen ? "" : "-rotate-90"}` })
2974
- ]
2975
- }
2976
- ),
2977
- profSectionOpen && /* @__PURE__ */ jsx("div", { className: "mt-1.5 space-y-0.5", children: professionals.map((prof) => {
2978
- const active = selectedProfIds.length === 0 || selectedProfIds.includes(prof.id);
2979
- return /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2.5 rounded px-1 py-1 cursor-pointer hover:bg-muted/50 transition-colors", children: [
2980
- /* @__PURE__ */ jsx("span", { className: `flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border-2 transition-colors ${active ? "border-primary bg-primary" : "border-muted-foreground/30"}`, children: active && /* @__PURE__ */ jsx("svg", { className: "h-2.5 w-2.5 text-primary-foreground", viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ jsx("path", { d: "M2 6l3 3 5-5", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }),
2981
- /* @__PURE__ */ jsx("span", { className: `text-xs truncate ${active ? "text-foreground" : "text-muted-foreground"}`, children: prof.name }),
2982
- /* @__PURE__ */ jsx("input", { type: "checkbox", className: "sr-only", checked: active, onChange: () => toggleProfessional(prof.id) })
2983
- ] }, prof.id);
2984
- }) }),
2985
- /* @__PURE__ */ jsx("div", { className: "h-px bg-border my-4" }),
2986
- /* @__PURE__ */ jsxs(
2987
- "button",
2988
- {
2989
- onClick: () => setStatusSectionOpen((p) => !p),
2990
- className: "flex items-center justify-between w-full text-xs font-medium text-muted-foreground hover:text-foreground py-1 transition-colors",
2991
- children: [
2992
- /* @__PURE__ */ jsx("span", { children: "Status" }),
2993
- /* @__PURE__ */ jsx(ChevronDown, { className: `h-3.5 w-3.5 transition-transform ${statusSectionOpen ? "" : "-rotate-90"}` })
2994
- ]
2995
- }
2996
- ),
2997
- statusSectionOpen && /* @__PURE__ */ jsx("div", { className: "mt-1.5 space-y-0.5", children: config.statuses.map((st) => {
2998
- const active = selectedStatuses.length === 0 || selectedStatuses.includes(st.value);
2999
- return /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2.5 rounded px-1 py-1 cursor-pointer hover:bg-muted/50 transition-colors", children: [
3000
- /* @__PURE__ */ jsx(
3001
- "span",
3002
- {
3003
- className: "flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border-2 transition-colors",
3004
- style: { borderColor: active ? st.color : st.color + "40", backgroundColor: active ? st.color : "transparent" },
3005
- children: active && /* @__PURE__ */ jsx("svg", { className: "h-2.5 w-2.5 text-white", viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ jsx("path", { d: "M2 6l3 3 5-5", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) })
3006
- }
3007
- ),
3008
- /* @__PURE__ */ jsx("span", { className: `text-xs ${active ? "text-foreground" : "text-muted-foreground"}`, children: st.label }),
3009
- /* @__PURE__ */ jsx("input", { type: "checkbox", className: "sr-only", checked: active, onChange: () => toggleStatus(st.value) })
3010
- ] }, st.value);
3011
- }) })
3207
+ config.capabilities.professional && /* @__PURE__ */ jsxs(Fragment, { children: [
3208
+ /* @__PURE__ */ jsx("div", { className: "h-px bg-border my-4" }),
3209
+ /* @__PURE__ */ jsxs(
3210
+ "button",
3211
+ {
3212
+ onClick: () => setProfSectionOpen((p) => !p),
3213
+ className: "flex items-center justify-between w-full text-xs font-medium text-muted-foreground hover:text-foreground py-1 transition-colors",
3214
+ children: [
3215
+ /* @__PURE__ */ jsx("span", { children: "Agendas" }),
3216
+ /* @__PURE__ */ jsx(ChevronDown, { className: `h-3.5 w-3.5 transition-transform ${profSectionOpen ? "" : "-rotate-90"}` })
3217
+ ]
3218
+ }
3219
+ ),
3220
+ profSectionOpen && /* @__PURE__ */ jsx("div", { className: "mt-1.5 space-y-0.5", children: professionals.map((prof) => {
3221
+ const active = selectedProfIds.length === 0 || selectedProfIds.includes(prof.id);
3222
+ return /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2.5 rounded px-1 py-1 cursor-pointer hover:bg-muted/50 transition-colors", children: [
3223
+ /* @__PURE__ */ jsx("span", { className: `flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border-2 transition-colors ${active ? "border-primary bg-primary" : "border-muted-foreground/30"}`, children: active && /* @__PURE__ */ jsx("svg", { className: "h-2.5 w-2.5 text-primary-foreground", viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ jsx("path", { d: "M2 6l3 3 5-5", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }),
3224
+ /* @__PURE__ */ jsx("span", { className: `text-xs truncate ${active ? "text-foreground" : "text-muted-foreground"}`, children: prof.name }),
3225
+ /* @__PURE__ */ jsx("input", { type: "checkbox", className: "sr-only", checked: active, onChange: () => toggleProfessional(prof.id) })
3226
+ ] }, prof.id);
3227
+ }) })
3228
+ ] }),
3229
+ config.capabilities.status && /* @__PURE__ */ jsxs(Fragment, { children: [
3230
+ /* @__PURE__ */ jsx("div", { className: "h-px bg-border my-4" }),
3231
+ /* @__PURE__ */ jsxs(
3232
+ "button",
3233
+ {
3234
+ onClick: () => setStatusSectionOpen((p) => !p),
3235
+ className: "flex items-center justify-between w-full text-xs font-medium text-muted-foreground hover:text-foreground py-1 transition-colors",
3236
+ children: [
3237
+ /* @__PURE__ */ jsx("span", { children: "Status" }),
3238
+ /* @__PURE__ */ jsx(ChevronDown, { className: `h-3.5 w-3.5 transition-transform ${statusSectionOpen ? "" : "-rotate-90"}` })
3239
+ ]
3240
+ }
3241
+ ),
3242
+ statusSectionOpen && /* @__PURE__ */ jsx("div", { className: "mt-1.5 space-y-0.5", children: config.statuses.map((st) => {
3243
+ const active = selectedStatuses.length === 0 || selectedStatuses.includes(st.value);
3244
+ return /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2.5 rounded px-1 py-1 cursor-pointer hover:bg-muted/50 transition-colors", children: [
3245
+ /* @__PURE__ */ jsx(
3246
+ "span",
3247
+ {
3248
+ className: "flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border-2 transition-colors",
3249
+ style: { borderColor: active ? st.color : st.color + "40", backgroundColor: active ? st.color : "transparent" },
3250
+ children: active && /* @__PURE__ */ jsx("svg", { className: "h-2.5 w-2.5 text-white", viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ jsx("path", { d: "M2 6l3 3 5-5", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) })
3251
+ }
3252
+ ),
3253
+ /* @__PURE__ */ jsx("span", { className: `text-xs ${active ? "text-foreground" : "text-muted-foreground"}`, children: st.label }),
3254
+ /* @__PURE__ */ jsx("input", { type: "checkbox", className: "sr-only", checked: active, onChange: () => toggleStatus(st.value) })
3255
+ ] }, st.value);
3256
+ }) })
3257
+ ] })
3012
3258
  ] }),
3013
3259
  /* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col min-w-0 overflow-hidden", children: [
3014
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 h-12 shrink-0 border-b", children: [
3015
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
3016
- /* @__PURE__ */ jsx(
3017
- "button",
3018
- {
3019
- onClick: () => openModal("create"),
3020
- className: "md:hidden flex h-8 w-8 items-center justify-center rounded-full bg-primary text-primary-foreground",
3021
- children: /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" })
3022
- }
3023
- ),
3024
- /* @__PURE__ */ jsx(
3025
- "button",
3026
- {
3027
- onClick: handleToday,
3028
- className: "rounded-md border px-3 py-1 text-sm font-medium hover:bg-muted bg-card shadow-button active:shadow-button-inset transition-colors",
3029
- children: t("agenda.toolbar.today")
3030
- }
3031
- ),
3032
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5", children: [
3033
- /* @__PURE__ */ jsx("button", { onClick: handlePrev, className: "flex h-7 w-7 items-center justify-center rounded-full hover:bg-muted transition-colors", children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4" }) }),
3034
- /* @__PURE__ */ jsx("button", { onClick: handleNext, className: "flex h-7 w-7 items-center justify-center rounded-full hover:bg-muted transition-colors", children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4" }) })
3260
+ /* @__PURE__ */ jsxs("div", { className: "shrink-0 border-b", children: [
3261
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between pl-4 pr-14 md:px-4 h-12", children: [
3262
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [
3263
+ /* @__PURE__ */ jsx(
3264
+ "button",
3265
+ {
3266
+ onClick: () => openModal("create"),
3267
+ className: "md:hidden flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground",
3268
+ children: /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" })
3269
+ }
3270
+ ),
3271
+ /* @__PURE__ */ jsxs("div", { className: `${mobileGridActive ? "flex" : "hidden"} md:flex items-center gap-3`, children: [
3272
+ /* @__PURE__ */ jsx(
3273
+ "button",
3274
+ {
3275
+ onClick: handleToday,
3276
+ className: "rounded-md border px-3 py-1 text-sm font-medium hover:bg-muted bg-card shadow-button active:shadow-button-inset transition-colors",
3277
+ children: t("agenda.toolbar.today")
3278
+ }
3279
+ ),
3280
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5", children: [
3281
+ /* @__PURE__ */ jsx("button", { onClick: handlePrev, className: "flex h-7 w-7 items-center justify-center rounded-full hover:bg-muted transition-colors", children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-4 w-4" }) }),
3282
+ /* @__PURE__ */ jsx("button", { onClick: handleNext, className: "flex h-7 w-7 items-center justify-center rounded-full hover:bg-muted transition-colors", children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4" }) })
3283
+ ] })
3284
+ ] }),
3285
+ /* @__PURE__ */ jsx("h1", { className: "text-base sm:text-lg font-normal truncate", children: isMobile && !mobileGridActive ? t("agenda.calendar.viewAgenda") : calendarTitle })
3035
3286
  ] }),
3036
- /* @__PURE__ */ jsx("h1", { className: "text-base sm:text-lg font-normal truncate", children: calendarTitle })
3287
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 shrink-0", children: [
3288
+ /* @__PURE__ */ jsx("div", { className: "hidden md:flex items-center gap-1", children: viewOptions.map((opt) => /* @__PURE__ */ jsx(
3289
+ "button",
3290
+ {
3291
+ onClick: () => handleViewChange(opt.key),
3292
+ className: `rounded-md px-3 py-1 text-sm transition-colors ${currentView === opt.key ? "bg-primary/10 text-primary font-medium" : "text-muted-foreground hover:bg-muted/50"}`,
3293
+ children: t(opt.labelKey)
3294
+ },
3295
+ opt.key
3296
+ )) }),
3297
+ /* @__PURE__ */ jsx(
3298
+ Button,
3299
+ {
3300
+ variant: "outline",
3301
+ size: "icon",
3302
+ onClick: () => window.location.hash = "/settings/agenda",
3303
+ title: "Agenda Settings",
3304
+ className: "ml-1",
3305
+ children: /* @__PURE__ */ jsx(Settings, { className: "h-4 w-4" })
3306
+ }
3307
+ )
3308
+ ] })
3037
3309
  ] }),
3038
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
3039
- /* @__PURE__ */ jsx("div", { className: "hidden sm:flex items-center gap-1", children: VIEW_OPTIONS.map((opt) => /* @__PURE__ */ jsx(
3040
- "button",
3041
- {
3042
- onClick: () => handleViewChange(opt.key),
3043
- className: `rounded-md px-3 py-1 text-sm transition-colors ${currentView === opt.key ? "bg-primary/10 text-primary font-medium" : "text-muted-foreground hover:bg-muted/50"}`,
3044
- children: t(opt.labelKey)
3045
- },
3046
- opt.key
3047
- )) }),
3048
- /* @__PURE__ */ jsx(
3049
- Button,
3050
- {
3051
- variant: "outline",
3052
- size: "icon",
3053
- onClick: () => window.location.hash = "/settings/agenda",
3054
- title: "Agenda Settings",
3055
- className: "ml-1",
3056
- children: /* @__PURE__ */ jsx(Settings, { className: "h-4 w-4" })
3057
- }
3058
- )
3059
- ] })
3310
+ /* @__PURE__ */ jsx("div", { className: "flex md:hidden items-center gap-1 overflow-x-auto px-3 pb-2", children: mobileViewOptions.map((opt) => /* @__PURE__ */ jsx(
3311
+ "button",
3312
+ {
3313
+ onClick: () => handleMobileViewChange(opt.key),
3314
+ className: `shrink-0 rounded-md px-3 py-1 text-sm transition-colors ${mobileView === opt.key ? "bg-primary/10 text-primary font-medium" : "text-muted-foreground hover:bg-muted/50"}`,
3315
+ children: t(opt.labelKey)
3316
+ },
3317
+ opt.key
3318
+ )) })
3060
3319
  ] }),
3061
- professionalsLoading && professionals.length === 0 ? /* @__PURE__ */ jsx(CalendarSkeleton, { profCount: 2 }) : /* @__PURE__ */ jsx("div", { ref: wrapperRef, className: "flex-1 overflow-hidden agenda-calendar", children: /* @__PURE__ */ jsx(
3320
+ isMobile && !mobileGridActive ? /* @__PURE__ */ jsx(
3321
+ AgendaMobileList,
3322
+ {
3323
+ bookings,
3324
+ statusColors,
3325
+ currencyCode: config.currency.code,
3326
+ onSelect: (b) => openModal("edit", { bookingId: b.id })
3327
+ }
3328
+ ) : professionalsLoading && professionals.length === 0 ? /* @__PURE__ */ jsx(CalendarSkeleton, { profCount: 2 }) : /* @__PURE__ */ jsx("div", { ref: wrapperRef, className: "flex-1 overflow-hidden agenda-calendar", children: /* @__PURE__ */ jsx(
3062
3329
  FullCalendar,
3063
3330
  {
3064
3331
  ref: calendarRef,
3065
3332
  plugins: [dayGridPlugin, timeGridPlugin, resourceTimeGridPlugin, interactionPlugin, listPlugin],
3066
- initialView: prefs.get("calendarView") === "timeGridWeek" ? "resourceTimeGridWeek" : prefs.get("calendarView"),
3333
+ initialView: mobileGridActive ? mobileView : effectiveInitialView,
3067
3334
  schedulerLicenseKey: "CC-Attribution-NonCommercial-NoDerivatives",
3068
- resources,
3335
+ resources: resourceCapable ? resources : void 0,
3069
3336
  events,
3070
3337
  headerToolbar: false,
3071
3338
  locale: fcLocale,
@@ -3155,8 +3422,14 @@ function CalendarView() {
3155
3422
  bookingId: modalState.bookingId,
3156
3423
  prefill: modalState.prefill,
3157
3424
  initialTab: modalState.initialTab,
3425
+ externalSource: modalState.externalSource,
3158
3426
  onClose: closeModal,
3159
- onCreated: (newId) => {
3427
+ onCreated: (newId, source) => {
3428
+ if (config.onBookingCreated) {
3429
+ void Promise.resolve(config.onBookingCreated({ bookingId: newId, source })).catch((error) => {
3430
+ console.error("Failed to run agenda booking-created bridge", error);
3431
+ });
3432
+ }
3160
3433
  setTimeout(() => {
3161
3434
  const booking = bookings.find((b) => b.id === newId);
3162
3435
  if (!booking) return;
@@ -3182,862 +3455,12 @@ function CalendarView() {
3182
3455
  function AgendaPage({ config, provider: provider2, store }) {
3183
3456
  const runtime = usePluginRuntimeOptional();
3184
3457
  const tenantId = runtime?.context.tenant?.id;
3185
- React11.useEffect(() => {
3458
+ React12.useEffect(() => {
3186
3459
  setAgendaTenantId(tenantId);
3187
3460
  return () => setAgendaTenantId(void 0);
3188
3461
  }, [tenantId]);
3189
3462
  return /* @__PURE__ */ jsx(AgendaContextProvider, { config, provider: provider2, store, children: /* @__PURE__ */ jsx(CalendarView, {}) });
3190
3463
  }
3191
- function getTenantId() {
3192
- return getAgendaTenantId() ?? getActiveTenantId();
3193
- }
3194
- function snakeToCamel(obj) {
3195
- const result = {};
3196
- for (const [key, value] of Object.entries(obj)) {
3197
- result[key.replace(/_([a-z])/g, (_, c) => c.toUpperCase())] = value;
3198
- }
3199
- return result;
3200
- }
3201
- function getClients() {
3202
- const supabase = getSupabaseClientOptional();
3203
- if (!supabase) throw new Error("Supabase not initialized");
3204
- return { core: supabase.schema("saas_core"), pub: supabase };
3205
- }
3206
- function mapBooking(row) {
3207
- return snakeToCamel(row);
3208
- }
3209
- function mapSchedule(row) {
3210
- return snakeToCamel(row);
3211
- }
3212
- function addMinutes(iso, minutes) {
3213
- const d = new Date(iso);
3214
- d.setMinutes(d.getMinutes() + minutes);
3215
- return d.toISOString();
3216
- }
3217
- function unique(values) {
3218
- return Array.from(new Set(values.filter((value) => !!value)));
3219
- }
3220
- function indexById(rows) {
3221
- return new Map((rows ?? []).filter((row) => row?.id).map((row) => [row.id, row]));
3222
- }
3223
- function groupBy(rows, getKey) {
3224
- const grouped = /* @__PURE__ */ new Map();
3225
- for (const row of rows) {
3226
- const key = getKey(row);
3227
- if (!key) continue;
3228
- grouped.set(key, [...grouped.get(key) ?? [], row]);
3229
- }
3230
- return grouped;
3231
- }
3232
- function getDurationMinutes(startsAt, endsAt) {
3233
- if (!endsAt) return null;
3234
- return Math.round((new Date(endsAt).getTime() - new Date(startsAt).getTime()) / 6e4);
3235
- }
3236
- function mergeBookings(viewBookings, canonicalBookings) {
3237
- const canonicalIds = new Set(canonicalBookings.map((booking) => booking.id));
3238
- const canonicalOrderIds = new Set(canonicalBookings.map((booking) => booking.orderId).filter(Boolean));
3239
- const merged = [
3240
- ...canonicalBookings,
3241
- ...viewBookings.filter((booking) => {
3242
- if (canonicalIds.has(booking.id)) return false;
3243
- if (booking.orderId && canonicalOrderIds.has(booking.orderId)) return false;
3244
- if (canonicalOrderIds.has(booking.id)) return false;
3245
- return true;
3246
- })
3247
- ];
3248
- return merged.sort((a, b) => new Date(a.startsAt).getTime() - new Date(b.startsAt).getTime());
3249
- }
3250
- async function hydrateCanonicalBookings(core, bookings) {
3251
- const personIds = unique(bookings.flatMap((booking) => [booking.party_id, booking.assignee_id]));
3252
- const locationIds = unique(bookings.map((booking) => booking.location_id));
3253
- const orderIds = unique(bookings.map((booking) => booking.order_id));
3254
- unique(bookings.map((booking) => booking.id));
3255
- const [
3256
- { data: persons, error: personsError },
3257
- { data: locations, error: locationsError },
3258
- { data: orders, error: ordersError },
3259
- { data: items, error: itemsError }
3260
- ] = await Promise.all([
3261
- personIds.length ? core.from("persons").select("id, name, phone, email, avatar_url").in("id", personIds) : Promise.resolve({ data: [], error: null }),
3262
- locationIds.length ? core.from("locations").select("id, name").in("id", locationIds) : Promise.resolve({ data: [], error: null }),
3263
- orderIds.length ? core.from("orders").select("id, total, status").in("id", orderIds) : Promise.resolve({ data: [], error: null }),
3264
- orderIds.length ? core.from("order_items").select("id, order_id, service_id, name, duration_minutes, unit_price, assignee_id, sort_order").in("order_id", orderIds) : Promise.resolve({ data: [], error: null })
3265
- ]);
3266
- if (personsError) throw personsError;
3267
- if (locationsError) throw locationsError;
3268
- if (ordersError) throw ordersError;
3269
- if (itemsError) throw itemsError;
3270
- const personsById = indexById(persons);
3271
- const locationsById = indexById(locations);
3272
- const ordersById = indexById(orders);
3273
- const itemsByOrderId = groupBy(items ?? [], (item) => item.order_id);
3274
- return bookings.map((booking) => {
3275
- const client = booking.party_id ? personsById.get(booking.party_id) : null;
3276
- const professional = booking.assignee_id ? personsById.get(booking.assignee_id) : null;
3277
- const location = booking.location_id ? locationsById.get(booking.location_id) : null;
3278
- const order = booking.order_id ? ordersById.get(booking.order_id) : null;
3279
- const bookingItems = ((booking.order_id ? itemsByOrderId.get(booking.order_id) : null) ?? []).sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)).map((item) => ({
3280
- id: item.id,
3281
- serviceId: item.service_id,
3282
- name: item.name,
3283
- durationMinutes: item.duration_minutes,
3284
- price: item.unit_price,
3285
- assigneeId: item.assignee_id
3286
- }));
3287
- const itemDuration = bookingItems.reduce((sum, item) => sum + (item.durationMinutes ?? 0), 0);
3288
- return {
3289
- id: booking.id,
3290
- tenantId: booking.tenant_id,
3291
- kind: booking.kind,
3292
- startsAt: booking.starts_at,
3293
- endsAt: booking.ends_at,
3294
- status: booking.status,
3295
- notes: booking.notes,
3296
- orderId: booking.order_id,
3297
- locationId: booking.location_id,
3298
- metadata: booking.metadata ?? {},
3299
- clientId: booking.party_id,
3300
- clientName: client?.name ?? null,
3301
- clientPhone: client?.phone ?? null,
3302
- clientEmail: client?.email ?? null,
3303
- clientAvatarUrl: client?.avatar_url ?? null,
3304
- professionalId: booking.assignee_id,
3305
- professionalName: professional?.name ?? null,
3306
- professionalAvatarUrl: professional?.avatar_url ?? null,
3307
- locationName: location?.name ?? null,
3308
- orderTotal: order?.total ?? null,
3309
- orderStatus: order?.status ?? null,
3310
- services: bookingItems.length ? bookingItems : null,
3311
- totalDurationMinutes: itemDuration || getDurationMinutes(booking.starts_at, booking.ends_at),
3312
- createdAt: booking.created_at,
3313
- updatedAt: booking.updated_at
3314
- };
3315
- });
3316
- }
3317
- async function getCanonicalBookings(core, query) {
3318
- let q = core.from("bookings").select("id, tenant_id, kind, starts_at, ends_at, status, notes, order_id, location_id, metadata, party_id, assignee_id, created_at, updated_at").gte("starts_at", query.dateRange.start).lte("starts_at", query.dateRange.end);
3319
- if (query.kind) q = q.eq("kind", query.kind);
3320
- if (query.statuses?.length) q = q.in("status", query.statuses);
3321
- if (query.professionalIds?.length) q = q.in("assignee_id", query.professionalIds);
3322
- if (query.locationId) q = q.eq("location_id", query.locationId);
3323
- const { data, error } = await q.order("starts_at", { ascending: true });
3324
- if (error) throw error;
3325
- return hydrateCanonicalBookings(core, data ?? []);
3326
- }
3327
- async function getCanonicalBookingById(core, id) {
3328
- const columns = "id, tenant_id, kind, starts_at, ends_at, status, notes, order_id, location_id, metadata, party_id, assignee_id, created_at, updated_at";
3329
- let { data, error } = await core.from("bookings").select(columns).eq("id", id).maybeSingle();
3330
- if (error) throw error;
3331
- if (!data) {
3332
- const fallback = await core.from("bookings").select(columns).eq("order_id", id).maybeSingle();
3333
- if (fallback.error) throw fallback.error;
3334
- data = fallback.data;
3335
- }
3336
- if (!data) return null;
3337
- const [booking] = await hydrateCanonicalBookings(core, [data]);
3338
- return booking ?? null;
3339
- }
3340
- function createSupabaseAgendaProvider() {
3341
- return {
3342
- // v_bookings is in public schema (PostgREST default)
3343
- async getBookings(query) {
3344
- const { core, pub } = getClients();
3345
- let q = pub.from("v_bookings").select("*").gte("starts_at", query.dateRange.start).lte("starts_at", query.dateRange.end);
3346
- if (query.kind) q = q.eq("kind", query.kind);
3347
- if (query.statuses?.length) q = q.in("status", query.statuses);
3348
- if (query.professionalIds?.length) q = q.in("professional_id", query.professionalIds);
3349
- if (query.locationId) q = q.eq("location_id", query.locationId);
3350
- q = q.order("starts_at", { ascending: true });
3351
- const { data, error } = await q;
3352
- if (error) throw error;
3353
- const viewBookings = (data ?? []).map(mapBooking);
3354
- const canonicalBookings = await getCanonicalBookings(core, query);
3355
- return mergeBookings(viewBookings, canonicalBookings);
3356
- },
3357
- async getBookingById(id) {
3358
- const { core, pub } = getClients();
3359
- const { data, error } = await pub.from("v_bookings").select("*").eq("id", id).maybeSingle();
3360
- if (error) throw error;
3361
- if (data) return mapBooking(data);
3362
- return getCanonicalBookingById(core, id);
3363
- },
3364
- async createBooking(input) {
3365
- const { core } = getClients();
3366
- const tenantId = getTenantId();
3367
- if (!tenantId) throw new Error("No active tenant");
3368
- const hasServices = input.services.length > 0;
3369
- const totalDuration = input.services.reduce((sum, s) => sum + s.durationMinutes, 0);
3370
- const totalPrice = input.services.reduce((sum, s) => sum + s.price, 0);
3371
- const endsAt = addMinutes(input.startsAt, totalDuration || 30);
3372
- const serviceNames = input.services.map((s) => s.name).join(", ");
3373
- let contactName;
3374
- if (input.clientId) {
3375
- const { data: person } = await core.from("persons").select("name").eq("id", input.clientId).single();
3376
- contactName = person?.name;
3377
- }
3378
- const { data: order, error: orderErr } = await core.from("orders").insert({
3379
- tenant_id: tenantId,
3380
- kind: input.kind ?? "appointment",
3381
- status: "scheduled",
3382
- party_id: input.clientId || null,
3383
- assignee_id: input.professionalId,
3384
- location_id: input.locationId ?? null,
3385
- subtotal: totalPrice,
3386
- total: totalPrice,
3387
- notes: input.notes ?? null,
3388
- metadata: {
3389
- source: "agenda",
3390
- ...serviceNames ? { serviceNames, itemsSummary: serviceNames } : {},
3391
- ...contactName ? { contactName } : {}
3392
- }
3393
- }).select("id").single();
3394
- if (orderErr) throw orderErr;
3395
- const { data: booking, error: bookingErr } = await core.from("bookings").insert({
3396
- tenant_id: tenantId,
3397
- kind: input.kind ?? "appointment",
3398
- party_id: input.clientId || null,
3399
- assignee_id: input.professionalId,
3400
- location_id: input.locationId ?? null,
3401
- order_id: order.id,
3402
- starts_at: input.startsAt,
3403
- ends_at: endsAt,
3404
- status: "scheduled",
3405
- notes: input.notes ?? null,
3406
- metadata: {
3407
- source: "agenda",
3408
- ...serviceNames ? { serviceNames, itemsSummary: serviceNames } : {},
3409
- ...contactName ? { contactName } : {}
3410
- }
3411
- }).select("id").single();
3412
- if (bookingErr) throw bookingErr;
3413
- if (hasServices) {
3414
- const orderItems = input.services.map((s, i) => ({
3415
- order_id: order.id,
3416
- service_id: s.serviceId,
3417
- name: s.name,
3418
- quantity: 1,
3419
- unit_price: s.price,
3420
- total: s.price,
3421
- sort_order: i,
3422
- duration_minutes: s.durationMinutes,
3423
- assignee_id: s.assigneeId ?? input.professionalId
3424
- }));
3425
- const { error: oiErr } = await core.from("order_items").insert(orderItems);
3426
- if (oiErr) throw oiErr;
3427
- }
3428
- const createdBooking = await this.getBookingById(booking.id);
3429
- if (!createdBooking) throw new Error("Created booking did not resolve from agenda read model");
3430
- return createdBooking;
3431
- },
3432
- async updateBooking(id, data) {
3433
- const { core } = getClients();
3434
- const { data: existing, error: existingErr } = await core.from("bookings").select("id, order_id, starts_at, assignee_id").eq("id", id).single();
3435
- if (existingErr) throw existingErr;
3436
- const updates = {};
3437
- if (data.startsAt) updates.starts_at = data.startsAt;
3438
- if (data.endsAt) updates.ends_at = data.endsAt;
3439
- if (data.status) {
3440
- updates.status = data.status;
3441
- }
3442
- if (data.notes !== void 0) updates.notes = data.notes;
3443
- if (data.professionalId) updates.assignee_id = data.professionalId;
3444
- if (data.clientId) updates.party_id = data.clientId;
3445
- if (data.locationId) updates.location_id = data.locationId;
3446
- if (data.services) {
3447
- const totalDuration = data.services.reduce((s, svc) => s + svc.durationMinutes, 0);
3448
- data.services.reduce((s, svc) => s + svc.price, 0);
3449
- const startsAt = data.startsAt ?? existing?.starts_at;
3450
- if (startsAt) {
3451
- const ends = new Date(new Date(startsAt).getTime() + totalDuration * 6e4);
3452
- updates.ends_at = ends.toISOString();
3453
- }
3454
- }
3455
- const effectiveAssigneeId = data.professionalId ?? existing?.assignee_id ?? null;
3456
- const { error } = await core.from("bookings").update(updates).eq("id", id);
3457
- if (error) throw error;
3458
- const orderId = existing?.order_id;
3459
- if (orderId) {
3460
- const orderUpdates = {};
3461
- if (data.notes !== void 0) orderUpdates.notes = data.notes;
3462
- if (data.professionalId) orderUpdates.assignee_id = data.professionalId;
3463
- if (data.clientId) orderUpdates.party_id = data.clientId;
3464
- if (data.locationId) orderUpdates.location_id = data.locationId;
3465
- if (data.services) {
3466
- const totalPrice = data.services.reduce((s, svc) => s + svc.price, 0);
3467
- orderUpdates.subtotal = totalPrice;
3468
- orderUpdates.total = totalPrice;
3469
- }
3470
- if (Object.keys(orderUpdates).length > 0) {
3471
- const { error: orderErr } = await core.from("orders").update(orderUpdates).eq("id", orderId);
3472
- if (orderErr) throw orderErr;
3473
- }
3474
- if (data.services) {
3475
- await core.from("order_items").delete().eq("order_id", orderId);
3476
- if (data.services.length > 0) {
3477
- const items = data.services.map((svc, idx) => ({
3478
- order_id: orderId,
3479
- service_id: svc.serviceId || null,
3480
- name: svc.name,
3481
- quantity: 1,
3482
- unit_price: svc.price,
3483
- total: svc.price,
3484
- sort_order: idx,
3485
- duration_minutes: svc.durationMinutes,
3486
- assignee_id: svc.assigneeId ?? effectiveAssigneeId
3487
- }));
3488
- const { error: itemErr } = await core.from("order_items").insert(items);
3489
- if (itemErr) throw itemErr;
3490
- }
3491
- }
3492
- }
3493
- return await this.getBookingById(id);
3494
- },
3495
- async deleteBooking(id) {
3496
- const { core, pub } = getClients();
3497
- const { data: booking, error: bookingErr } = await core.from("bookings").select("order_id").eq("id", id).single();
3498
- if (bookingErr) throw bookingErr;
3499
- const orderId = booking?.order_id;
3500
- const { count, error: movementErr } = orderId ? await pub.from("financial_movements").select("id", { count: "exact", head: true }).eq("invoice_id", orderId) : { count: 0, error: null };
3501
- if (movementErr) throw movementErr;
3502
- if (orderId && (count ?? 0) > 0) {
3503
- await pub.from("financial_movements").update({ status: "cancelled" }).eq("invoice_id", orderId);
3504
- await core.from("orders").update({ status: "cancelled" }).eq("id", orderId);
3505
- const { error } = await core.from("bookings").update({ status: "cancelled" }).eq("id", id);
3506
- if (error) throw error;
3507
- } else {
3508
- if (orderId) {
3509
- await pub.from("financial_movements").delete().eq("invoice_id", orderId);
3510
- }
3511
- const { error } = await core.from("bookings").delete().eq("id", id);
3512
- if (error) throw error;
3513
- if (orderId) {
3514
- const { error: orderErr } = await core.from("orders").delete().eq("id", orderId);
3515
- if (orderErr) throw orderErr;
3516
- }
3517
- }
3518
- },
3519
- async updateBookingStatus(id, status) {
3520
- return this.updateBooking(id, { status });
3521
- },
3522
- async checkConflict(params) {
3523
- const { core } = getClients();
3524
- let q = core.from("bookings").select("id", { count: "exact", head: true }).eq("assignee_id", params.assigneeId).eq("kind", "appointment").not("status", "in", '("cancelled","no_show")').not("starts_at", "is", null).lt("starts_at", params.endsAt).gt("ends_at", params.startsAt);
3525
- if (params.excludeBookingId) {
3526
- q = q.neq("id", params.excludeBookingId);
3527
- }
3528
- const { count, error } = await q;
3529
- if (error) throw error;
3530
- return (count ?? 0) > 0;
3531
- },
3532
- async getAvailableSlots(params) {
3533
- const { pub } = getClients();
3534
- const { data, error } = await pub.rpc("get_available_slots", {
3535
- p_tenant_id: getTenantId(),
3536
- p_assignee_id: params.assigneeId,
3537
- p_date: params.date,
3538
- p_duration_minutes: params.durationMinutes,
3539
- p_slot_interval: params.slotInterval ?? 30
3540
- });
3541
- if (error) throw error;
3542
- return (data ?? []).map((row) => ({ start: row.slot_start, end: row.slot_end }));
3543
- },
3544
- async getSchedules(professionalId) {
3545
- const { core } = getClients();
3546
- let q = core.from("schedules").select("*").eq("kind", "working_hours");
3547
- if (professionalId) q = q.eq("assignee_id", professionalId);
3548
- q = q.order("day_of_week").order("starts_at");
3549
- const { data, error } = await q;
3550
- if (error) throw error;
3551
- return (data ?? []).map(mapSchedule);
3552
- },
3553
- async saveSchedule(input) {
3554
- const { core } = getClients();
3555
- const tenantId = getTenantId();
3556
- if (!tenantId) throw new Error("No active tenant");
3557
- const { data, error } = await core.from("schedules").insert({
3558
- tenant_id: tenantId,
3559
- kind: "working_hours",
3560
- assignee_id: input.assigneeId,
3561
- location_id: input.locationId ?? null,
3562
- day_of_week: input.dayOfWeek ?? null,
3563
- specific_date: input.specificDate ?? null,
3564
- starts_at: input.startsAt,
3565
- ends_at: input.endsAt,
3566
- is_active: input.isActive !== false,
3567
- metadata: input.metadata ?? {}
3568
- }).select("*").single();
3569
- if (error) throw error;
3570
- return mapSchedule(data);
3571
- },
3572
- async deleteSchedule(id) {
3573
- const { core } = getClients();
3574
- const { error } = await core.from("schedules").delete().eq("id", id);
3575
- if (error) throw error;
3576
- },
3577
- async getProfessionals() {
3578
- const { core } = getClients();
3579
- const { data, error } = await core.from("persons").select("id, name, avatar_url, is_active").eq("kind", "staff").eq("is_active", true).order("name");
3580
- if (error) throw error;
3581
- return (data ?? []).map((p) => ({
3582
- id: p.id,
3583
- name: p.name,
3584
- avatarUrl: p.avatar_url,
3585
- locationId: null,
3586
- locationName: null,
3587
- isActive: p.is_active
3588
- }));
3589
- },
3590
- async getLocations() {
3591
- const { core } = getClients();
3592
- const { data, error } = await core.from("locations").select("id, name").eq("is_active", true).order("name");
3593
- if (error) throw error;
3594
- return (data ?? []).map((l) => ({ id: l.id, name: l.name }));
3595
- },
3596
- async getConfirmationsPending(daysAhead = 2) {
3597
- const { pub } = getClients();
3598
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
3599
- const cutoff = /* @__PURE__ */ new Date();
3600
- cutoff.setDate(cutoff.getDate() + daysAhead);
3601
- const { data, error } = await pub.from("v_bookings").select("*").eq("status", "scheduled").gte("starts_at", now2).lte("starts_at", cutoff.toISOString()).order("starts_at");
3602
- if (error) throw error;
3603
- return (data ?? []).map(mapBooking);
3604
- },
3605
- async sendConfirmation(bookingId, channel) {
3606
- const { core } = getClients();
3607
- const { data: booking } = await core.from("bookings").select("metadata").eq("id", bookingId).single();
3608
- const metadata = booking?.metadata ?? {};
3609
- const confirmations = metadata.confirmations ?? [];
3610
- confirmations.push({ channel, sentAt: (/* @__PURE__ */ new Date()).toISOString(), status: "sent" });
3611
- await core.from("bookings").update({ metadata: { ...metadata, confirmations } }).eq("id", bookingId);
3612
- },
3613
- async completeBooking(bookingId) {
3614
- const { core } = getClients();
3615
- const { data: existing, error: existingErr } = await core.from("bookings").select("order_id").eq("id", bookingId).single();
3616
- if (existingErr) throw existingErr;
3617
- const { error } = await core.from("bookings").update({ status: "completed" }).eq("id", bookingId);
3618
- if (error) throw error;
3619
- const orderId = existing?.order_id;
3620
- const booking = await this.getBookingById(bookingId);
3621
- if (!booking) throw new Error("Booking not found");
3622
- return { booking, orderId: orderId ?? bookingId };
3623
- }
3624
- };
3625
- }
3626
-
3627
- // src/data/mock.ts
3628
- var nextId = 1;
3629
- function uid() {
3630
- return `mock-${nextId++}`;
3631
- }
3632
- function now() {
3633
- return (/* @__PURE__ */ new Date()).toISOString();
3634
- }
3635
- function addMinutes2(date, minutes) {
3636
- const d = new Date(date);
3637
- d.setMinutes(d.getMinutes() + minutes);
3638
- return d.toISOString();
3639
- }
3640
- function timeOverlaps(aStart, aEnd, bStart, bEnd) {
3641
- return aStart < bEnd && aEnd > bStart;
3642
- }
3643
- var SAMPLE_PROFESSIONALS = [
3644
- { id: "prof-1", name: "Ana Silva", avatarUrl: null, locationId: "loc-1", locationName: "Main Branch", isActive: true },
3645
- { id: "prof-2", name: "Carlos Santos", avatarUrl: null, locationId: "loc-1", locationName: "Main Branch", isActive: true },
3646
- { id: "prof-3", name: "Maria Oliveira", avatarUrl: null, locationId: "loc-1", locationName: "Main Branch", isActive: true }
3647
- ];
3648
- function createSampleBookings() {
3649
- const today = /* @__PURE__ */ new Date();
3650
- const year = today.getFullYear();
3651
- const month = today.getMonth();
3652
- const day = today.getDate();
3653
- function dt(hour, minute = 0) {
3654
- return new Date(year, month, day, hour, minute).toISOString();
3655
- }
3656
- return [
3657
- {
3658
- id: "bk-1",
3659
- tenantId: "mock",
3660
- kind: "appointment",
3661
- startsAt: dt(9, 0),
3662
- endsAt: dt(10, 30),
3663
- status: "confirmed",
3664
- notes: null,
3665
- orderId: "ord-1",
3666
- locationId: "loc-1",
3667
- metadata: {},
3668
- clientId: "cl-1",
3669
- clientName: "Sarah Johnson",
3670
- clientPhone: "(11) 99999-1111",
3671
- clientEmail: "sarah@email.com",
3672
- clientAvatarUrl: null,
3673
- professionalId: "prof-1",
3674
- professionalName: "Ana Silva",
3675
- professionalAvatarUrl: null,
3676
- locationName: "Main Branch",
3677
- orderTotal: 250,
3678
- orderStatus: "draft",
3679
- services: [{ id: "bi-1", serviceId: "svc-1", name: "Balayage", durationMinutes: 90, price: 250, assigneeId: "prof-1" }],
3680
- totalDurationMinutes: 90,
3681
- createdAt: now(),
3682
- updatedAt: now()
3683
- },
3684
- {
3685
- id: "bk-2",
3686
- tenantId: "mock",
3687
- kind: "appointment",
3688
- startsAt: dt(10, 0),
3689
- endsAt: dt(11, 0),
3690
- status: "scheduled",
3691
- notes: null,
3692
- orderId: "ord-2",
3693
- locationId: "loc-1",
3694
- metadata: {},
3695
- clientId: "cl-2",
3696
- clientName: "Emily Carter",
3697
- clientPhone: "(11) 99999-2222",
3698
- clientEmail: "emily@email.com",
3699
- clientAvatarUrl: null,
3700
- professionalId: "prof-2",
3701
- professionalName: "Carlos Santos",
3702
- professionalAvatarUrl: null,
3703
- locationName: "Main Branch",
3704
- orderTotal: 80,
3705
- orderStatus: "draft",
3706
- services: [{ id: "bi-2", serviceId: "svc-2", name: "Haircut", durationMinutes: 60, price: 80, assigneeId: "prof-2" }],
3707
- totalDurationMinutes: 60,
3708
- createdAt: now(),
3709
- updatedAt: now()
3710
- },
3711
- {
3712
- id: "bk-3",
3713
- tenantId: "mock",
3714
- kind: "appointment",
3715
- startsAt: dt(14, 0),
3716
- endsAt: dt(15, 0),
3717
- status: "confirmed",
3718
- notes: "First visit",
3719
- orderId: "ord-3",
3720
- locationId: "loc-1",
3721
- metadata: {},
3722
- clientId: "cl-3",
3723
- clientName: "Rachel Kim",
3724
- clientPhone: "(11) 99999-3333",
3725
- clientEmail: null,
3726
- clientAvatarUrl: null,
3727
- professionalId: "prof-3",
3728
- professionalName: "Maria Oliveira",
3729
- professionalAvatarUrl: null,
3730
- locationName: "Main Branch",
3731
- orderTotal: 120,
3732
- orderStatus: "draft",
3733
- services: [{ id: "bi-3", serviceId: "svc-3", name: "Facial Treatment", durationMinutes: 60, price: 120, assigneeId: "prof-3" }],
3734
- totalDurationMinutes: 60,
3735
- createdAt: now(),
3736
- updatedAt: now()
3737
- },
3738
- {
3739
- id: "bk-4",
3740
- tenantId: "mock",
3741
- kind: "appointment",
3742
- startsAt: dt(11, 0),
3743
- endsAt: dt(12, 0),
3744
- status: "scheduled",
3745
- notes: null,
3746
- orderId: "ord-4",
3747
- locationId: "loc-1",
3748
- metadata: {},
3749
- clientId: "cl-4",
3750
- clientName: "Jessica Lima",
3751
- clientPhone: "(11) 99999-4444",
3752
- clientEmail: null,
3753
- clientAvatarUrl: null,
3754
- professionalId: "prof-1",
3755
- professionalName: "Ana Silva",
3756
- professionalAvatarUrl: null,
3757
- locationName: "Main Branch",
3758
- orderTotal: 60,
3759
- orderStatus: "draft",
3760
- services: [{ id: "bi-4", serviceId: "svc-4", name: "Manicure", durationMinutes: 60, price: 60, assigneeId: "prof-1" }],
3761
- totalDurationMinutes: 60,
3762
- createdAt: now(),
3763
- updatedAt: now()
3764
- },
3765
- {
3766
- id: "bk-5",
3767
- tenantId: "mock",
3768
- kind: "appointment",
3769
- startsAt: dt(15, 30),
3770
- endsAt: dt(17, 0),
3771
- status: "confirmed",
3772
- notes: null,
3773
- orderId: "ord-5",
3774
- locationId: "loc-1",
3775
- metadata: {},
3776
- clientId: "cl-5",
3777
- clientName: "Amanda Wright",
3778
- clientPhone: "(11) 99999-5555",
3779
- clientEmail: "amanda@email.com",
3780
- clientAvatarUrl: null,
3781
- professionalId: "prof-2",
3782
- professionalName: "Carlos Santos",
3783
- professionalAvatarUrl: null,
3784
- locationName: "Main Branch",
3785
- orderTotal: 350,
3786
- orderStatus: "draft",
3787
- services: [
3788
- { id: "bi-5", serviceId: "svc-1", name: "Highlights", durationMinutes: 60, price: 200, assigneeId: "prof-2" },
3789
- { id: "bi-6", serviceId: "svc-2", name: "Haircut", durationMinutes: 30, price: 150, assigneeId: "prof-2" }
3790
- ],
3791
- totalDurationMinutes: 90,
3792
- createdAt: now(),
3793
- updatedAt: now()
3794
- }
3795
- ];
3796
- }
3797
- function createSampleSchedules() {
3798
- const schedules = [];
3799
- for (const prof of SAMPLE_PROFESSIONALS) {
3800
- for (let day = 1; day <= 5; day++) {
3801
- schedules.push({
3802
- id: uid(),
3803
- tenantId: "mock",
3804
- kind: "working_hours",
3805
- assigneeId: prof.id,
3806
- locationId: prof.locationId,
3807
- dayOfWeek: day,
3808
- specificDate: null,
3809
- startsAt: "08:00",
3810
- endsAt: "12:00",
3811
- isActive: true,
3812
- metadata: {}
3813
- });
3814
- schedules.push({
3815
- id: uid(),
3816
- tenantId: "mock",
3817
- kind: "working_hours",
3818
- assigneeId: prof.id,
3819
- locationId: prof.locationId,
3820
- dayOfWeek: day,
3821
- specificDate: null,
3822
- startsAt: "13:00",
3823
- endsAt: "18:00",
3824
- isActive: true,
3825
- metadata: {}
3826
- });
3827
- }
3828
- schedules.push({
3829
- id: uid(),
3830
- tenantId: "mock",
3831
- kind: "working_hours",
3832
- assigneeId: prof.id,
3833
- locationId: prof.locationId,
3834
- dayOfWeek: 6,
3835
- specificDate: null,
3836
- startsAt: "08:00",
3837
- endsAt: "14:00",
3838
- isActive: true,
3839
- metadata: {}
3840
- });
3841
- }
3842
- return schedules;
3843
- }
3844
- function createMockAgendaProvider() {
3845
- const bookings = createSampleBookings();
3846
- const schedules = createSampleSchedules();
3847
- const professionals = [...SAMPLE_PROFESSIONALS];
3848
- return {
3849
- async getLocations() {
3850
- return [];
3851
- },
3852
- async getBookings(query) {
3853
- return bookings.filter((b) => {
3854
- if (query.kind && b.kind !== query.kind) return false;
3855
- if (query.statuses?.length && !query.statuses.includes(b.status)) return false;
3856
- if (query.professionalIds?.length && !query.professionalIds.includes(b.professionalId ?? "")) return false;
3857
- if (query.locationId && b.locationId !== query.locationId) return false;
3858
- if (query.dateRange) {
3859
- if (b.startsAt > query.dateRange.end || (b.endsAt ?? b.startsAt) < query.dateRange.start) return false;
3860
- }
3861
- return true;
3862
- });
3863
- },
3864
- async getBookingById(id) {
3865
- return bookings.find((b) => b.id === id) ?? null;
3866
- },
3867
- async createBooking(input) {
3868
- const totalDuration = input.services.reduce((sum, s) => sum + s.durationMinutes, 0);
3869
- const totalPrice = input.services.reduce((sum, s) => sum + s.price, 0);
3870
- const booking = {
3871
- id: uid(),
3872
- tenantId: "mock",
3873
- kind: input.kind ?? "appointment",
3874
- startsAt: input.startsAt,
3875
- endsAt: addMinutes2(input.startsAt, totalDuration || 30),
3876
- status: "scheduled",
3877
- notes: input.notes ?? null,
3878
- orderId: input.services.length > 0 ? uid() : null,
3879
- locationId: input.locationId ?? null,
3880
- metadata: {},
3881
- clientId: input.clientId,
3882
- clientName: "New Client",
3883
- clientPhone: null,
3884
- clientEmail: null,
3885
- clientAvatarUrl: null,
3886
- professionalId: input.professionalId,
3887
- professionalName: professionals.find((p) => p.id === input.professionalId)?.name ?? "Unknown",
3888
- professionalAvatarUrl: null,
3889
- locationName: null,
3890
- orderTotal: totalPrice,
3891
- orderStatus: "draft",
3892
- services: input.services.map((s) => ({
3893
- id: uid(),
3894
- serviceId: s.serviceId,
3895
- name: s.name,
3896
- durationMinutes: s.durationMinutes,
3897
- price: s.price,
3898
- assigneeId: s.assigneeId ?? input.professionalId
3899
- })),
3900
- totalDurationMinutes: totalDuration,
3901
- createdAt: now(),
3902
- updatedAt: now()
3903
- };
3904
- bookings.push(booking);
3905
- return booking;
3906
- },
3907
- async updateBooking(id, data) {
3908
- const idx = bookings.findIndex((b) => b.id === id);
3909
- if (idx === -1) throw new Error("Booking not found");
3910
- const existing = bookings[idx];
3911
- const serviceUpdates = {};
3912
- if (data.services) {
3913
- serviceUpdates.services = data.services.map((s) => ({
3914
- id: uid(),
3915
- serviceId: s.serviceId,
3916
- name: s.name,
3917
- durationMinutes: s.durationMinutes,
3918
- price: s.price,
3919
- assigneeId: s.assigneeId ?? existing.professionalId ?? ""
3920
- }));
3921
- serviceUpdates.totalDurationMinutes = data.services.reduce((sum, s) => sum + s.durationMinutes, 0);
3922
- serviceUpdates.orderTotal = data.services.reduce((sum, s) => sum + s.price, 0);
3923
- }
3924
- const updated = {
3925
- ...existing,
3926
- ...data.startsAt && { startsAt: data.startsAt },
3927
- ...data.endsAt && { endsAt: data.endsAt },
3928
- ...data.status && { status: data.status },
3929
- ...data.notes !== void 0 && { notes: data.notes ?? null },
3930
- ...data.professionalId && { professionalId: data.professionalId },
3931
- ...data.clientId && { clientId: data.clientId },
3932
- ...data.locationId && { locationId: data.locationId },
3933
- ...serviceUpdates,
3934
- updatedAt: now()
3935
- };
3936
- bookings[idx] = updated;
3937
- return updated;
3938
- },
3939
- async deleteBooking(id) {
3940
- const idx = bookings.findIndex((b) => b.id === id);
3941
- if (idx !== -1) bookings.splice(idx, 1);
3942
- },
3943
- async updateBookingStatus(id, status) {
3944
- const idx = bookings.findIndex((b) => b.id === id);
3945
- if (idx === -1) throw new Error("Booking not found");
3946
- bookings[idx] = { ...bookings[idx], status, updatedAt: now() };
3947
- return bookings[idx];
3948
- },
3949
- async checkConflict(params) {
3950
- return bookings.some((b) => {
3951
- if (params.excludeBookingId && b.id === params.excludeBookingId) return false;
3952
- if (b.professionalId !== params.assigneeId) return false;
3953
- if (b.status === "cancelled" || b.status === "no_show") return false;
3954
- return timeOverlaps(b.startsAt, b.endsAt ?? b.startsAt, params.startsAt, params.endsAt);
3955
- });
3956
- },
3957
- async getAvailableSlots(params) {
3958
- const date = new Date(params.date);
3959
- const dayOfWeek = date.getDay();
3960
- const interval = params.slotInterval ?? 30;
3961
- const daySchedules = schedules.filter(
3962
- (s) => s.assigneeId === params.assigneeId && s.isActive && (s.dayOfWeek === dayOfWeek || s.specificDate === params.date)
3963
- );
3964
- const slots = [];
3965
- for (const sched of daySchedules) {
3966
- const [startH, startM] = sched.startsAt.split(":").map(Number);
3967
- const [endH, endM] = sched.endsAt.split(":").map(Number);
3968
- const schedStart = new Date(date);
3969
- schedStart.setHours(startH, startM, 0, 0);
3970
- const schedEnd = new Date(date);
3971
- schedEnd.setHours(endH, endM, 0, 0);
3972
- let slotStart = new Date(schedStart);
3973
- while (true) {
3974
- const slotEnd = new Date(slotStart.getTime() + params.durationMinutes * 6e4);
3975
- if (slotEnd > schedEnd) break;
3976
- const startIso = slotStart.toISOString();
3977
- const endIso = slotEnd.toISOString();
3978
- const hasConflict = bookings.some((b) => {
3979
- if (b.professionalId !== params.assigneeId) return false;
3980
- if (b.status === "cancelled" || b.status === "no_show") return false;
3981
- return timeOverlaps(b.startsAt, b.endsAt ?? b.startsAt, startIso, endIso);
3982
- });
3983
- if (!hasConflict) {
3984
- slots.push({ start: startIso, end: endIso });
3985
- }
3986
- slotStart = new Date(slotStart.getTime() + interval * 6e4);
3987
- }
3988
- }
3989
- return slots;
3990
- },
3991
- async getSchedules(professionalId) {
3992
- if (professionalId) return schedules.filter((s) => s.assigneeId === professionalId);
3993
- return [...schedules];
3994
- },
3995
- async saveSchedule(input) {
3996
- const schedule = {
3997
- id: uid(),
3998
- tenantId: "mock",
3999
- kind: "working_hours",
4000
- assigneeId: input.assigneeId,
4001
- locationId: input.locationId ?? null,
4002
- dayOfWeek: input.dayOfWeek ?? null,
4003
- specificDate: input.specificDate ?? null,
4004
- startsAt: input.startsAt,
4005
- endsAt: input.endsAt,
4006
- isActive: input.isActive !== false,
4007
- metadata: input.metadata ?? {}
4008
- };
4009
- schedules.push(schedule);
4010
- return schedule;
4011
- },
4012
- async deleteSchedule(id) {
4013
- const idx = schedules.findIndex((s) => s.id === id);
4014
- if (idx !== -1) schedules.splice(idx, 1);
4015
- },
4016
- async getProfessionals() {
4017
- return [...professionals];
4018
- },
4019
- async getConfirmationsPending(daysAhead = 2) {
4020
- const cutoff = /* @__PURE__ */ new Date();
4021
- cutoff.setDate(cutoff.getDate() + daysAhead);
4022
- return bookings.filter(
4023
- (b) => b.status === "scheduled" && new Date(b.startsAt) <= cutoff && new Date(b.startsAt) >= /* @__PURE__ */ new Date()
4024
- );
4025
- },
4026
- async sendConfirmation(bookingId, channel) {
4027
- const booking = bookings.find((b) => b.id === bookingId);
4028
- if (!booking) throw new Error("Booking not found");
4029
- const confirmations = booking.metadata.confirmations ?? [];
4030
- confirmations.push({ channel, sentAt: now(), status: "sent" });
4031
- booking.metadata = { ...booking.metadata, confirmations };
4032
- },
4033
- async completeBooking(bookingId) {
4034
- const idx = bookings.findIndex((b) => b.id === bookingId);
4035
- if (idx === -1) throw new Error("Booking not found");
4036
- bookings[idx] = { ...bookings[idx], status: "completed", updatedAt: now() };
4037
- return { booking: bookings[idx], orderId: bookings[idx].orderId ?? "" };
4038
- }
4039
- };
4040
- }
4041
3464
  function getCurrentBookingQuery(state, dateRange = state.dateRange) {
4042
3465
  return {
4043
3466
  dateRange,
@@ -4233,7 +3656,7 @@ function createAgendaStore(provider2, financialBridge) {
4233
3656
  set({ selectedDate: date });
4234
3657
  },
4235
3658
  openAppointmentModal(mode, options) {
4236
- set({ appointmentModal: { open: true, mode, bookingId: options?.bookingId, prefill: options?.prefill, initialTab: options?.initialTab } });
3659
+ set({ appointmentModal: { open: true, mode, bookingId: options?.bookingId, prefill: options?.prefill, initialTab: options?.initialTab, externalSource: options?.externalSource } });
4237
3660
  },
4238
3661
  closeAppointmentModal() {
4239
3662
  set((state) => ({ appointmentModal: { ...state.appointmentModal, open: false } }));
@@ -4424,8 +3847,12 @@ var en = {
4424
3847
  "agenda.calendar.today": "Today",
4425
3848
  "agenda.calendar.viewAgenda": "Agenda",
4426
3849
  "agenda.calendar.viewDay": "Day",
3850
+ "agenda.calendar.viewList": "List",
4427
3851
  "agenda.calendar.viewMonth": "Month",
4428
3852
  "agenda.calendar.viewWeek": "Week",
3853
+ "agenda.list.today": "Today",
3854
+ "agenda.list.tomorrow": "Tomorrow",
3855
+ "agenda.list.empty": "No upcoming items",
4429
3856
  "agenda.confirmations.confirmed": "Confirmed:",
4430
3857
  "agenda.confirmations.empty": "No pending confirmations",
4431
3858
  "agenda.confirmations.emptySubtitle": "All upcoming appointments have been confirmed",
@@ -4436,6 +3863,10 @@ var en = {
4436
3863
  "agenda.confirmations.call": "Call",
4437
3864
  "agenda.confirmations.markConfirmed": "Mark Confirmed",
4438
3865
  "agenda.confirmations.markCancelled": "Mark Cancelled",
3866
+ "agenda.cancellation.loadingReasons": "Loading reasons...",
3867
+ "agenda.cancellation.notesOptional": "Cancellation notes (optional)",
3868
+ "agenda.cancellation.notesRequired": "Cancellation notes required",
3869
+ "agenda.cancellation.selectReason": "Select cancellation reason",
4439
3870
  "agenda.contextMenu.changeStatus": "Change status",
4440
3871
  "agenda.contextMenu.delete": "Delete",
4441
3872
  "agenda.contextMenu.edit": "Edit",
@@ -4536,7 +3967,11 @@ var en = {
4536
3967
  "agenda.appointment.addNotes": "Add notes",
4537
3968
  "agenda.appointment.duration": "Duration (minutes)",
4538
3969
  "agenda.appointment.price": "Price",
3970
+ "agenda.appointment.title": "Title",
3971
+ "agenda.appointment.addTitle": "Add a title",
3972
+ "agenda.appointment.untitled": "(no title)",
4539
3973
  "agenda.bookingType.appointment": "Appointment",
3974
+ "agenda.bookingType.event": "Event",
4540
3975
  "agenda.bookingType.task": "Task",
4541
3976
  "agenda.bookingType.out_of_office": "Out of Office",
4542
3977
  "agenda.bookingType.block": "Block",
@@ -4632,8 +4067,12 @@ var ptBR = {
4632
4067
  "agenda.calendar.today": "Hoje",
4633
4068
  "agenda.calendar.viewAgenda": "Agenda",
4634
4069
  "agenda.calendar.viewDay": "Dia",
4070
+ "agenda.calendar.viewList": "Lista",
4635
4071
  "agenda.calendar.viewMonth": "M\xEAs",
4636
4072
  "agenda.calendar.viewWeek": "Semana",
4073
+ "agenda.list.today": "Hoje",
4074
+ "agenda.list.tomorrow": "Amanh\xE3",
4075
+ "agenda.list.empty": "Nenhum item pr\xF3ximo",
4637
4076
  "agenda.confirmations.confirmed": "Confirmado:",
4638
4077
  "agenda.confirmations.empty": "Nenhuma confirma\xE7\xE3o pendente",
4639
4078
  "agenda.confirmations.emptySubtitle": "Todos os agendamentos futuros foram confirmados",
@@ -4644,6 +4083,10 @@ var ptBR = {
4644
4083
  "agenda.confirmations.call": "Ligar",
4645
4084
  "agenda.confirmations.markConfirmed": "Marcar Confirmado",
4646
4085
  "agenda.confirmations.markCancelled": "Marcar Cancelado",
4086
+ "agenda.cancellation.loadingReasons": "Carregando motivos...",
4087
+ "agenda.cancellation.notesOptional": "Observa\xE7\xF5es do cancelamento (opcional)",
4088
+ "agenda.cancellation.notesRequired": "Observa\xE7\xF5es do cancelamento obrigat\xF3rias",
4089
+ "agenda.cancellation.selectReason": "Selecionar motivo do cancelamento",
4647
4090
  "agenda.contextMenu.changeStatus": "Alterar status",
4648
4091
  "agenda.contextMenu.delete": "Excluir",
4649
4092
  "agenda.contextMenu.edit": "Editar",
@@ -4744,7 +4187,11 @@ var ptBR = {
4744
4187
  "agenda.appointment.addNotes": "Adicionar observa\xE7\xF5es",
4745
4188
  "agenda.appointment.duration": "Dura\xE7\xE3o (minutos)",
4746
4189
  "agenda.appointment.price": "Pre\xE7o",
4190
+ "agenda.appointment.title": "T\xEDtulo",
4191
+ "agenda.appointment.addTitle": "Adicionar um t\xEDtulo",
4192
+ "agenda.appointment.untitled": "(sem t\xEDtulo)",
4747
4193
  "agenda.bookingType.appointment": "Agendamento",
4194
+ "agenda.bookingType.event": "Evento",
4748
4195
  "agenda.bookingType.task": "Tarefa",
4749
4196
  "agenda.bookingType.out_of_office": "Aus\xEAncia",
4750
4197
  "agenda.bookingType.block": "Bloqueio",
@@ -5180,6 +4627,7 @@ function createAgendaPlugin(options) {
5180
4627
  () => createMockAgendaProvider()
5181
4628
  );
5182
4629
  const store = createAgendaStore(provider2, options?.financialBridge);
4630
+ const registries = [...agendaRegistries, ...options?.settingsRegistries ?? []];
5183
4631
  setScheduleBlockConfig({
5184
4632
  defaults: config.scheduleBlockDefaults,
5185
4633
  showServices: !!config.serviceLookup,
@@ -5195,7 +4643,7 @@ function createAgendaPlugin(options) {
5195
4643
  return locations;
5196
4644
  } : void 0
5197
4645
  });
5198
- const PageComponent = () => React11.createElement(AgendaPage, { config, provider: provider2, store });
4646
+ const PageComponent = () => React12.createElement(AgendaPage, { config, provider: provider2, store });
5199
4647
  return {
5200
4648
  id: "agenda",
5201
4649
  name: config.labels.pageTitle,
@@ -5203,6 +4651,9 @@ function createAgendaPlugin(options) {
5203
4651
  version: "1.0.0",
5204
4652
  scope: options?.scope ?? "universal",
5205
4653
  verticalId: options?.verticalId,
4654
+ // Booking/scheduling is business-ops (clinic, salon, services) — targets
4655
+ // the saas/admin Panel world, not the ecommerce storefront.
4656
+ scaffolds: ["saas"],
5206
4657
  defaultEnabled: true,
5207
4658
  dependencies: [],
5208
4659
  declaredFeatures: [
@@ -5295,18 +4746,18 @@ function createAgendaPlugin(options) {
5295
4746
  permission: { feature: "appointments", action: "read" }
5296
4747
  }
5297
4748
  ],
5298
- registries: agendaRegistries,
4749
+ registries,
5299
4750
  settings: [
5300
4751
  {
5301
4752
  id: "agenda",
5302
4753
  label: "Agenda",
5303
4754
  icon: "Calendar",
5304
- // Standard layout: General | Integrations (Google Calendar addon) | Properties (holidays).
4755
+ // Standard layout: General | Integrations (Google Calendar addon) | Properties.
5305
4756
  component: (() => {
5306
- const AgendaSettingsTab = () => React11.createElement(PluginSettingsPanel, {
4757
+ const AgendaSettingsTab = () => React12.createElement(PluginSettingsPanel, {
5307
4758
  title: "Agenda",
5308
- generalSettings: React11.createElement(AgendaGeneralSettings),
5309
- registries: agendaRegistries,
4759
+ generalSettings: React12.createElement(AgendaGeneralSettings),
4760
+ registries,
5310
4761
  hostPluginId: "agenda",
5311
4762
  routeBase: "/settings/agenda"
5312
4763
  });