@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.
- package/dist/chunk-GAJVIEXM.cjs +870 -0
- package/dist/chunk-GAJVIEXM.cjs.map +1 -0
- package/dist/chunk-U6VTLOHN.js +867 -0
- package/dist/chunk-U6VTLOHN.js.map +1 -0
- package/dist/components/AgendaMobileList.d.ts +10 -0
- package/dist/components/AgendaMobileList.d.ts.map +1 -0
- package/dist/components/AppointmentModal.d.ts +4 -2
- package/dist/components/AppointmentModal.d.ts.map +1 -1
- package/dist/components/AppointmentPopover.d.ts.map +1 -1
- package/dist/components/EventContextMenu.d.ts.map +1 -1
- package/dist/config.d.ts +62 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/data/index.d.ts +1 -0
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/mock.d.ts +15 -1
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/index.cjs +611 -1157
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +445 -994
- package/dist/index.js.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/public/BookingWidget.d.ts +9 -0
- package/dist/public/BookingWidget.d.ts.map +1 -0
- package/dist/public/context.d.ts +37 -0
- package/dist/public/context.d.ts.map +1 -0
- package/dist/public/createPublicBookingPlugin.d.ts +45 -0
- package/dist/public/createPublicBookingPlugin.d.ts.map +1 -0
- package/dist/public/hooks.d.ts +32 -0
- package/dist/public/hooks.d.ts.map +1 -0
- package/dist/public/index.cjs +1020 -0
- package/dist/public/index.cjs.map +1 -0
- package/dist/public/index.d.ts +9 -0
- package/dist/public/index.d.ts.map +1 -0
- package/dist/public/index.js +1012 -0
- package/dist/public/index.js.map +1 -0
- package/dist/public/types.d.ts +51 -0
- package/dist/public/types.d.ts.map +1 -0
- package/dist/store.d.ts +3 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/CalendarView.d.ts.map +1 -1
- package/package.json +11 -8
- package/src/components/AgendaMobileList.tsx +103 -0
- package/src/components/AppointmentModal.tsx +86 -9
- package/src/components/AppointmentPopover.tsx +20 -10
- package/src/components/EventContextMenu.tsx +3 -0
- package/src/config.ts +104 -5
- package/src/data/index.ts +1 -0
- package/src/data/mock.ts +26 -6
- package/src/data/supabase.ts +13 -2
- package/src/index.ts +11 -3
- package/src/integrations/google-calendar/integration.test.ts +1 -1
- package/src/locales/en.ts +12 -0
- package/src/locales/pt-BR.ts +12 -0
- package/src/public/BookingWidget.tsx +892 -0
- package/src/public/context.tsx +43 -0
- package/src/public/createPublicBookingPlugin.ts +155 -0
- package/src/public/hooks.ts +117 -0
- package/src/public/index.ts +15 -0
- package/src/public/types.ts +63 -0
- package/src/store.ts +4 -2
- package/src/types.ts +8 -0
- package/src/views/CalendarView.tsx +220 -54
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import {
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
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] =
|
|
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:
|
|
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
|
-
|
|
852
|
+
formatCurrency2(mov.paidAmount),
|
|
825
853
|
"/",
|
|
826
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 ?
|
|
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")} ${
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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:
|
|
1658
|
-
const supabase =
|
|
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:
|
|
1806
|
-
const { getAgendaTenantId
|
|
1807
|
-
const supabase =
|
|
1808
|
-
const tenantId =
|
|
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
|
|
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
|
|
2891
|
-
|
|
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__ */
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
/* @__PURE__ */
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
{
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
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: "
|
|
3015
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center
|
|
3016
|
-
/* @__PURE__ */
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
"
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
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__ */
|
|
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__ */
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 = () =>
|
|
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
|
|
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
|
|
4755
|
+
// Standard layout: General | Integrations (Google Calendar addon) | Properties.
|
|
5305
4756
|
component: (() => {
|
|
5306
|
-
const AgendaSettingsTab = () =>
|
|
4757
|
+
const AgendaSettingsTab = () => React12.createElement(PluginSettingsPanel, {
|
|
5307
4758
|
title: "Agenda",
|
|
5308
|
-
generalSettings:
|
|
5309
|
-
registries
|
|
4759
|
+
generalSettings: React12.createElement(AgendaGeneralSettings),
|
|
4760
|
+
registries,
|
|
5310
4761
|
hostPluginId: "agenda",
|
|
5311
4762
|
routeBase: "/settings/agenda"
|
|
5312
4763
|
});
|