@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.cjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var chunkGAJVIEXM_cjs = require('./chunk-GAJVIEXM.cjs');
|
|
3
4
|
var chunkSYDBDI3Q_cjs = require('./chunk-SYDBDI3Q.cjs');
|
|
4
|
-
var
|
|
5
|
+
var React12 = require('react');
|
|
5
6
|
var core = require('@fayz-ai/core');
|
|
6
7
|
var saas = require('@fayz-ai/saas');
|
|
7
8
|
var FullCalendar = require('@fullcalendar/react');
|
|
@@ -19,7 +20,7 @@ var vanilla = require('zustand/vanilla');
|
|
|
19
20
|
|
|
20
21
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
21
22
|
|
|
22
|
-
var
|
|
23
|
+
var React12__default = /*#__PURE__*/_interopDefault(React12);
|
|
23
24
|
var FullCalendar__default = /*#__PURE__*/_interopDefault(FullCalendar);
|
|
24
25
|
var dayGridPlugin__default = /*#__PURE__*/_interopDefault(dayGridPlugin);
|
|
25
26
|
var timeGridPlugin__default = /*#__PURE__*/_interopDefault(timeGridPlugin);
|
|
@@ -80,23 +81,47 @@ var DEFAULT_BOOKING_TYPES = [
|
|
|
80
81
|
requiresClient: false
|
|
81
82
|
}
|
|
82
83
|
];
|
|
84
|
+
var SIMPLE_EVENT_BOOKING_TYPES = [
|
|
85
|
+
{
|
|
86
|
+
id: "event",
|
|
87
|
+
label: "Event",
|
|
88
|
+
icon: "Calendar",
|
|
89
|
+
color: "#6366f1",
|
|
90
|
+
fields: { title: true, client: false, professional: false, services: false, location: false, status: false },
|
|
91
|
+
requiresServices: false,
|
|
92
|
+
requiresClient: false
|
|
93
|
+
}
|
|
94
|
+
];
|
|
83
95
|
function resolveConfig(options) {
|
|
96
|
+
const simple = options?.eventMode === "simple";
|
|
97
|
+
const bookingTypes = options?.bookingTypes ?? (simple ? SIMPLE_EVENT_BOOKING_TYPES : DEFAULT_BOOKING_TYPES);
|
|
98
|
+
const capabilities = {
|
|
99
|
+
title: bookingTypes.some((bt) => bt.fields.title === true),
|
|
100
|
+
client: bookingTypes.some((bt) => bt.fields.client),
|
|
101
|
+
professional: bookingTypes.some((bt) => bt.fields.professional),
|
|
102
|
+
services: bookingTypes.some((bt) => bt.fields.services),
|
|
103
|
+
location: bookingTypes.some((bt) => bt.fields.location),
|
|
104
|
+
status: bookingTypes.some((bt) => bt.fields.status)
|
|
105
|
+
};
|
|
84
106
|
return {
|
|
85
107
|
modules: {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
108
|
+
// Off-unless-explicit in simple mode (`?? !simple`); default-on in full.
|
|
109
|
+
workingHours: options?.modules?.workingHours ?? !simple,
|
|
110
|
+
confirmations: options?.modules?.confirmations ?? !simple,
|
|
111
|
+
conflictDetection: options?.modules?.conflictDetection ?? !simple,
|
|
89
112
|
dragAndDrop: options?.modules?.dragAndDrop !== false,
|
|
90
113
|
locationSelection: options?.modules?.locationSelection === true,
|
|
91
114
|
financial: options?.modules?.financial ?? !!options?.financialBridge
|
|
92
115
|
},
|
|
116
|
+
simpleMode: simple,
|
|
117
|
+
capabilities,
|
|
93
118
|
labels: { ...DEFAULT_LABELS, ...options?.labels },
|
|
94
119
|
currency: { ...DEFAULT_CURRENCY, ...options?.currency },
|
|
95
120
|
bookingKind: options?.bookingKind ?? "appointment",
|
|
96
121
|
orderKind: options?.orderKind ?? "service_order",
|
|
97
122
|
scheduleKind: options?.scheduleKind ?? "working_hours",
|
|
98
123
|
statuses: options?.statuses ?? DEFAULT_STATUSES,
|
|
99
|
-
bookingTypes
|
|
124
|
+
bookingTypes,
|
|
100
125
|
businessHours: options?.businessHours ?? { startTime: "07:00", endTime: "21:00" },
|
|
101
126
|
slotDuration: options?.slotDuration ?? 30,
|
|
102
127
|
professionalKind: options?.professionalKind ?? "staff",
|
|
@@ -104,6 +129,8 @@ function resolveConfig(options) {
|
|
|
104
129
|
autoCreateOrder: options?.autoCreateOrder !== false,
|
|
105
130
|
locations: options?.locations ?? [],
|
|
106
131
|
confirmationChannels: options?.confirmationChannels ?? [],
|
|
132
|
+
cancellationDetails: options?.cancellationDetails,
|
|
133
|
+
onBookingCreated: options?.onBookingCreated,
|
|
107
134
|
contactLookup: options?.contactLookup,
|
|
108
135
|
clientEntityDef: options?.clientEntityDef,
|
|
109
136
|
serviceLookup: options?.serviceLookup,
|
|
@@ -431,10 +458,10 @@ function toDateLocale(locale) {
|
|
|
431
458
|
}
|
|
432
459
|
function MiniCalendar({ selectedDate, onDateSelect, onMonthChange }) {
|
|
433
460
|
const locale = core.getCurrentLocale();
|
|
434
|
-
const [viewMonth, setViewMonth] =
|
|
461
|
+
const [viewMonth, setViewMonth] = React12__default.default.useState(() => new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1));
|
|
435
462
|
const monthLabel = viewMonth.toLocaleDateString(toDateLocale(locale), { month: "long", year: "numeric" });
|
|
436
463
|
const dayLabels = DAY_LABELS[locale] ?? DAY_LABELS.en;
|
|
437
|
-
const weeks =
|
|
464
|
+
const weeks = React12.useMemo(() => {
|
|
438
465
|
const year = viewMonth.getFullYear();
|
|
439
466
|
const month = viewMonth.getMonth();
|
|
440
467
|
const firstDay = new Date(year, month, 1);
|
|
@@ -505,14 +532,14 @@ function SearchCombobox({
|
|
|
505
532
|
loading,
|
|
506
533
|
onFocus: onFocusProp
|
|
507
534
|
}) {
|
|
508
|
-
const [open, setOpen] =
|
|
509
|
-
const [activeIndex, setActiveIndex] =
|
|
510
|
-
const [expanded, setExpanded] =
|
|
511
|
-
const [dropRect, setDropRect] =
|
|
512
|
-
const inputRef =
|
|
513
|
-
const wrapperRef =
|
|
514
|
-
const listRef =
|
|
515
|
-
|
|
535
|
+
const [open, setOpen] = React12.useState(false);
|
|
536
|
+
const [activeIndex, setActiveIndex] = React12.useState(-1);
|
|
537
|
+
const [expanded, setExpanded] = React12.useState(!inlineLabel);
|
|
538
|
+
const [dropRect, setDropRect] = React12.useState(null);
|
|
539
|
+
const inputRef = React12.useRef(null);
|
|
540
|
+
const wrapperRef = React12.useRef(null);
|
|
541
|
+
const listRef = React12.useRef(null);
|
|
542
|
+
React12.useEffect(() => {
|
|
516
543
|
if (value && inlineLabel) setExpanded(true);
|
|
517
544
|
if (!value && inlineLabel && !open) setExpanded(false);
|
|
518
545
|
}, [value, open]);
|
|
@@ -521,13 +548,13 @@ function SearchCombobox({
|
|
|
521
548
|
}
|
|
522
549
|
const showCreate = open && allowCreate && onCreateNew && value.trim().length > 0 && !options.some((o) => o.label.toLowerCase() === value.trim().toLowerCase());
|
|
523
550
|
const totalItems = options.length + (showCreate ? 1 : 0);
|
|
524
|
-
|
|
551
|
+
React12.useEffect(() => {
|
|
525
552
|
setActiveIndex(-1);
|
|
526
553
|
}, [options]);
|
|
527
|
-
|
|
554
|
+
React12.useEffect(() => {
|
|
528
555
|
if ((options.length > 0 || showCreate || loading) && document.activeElement === inputRef.current) setOpen(true);
|
|
529
556
|
}, [options, showCreate, loading]);
|
|
530
|
-
const handleKeyDown =
|
|
557
|
+
const handleKeyDown = React12.useCallback((e) => {
|
|
531
558
|
if (!open || totalItems === 0) {
|
|
532
559
|
if (e.key === "ArrowDown" && totalItems > 0) {
|
|
533
560
|
e.preventDefault();
|
|
@@ -564,7 +591,7 @@ function SearchCombobox({
|
|
|
564
591
|
break;
|
|
565
592
|
}
|
|
566
593
|
}, [open, options, totalItems, activeIndex, onSelect, showCreate, onCreateNew, value]);
|
|
567
|
-
|
|
594
|
+
React12.useEffect(() => {
|
|
568
595
|
if (activeIndex < 0 || !listRef.current) return;
|
|
569
596
|
const items = listRef.current.children;
|
|
570
597
|
if (items[activeIndex]) items[activeIndex].scrollIntoView({ block: "nearest" });
|
|
@@ -720,27 +747,27 @@ var ICON_COLORS = {
|
|
|
720
747
|
bank_transfer: "text-warning",
|
|
721
748
|
check: "text-muted-foreground"
|
|
722
749
|
};
|
|
723
|
-
function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPaymentChange, formatCurrency, inline }) {
|
|
750
|
+
function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPaymentChange, formatCurrency: formatCurrency2, inline }) {
|
|
724
751
|
const t = core.useTranslation();
|
|
725
752
|
const config = useAgendaConfig();
|
|
726
753
|
const bridge = config.financialBridge;
|
|
727
|
-
const [visible, setVisible] =
|
|
728
|
-
const [loading, setLoading] =
|
|
729
|
-
const [detail, setDetail] =
|
|
730
|
-
const [selectedTypeId, setSelectedTypeId] =
|
|
731
|
-
const [paymentTypes, setPaymentTypes] =
|
|
732
|
-
const [bankAccounts, setBankAccounts] =
|
|
733
|
-
const [filteredMethods, setFilteredMethods] =
|
|
734
|
-
const [selectedBankId, setSelectedBankId] =
|
|
735
|
-
const [paymentMethodId, setPaymentMethodId] =
|
|
736
|
-
const [cardBrand, setCardBrand] =
|
|
737
|
-
const [cardInstallments, setCardInstallments] =
|
|
738
|
-
const [processing, setProcessing] =
|
|
739
|
-
const [justPaid, setJustPaid] =
|
|
740
|
-
|
|
754
|
+
const [visible, setVisible] = React12.useState(false);
|
|
755
|
+
const [loading, setLoading] = React12.useState(true);
|
|
756
|
+
const [detail, setDetail] = React12.useState(null);
|
|
757
|
+
const [selectedTypeId, setSelectedTypeId] = React12.useState("");
|
|
758
|
+
const [paymentTypes, setPaymentTypes] = React12.useState([]);
|
|
759
|
+
const [bankAccounts, setBankAccounts] = React12.useState([]);
|
|
760
|
+
const [filteredMethods, setFilteredMethods] = React12.useState([]);
|
|
761
|
+
const [selectedBankId, setSelectedBankId] = React12.useState("");
|
|
762
|
+
const [paymentMethodId, setPaymentMethodId] = React12.useState("");
|
|
763
|
+
const [cardBrand, setCardBrand] = React12.useState("");
|
|
764
|
+
const [cardInstallments, setCardInstallments] = React12.useState(1);
|
|
765
|
+
const [processing, setProcessing] = React12.useState(false);
|
|
766
|
+
const [justPaid, setJustPaid] = React12.useState(false);
|
|
767
|
+
React12.useEffect(() => {
|
|
741
768
|
requestAnimationFrame(() => setVisible(true));
|
|
742
769
|
}, []);
|
|
743
|
-
|
|
770
|
+
React12.useEffect(() => {
|
|
744
771
|
let cancelled = false;
|
|
745
772
|
setLoading(true);
|
|
746
773
|
Promise.all([
|
|
@@ -802,7 +829,7 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
|
|
|
802
829
|
!loading && isPaid && justPaid && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center py-8 space-y-2 animate-in fade-in zoom-in-95 duration-300", children: [
|
|
803
830
|
/* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-7 w-7 text-success" }) }),
|
|
804
831
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-semibold text-success", children: t("agenda.payment.checkoutComplete") }),
|
|
805
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children:
|
|
832
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: formatCurrency2(detail.totalAmount) })
|
|
806
833
|
] }),
|
|
807
834
|
!loading && detail && detail.status !== "none" && !justPaid && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3 animate-in fade-in duration-300", children: [
|
|
808
835
|
detail.movements.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1.5", children: detail.movements.map((mov) => {
|
|
@@ -834,21 +861,21 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
|
|
|
834
861
|
] }),
|
|
835
862
|
isPartial && /* @__PURE__ */ jsxRuntime.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: [
|
|
836
863
|
"! ",
|
|
837
|
-
|
|
864
|
+
formatCurrency2(mov.paidAmount),
|
|
838
865
|
"/",
|
|
839
|
-
|
|
866
|
+
formatCurrency2(mov.amount)
|
|
840
867
|
] }),
|
|
841
868
|
isOverdue && /* @__PURE__ */ jsxRuntime.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: [
|
|
842
869
|
"! ",
|
|
843
870
|
t("agenda.payment.overdue")
|
|
844
871
|
] }),
|
|
845
872
|
mov.status === "pending" && /* @__PURE__ */ jsxRuntime.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") }),
|
|
846
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-semibold", children:
|
|
873
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-semibold", children: formatCurrency2(mov.paidAmount || mov.amount) })
|
|
847
874
|
] })
|
|
848
875
|
] }, mov.id);
|
|
849
876
|
}) }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-sm px-1", children: [
|
|
850
877
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: t("agenda.payment.total") }),
|
|
851
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-semibold", children:
|
|
878
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-semibold", children: formatCurrency2(detail.totalAmount) })
|
|
852
879
|
] }),
|
|
853
880
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
854
881
|
detail.referenceNumber && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-[10px] font-mono text-muted-foreground", children: [
|
|
@@ -873,12 +900,12 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
|
|
|
873
900
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[11px] font-medium text-muted-foreground mb-2", children: t("agenda.payment.serviceSummary") }),
|
|
874
901
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-0.5", children: services.map((svc, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-sm py-1", children: [
|
|
875
902
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: svc.name }),
|
|
876
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 text-muted-foreground", children:
|
|
903
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 text-muted-foreground", children: formatCurrency2(svc.price) })
|
|
877
904
|
] }, i)) })
|
|
878
905
|
] }),
|
|
879
906
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-sm font-semibold pt-2 border-t", children: [
|
|
880
907
|
/* @__PURE__ */ jsxRuntime.jsx("span", { children: t("agenda.payment.total") }),
|
|
881
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { children:
|
|
908
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: formatCurrency2(orderTotal) })
|
|
882
909
|
] }),
|
|
883
910
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
884
911
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[11px] font-medium text-muted-foreground mb-2", children: t("agenda.payment.paymentMethod") }),
|
|
@@ -953,7 +980,7 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
|
|
|
953
980
|
children: Array.from({ length: 12 }, (_, i) => i + 1).map((n) => /* @__PURE__ */ jsxRuntime.jsxs("option", { value: n, children: [
|
|
954
981
|
n,
|
|
955
982
|
"x ",
|
|
956
|
-
n > 1 ?
|
|
983
|
+
n > 1 ? formatCurrency2(orderTotal / n) : ""
|
|
957
984
|
] }, n))
|
|
958
985
|
}
|
|
959
986
|
)
|
|
@@ -982,7 +1009,7 @@ function BookingPaymentPanel({ orderId, orderTotal, services, onClose, onPayment
|
|
|
982
1009
|
onClick: handleCheckout,
|
|
983
1010
|
disabled: processing || !selectedTypeId,
|
|
984
1011
|
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",
|
|
985
|
-
children: processing ? t("agenda.payment.checkingOut") : `${t("agenda.payment.checkout")} ${
|
|
1012
|
+
children: processing ? t("agenda.payment.checkingOut") : `${t("agenda.payment.checkout")} ${formatCurrency2(orderTotal)}`
|
|
986
1013
|
}
|
|
987
1014
|
)
|
|
988
1015
|
] })
|
|
@@ -1043,9 +1070,9 @@ function formatCompactDate(dateStr, locale) {
|
|
|
1043
1070
|
});
|
|
1044
1071
|
}
|
|
1045
1072
|
function InlineNumber({ value, onChange, min = 0, step = 1, className, disabled, tooltip }) {
|
|
1046
|
-
const [draft, setDraft] =
|
|
1047
|
-
const [focused, setFocused] =
|
|
1048
|
-
|
|
1073
|
+
const [draft, setDraft] = React12.useState(String(value));
|
|
1074
|
+
const [focused, setFocused] = React12.useState(false);
|
|
1075
|
+
React12.useEffect(() => {
|
|
1049
1076
|
if (!focused) setDraft(String(value));
|
|
1050
1077
|
}, [value, focused]);
|
|
1051
1078
|
function commit() {
|
|
@@ -1089,11 +1116,11 @@ function InlineNumber({ value, onChange, min = 0, step = 1, className, disabled,
|
|
|
1089
1116
|
] }) });
|
|
1090
1117
|
}
|
|
1091
1118
|
function StatusSelect({ value, onChange, statuses, bookingDate }) {
|
|
1092
|
-
const [open, setOpen] =
|
|
1093
|
-
const ref =
|
|
1094
|
-
const dropdownRef =
|
|
1119
|
+
const [open, setOpen] = React12.useState(false);
|
|
1120
|
+
const ref = React12.useRef(null);
|
|
1121
|
+
const dropdownRef = React12.useRef(null);
|
|
1095
1122
|
const current = statuses.find((s) => s.value === value);
|
|
1096
|
-
|
|
1123
|
+
React12.useEffect(() => {
|
|
1097
1124
|
if (!open) return;
|
|
1098
1125
|
const handler = (e) => {
|
|
1099
1126
|
const target = e.target;
|
|
@@ -1104,8 +1131,8 @@ function StatusSelect({ value, onChange, statuses, bookingDate }) {
|
|
|
1104
1131
|
document.addEventListener("mousedown", handler);
|
|
1105
1132
|
return () => document.removeEventListener("mousedown", handler);
|
|
1106
1133
|
}, [open]);
|
|
1107
|
-
const btnRef =
|
|
1108
|
-
const [dropRect, setDropRect] =
|
|
1134
|
+
const btnRef = React12.useRef(null);
|
|
1135
|
+
const [dropRect, setDropRect] = React12.useState(null);
|
|
1109
1136
|
function handleOpen() {
|
|
1110
1137
|
if (btnRef.current) setDropRect(btnRef.current.getBoundingClientRect());
|
|
1111
1138
|
setOpen((p) => !p);
|
|
@@ -1165,10 +1192,10 @@ function StatusSelect({ value, onChange, statuses, bookingDate }) {
|
|
|
1165
1192
|
}
|
|
1166
1193
|
function ProfessionalSelect({ value, onChange, professionals }) {
|
|
1167
1194
|
const t = core.useTranslation();
|
|
1168
|
-
const [open, setOpen] =
|
|
1169
|
-
const ref =
|
|
1195
|
+
const [open, setOpen] = React12.useState(false);
|
|
1196
|
+
const ref = React12.useRef(null);
|
|
1170
1197
|
const current = professionals.find((p) => p.id === value);
|
|
1171
|
-
|
|
1198
|
+
React12.useEffect(() => {
|
|
1172
1199
|
if (!open) return;
|
|
1173
1200
|
const handler = (e) => {
|
|
1174
1201
|
if (ref.current && !ref.current.contains(e.target)) setOpen(false);
|
|
@@ -1206,10 +1233,10 @@ function ProfessionalSelect({ value, onChange, professionals }) {
|
|
|
1206
1233
|
}
|
|
1207
1234
|
function LocationSelect({ value, onChange, locations }) {
|
|
1208
1235
|
const t = core.useTranslation();
|
|
1209
|
-
const [open, setOpen] =
|
|
1210
|
-
const ref =
|
|
1236
|
+
const [open, setOpen] = React12.useState(false);
|
|
1237
|
+
const ref = React12.useRef(null);
|
|
1211
1238
|
const current = locations.find((l) => l.id === value);
|
|
1212
|
-
|
|
1239
|
+
React12.useEffect(() => {
|
|
1213
1240
|
if (!open) return;
|
|
1214
1241
|
const handler = (e) => {
|
|
1215
1242
|
if (ref.current && !ref.current.contains(e.target)) setOpen(false);
|
|
@@ -1245,12 +1272,12 @@ function LocationSelect({ value, onChange, locations }) {
|
|
|
1245
1272
|
)) })
|
|
1246
1273
|
] });
|
|
1247
1274
|
}
|
|
1248
|
-
function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose, onCreated }) {
|
|
1275
|
+
function AppointmentModal({ open, mode, bookingId, prefill, initialTab, externalSource, onClose, onCreated }) {
|
|
1249
1276
|
const t = core.useTranslation();
|
|
1250
1277
|
const locale = core.getCurrentLocale();
|
|
1251
1278
|
const config = useAgendaConfig();
|
|
1252
1279
|
const provider2 = useAgendaProvider();
|
|
1253
|
-
const initialSnapshot =
|
|
1280
|
+
const initialSnapshot = React12.useRef("");
|
|
1254
1281
|
const createBooking = useAgendaStore((s) => s.createBooking);
|
|
1255
1282
|
const updateBooking = useAgendaStore((s) => s.updateBooking);
|
|
1256
1283
|
const deleteBooking = useAgendaStore((s) => s.deleteBooking);
|
|
@@ -1258,54 +1285,60 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
|
|
|
1258
1285
|
const agendaDateRange = useAgendaStore((s) => s.dateRange);
|
|
1259
1286
|
const professionals = useAgendaStore((s) => s.professionals);
|
|
1260
1287
|
const storeLocations = useAgendaStore((s) => s.locations);
|
|
1261
|
-
const
|
|
1262
|
-
const [
|
|
1263
|
-
const [
|
|
1264
|
-
const [
|
|
1265
|
-
const [
|
|
1266
|
-
const [
|
|
1267
|
-
const [
|
|
1268
|
-
const [
|
|
1269
|
-
const [
|
|
1270
|
-
const [
|
|
1288
|
+
const defaultBookingType = config.bookingTypes[0]?.id ?? "appointment";
|
|
1289
|
+
const [loading, setLoading] = React12.useState(mode === "edit");
|
|
1290
|
+
const [bookingType, setBookingType] = React12.useState(prefill?.kind ?? defaultBookingType);
|
|
1291
|
+
const [title, setTitle] = React12.useState("");
|
|
1292
|
+
const [clientId, setClientId] = React12.useState(prefill?.clientId ?? "");
|
|
1293
|
+
const [clientSearch, setClientSearch] = React12.useState("");
|
|
1294
|
+
const [clientPhone, setClientPhone] = React12.useState("");
|
|
1295
|
+
const [professionalId, setProfessionalId] = React12.useState(prefill?.professionalId ?? "");
|
|
1296
|
+
const [locationId, setLocationId] = React12.useState(prefill?.locationId ?? "");
|
|
1297
|
+
const [services, setServices] = React12.useState([]);
|
|
1298
|
+
const [serviceSearch, setServiceSearch] = React12.useState("");
|
|
1299
|
+
const [date, setDate] = React12.useState(() => {
|
|
1271
1300
|
if (prefill?.startsAt) return prefill.startsAt.slice(0, 10);
|
|
1272
1301
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1273
1302
|
});
|
|
1274
|
-
const [startTime, setStartTime] =
|
|
1303
|
+
const [startTime, setStartTime] = React12.useState(() => {
|
|
1275
1304
|
if (prefill?.startsAt) {
|
|
1276
1305
|
const d = new Date(prefill.startsAt);
|
|
1277
1306
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
1278
1307
|
}
|
|
1279
1308
|
return "09:00";
|
|
1280
1309
|
});
|
|
1281
|
-
const [status, setStatus] =
|
|
1282
|
-
const [notes, setNotes] =
|
|
1283
|
-
const [showNotes, setShowNotes] =
|
|
1284
|
-
const [saving, setSaving] =
|
|
1285
|
-
const [conflict, setConflict] =
|
|
1286
|
-
const [quickCreate, setQuickCreate] =
|
|
1287
|
-
const [newClientName, setNewClientName] =
|
|
1288
|
-
const [newClientPhone, setNewClientPhone] =
|
|
1289
|
-
const [newClientEmail, setNewClientEmail] =
|
|
1290
|
-
const [creatingClient, setCreatingClient] =
|
|
1291
|
-
const [clientOptions, setClientOptions] =
|
|
1292
|
-
const [clientSearching, setClientSearching] =
|
|
1293
|
-
const [serviceOptions, setServiceOptions] =
|
|
1294
|
-
const [initialServiceOptions, setInitialServiceOptions] =
|
|
1295
|
-
const [editingDate, setEditingDate] =
|
|
1296
|
-
const [editingTime, setEditingTime] =
|
|
1297
|
-
const [confirmingDelete, setConfirmingDelete] =
|
|
1298
|
-
const [deleting, setDeleting] =
|
|
1299
|
-
const [editOrderId, setEditOrderId] =
|
|
1300
|
-
const [editOrderTotal, setEditOrderTotal] =
|
|
1301
|
-
const [editPaymentStatus, setEditPaymentStatus] =
|
|
1302
|
-
const [paymentStatusLoading, setPaymentStatusLoading] =
|
|
1303
|
-
const [modalTab, setModalTab] =
|
|
1304
|
-
const [clientData, setClientData] =
|
|
1305
|
-
const [clientDataLoading, setClientDataLoading] =
|
|
1306
|
-
const [clientEditing, setClientEditing] =
|
|
1310
|
+
const [status, setStatus] = React12.useState("scheduled");
|
|
1311
|
+
const [notes, setNotes] = React12.useState("");
|
|
1312
|
+
const [showNotes, setShowNotes] = React12.useState(false);
|
|
1313
|
+
const [saving, setSaving] = React12.useState(false);
|
|
1314
|
+
const [conflict, setConflict] = React12.useState(false);
|
|
1315
|
+
const [quickCreate, setQuickCreate] = React12.useState(false);
|
|
1316
|
+
const [newClientName, setNewClientName] = React12.useState("");
|
|
1317
|
+
const [newClientPhone, setNewClientPhone] = React12.useState("");
|
|
1318
|
+
const [newClientEmail, setNewClientEmail] = React12.useState("");
|
|
1319
|
+
const [creatingClient, setCreatingClient] = React12.useState(false);
|
|
1320
|
+
const [clientOptions, setClientOptions] = React12.useState([]);
|
|
1321
|
+
const [clientSearching, setClientSearching] = React12.useState(false);
|
|
1322
|
+
const [serviceOptions, setServiceOptions] = React12.useState([]);
|
|
1323
|
+
const [initialServiceOptions, setInitialServiceOptions] = React12.useState([]);
|
|
1324
|
+
const [editingDate, setEditingDate] = React12.useState(false);
|
|
1325
|
+
const [editingTime, setEditingTime] = React12.useState(false);
|
|
1326
|
+
const [confirmingDelete, setConfirmingDelete] = React12.useState(false);
|
|
1327
|
+
const [deleting, setDeleting] = React12.useState(false);
|
|
1328
|
+
const [editOrderId, setEditOrderId] = React12.useState(null);
|
|
1329
|
+
const [editOrderTotal, setEditOrderTotal] = React12.useState(0);
|
|
1330
|
+
const [editPaymentStatus, setEditPaymentStatus] = React12.useState("none");
|
|
1331
|
+
const [paymentStatusLoading, setPaymentStatusLoading] = React12.useState(false);
|
|
1332
|
+
const [modalTab, setModalTab] = React12.useState("appointment");
|
|
1333
|
+
const [clientData, setClientData] = React12.useState(null);
|
|
1334
|
+
const [clientDataLoading, setClientDataLoading] = React12.useState(false);
|
|
1335
|
+
const [clientEditing, setClientEditing] = React12.useState(false);
|
|
1336
|
+
const [cancellationReasons, setCancellationReasons] = React12.useState([]);
|
|
1337
|
+
const [cancellationReasonId, setCancellationReasonId] = React12.useState("");
|
|
1338
|
+
const [cancellationNotes, setCancellationNotes] = React12.useState("");
|
|
1339
|
+
const [cancellationReasonsLoading, setCancellationReasonsLoading] = React12.useState(false);
|
|
1307
1340
|
const activeType = config.bookingTypes.find((bt) => bt.id === bookingType) ?? config.bookingTypes[0];
|
|
1308
|
-
|
|
1341
|
+
React12.useEffect(() => {
|
|
1309
1342
|
if (!open) return;
|
|
1310
1343
|
setClientId(prefill?.clientId ?? "");
|
|
1311
1344
|
setClientSearch("");
|
|
@@ -1324,7 +1357,8 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
|
|
|
1324
1357
|
setNewClientPhone("");
|
|
1325
1358
|
setNewClientEmail("");
|
|
1326
1359
|
setCreatingClient(false);
|
|
1327
|
-
setBookingType(prefill?.kind ??
|
|
1360
|
+
setBookingType(prefill?.kind ?? defaultBookingType);
|
|
1361
|
+
setTitle("");
|
|
1328
1362
|
setEditingDate(false);
|
|
1329
1363
|
setEditingTime(false);
|
|
1330
1364
|
setConfirmingDelete(false);
|
|
@@ -1336,6 +1370,9 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
|
|
|
1336
1370
|
setClientData(null);
|
|
1337
1371
|
setClientDataLoading(false);
|
|
1338
1372
|
setClientEditing(false);
|
|
1373
|
+
setCancellationReasonId("");
|
|
1374
|
+
setCancellationNotes("");
|
|
1375
|
+
setCancellationReasonsLoading(false);
|
|
1339
1376
|
setModalTab(initialTab || "appointment");
|
|
1340
1377
|
if (!prefill?.professionalId && professionals.length === 1) setProfessionalId(professionals[0].id);
|
|
1341
1378
|
setDate(() => {
|
|
@@ -1350,7 +1387,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
|
|
|
1350
1387
|
return "09:00";
|
|
1351
1388
|
});
|
|
1352
1389
|
}, [open]);
|
|
1353
|
-
|
|
1390
|
+
React12.useEffect(() => {
|
|
1354
1391
|
if (!open || mode !== "edit" || !bookingId) return;
|
|
1355
1392
|
setLoading(true);
|
|
1356
1393
|
provider2.getBookingById(bookingId).then((b) => {
|
|
@@ -1358,7 +1395,8 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
|
|
|
1358
1395
|
setLoading(false);
|
|
1359
1396
|
return;
|
|
1360
1397
|
}
|
|
1361
|
-
setBookingType(b.kind ??
|
|
1398
|
+
setBookingType(b.kind ?? defaultBookingType);
|
|
1399
|
+
setTitle(b.clientName ?? "");
|
|
1362
1400
|
setClientId(b.clientId ?? "");
|
|
1363
1401
|
setClientSearch(b.clientName ?? "");
|
|
1364
1402
|
setProfessionalId(b.professionalId ?? "");
|
|
@@ -1380,6 +1418,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
|
|
|
1380
1418
|
setEditPaymentStatus(b.paymentStatus ?? "none");
|
|
1381
1419
|
const svcSnap = b.services ? b.services.map((s) => ({ id: s.serviceId ?? "", dur: s.durationMinutes, price: s.price })) : [];
|
|
1382
1420
|
initialSnapshot.current = JSON.stringify({
|
|
1421
|
+
title: b.clientName ?? "",
|
|
1383
1422
|
clientId: b.clientId ?? "",
|
|
1384
1423
|
professionalId: b.professionalId ?? "",
|
|
1385
1424
|
locationId: b.locationId ?? "",
|
|
@@ -1387,6 +1426,8 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
|
|
|
1387
1426
|
startTime: `${String(new Date(b.startsAt).getHours()).padStart(2, "0")}:${String(new Date(b.startsAt).getMinutes()).padStart(2, "0")}`,
|
|
1388
1427
|
status: b.status,
|
|
1389
1428
|
notes: b.notes ?? "",
|
|
1429
|
+
cancellationReasonId: "",
|
|
1430
|
+
cancellationNotes: "",
|
|
1390
1431
|
services: svcSnap
|
|
1391
1432
|
});
|
|
1392
1433
|
setLoading(false);
|
|
@@ -1403,7 +1444,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
|
|
|
1403
1444
|
}
|
|
1404
1445
|
}).catch(() => setLoading(false));
|
|
1405
1446
|
}, [open, mode, bookingId]);
|
|
1406
|
-
|
|
1447
|
+
React12.useEffect(() => {
|
|
1407
1448
|
if (modalTab !== "client" || !clientId || !config.contactLookup || clientData) return;
|
|
1408
1449
|
setClientDataLoading(true);
|
|
1409
1450
|
config.contactLookup.getById(clientId).then((result) => {
|
|
@@ -1411,7 +1452,12 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
|
|
|
1411
1452
|
setClientDataLoading(false);
|
|
1412
1453
|
}).catch(() => setClientDataLoading(false));
|
|
1413
1454
|
}, [modalTab, clientId]);
|
|
1414
|
-
|
|
1455
|
+
React12.useEffect(() => {
|
|
1456
|
+
if (!open || status !== "cancelled" || !config.cancellationDetails || cancellationReasons.length > 0) return;
|
|
1457
|
+
setCancellationReasonsLoading(true);
|
|
1458
|
+
config.cancellationDetails.listReasons().then((reasons) => setCancellationReasons(reasons)).catch(() => setCancellationReasons([])).finally(() => setCancellationReasonsLoading(false));
|
|
1459
|
+
}, [open, status, config.cancellationDetails, cancellationReasons.length]);
|
|
1460
|
+
React12.useEffect(() => {
|
|
1415
1461
|
if (!open || !config.serviceLookup) return;
|
|
1416
1462
|
const lookup = config.serviceLookup;
|
|
1417
1463
|
const fetch = lookup.list ? () => lookup.list() : () => lookup.search("");
|
|
@@ -1420,7 +1466,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
|
|
|
1420
1466
|
setInitialServiceOptions(opts);
|
|
1421
1467
|
}).catch(() => setInitialServiceOptions([]));
|
|
1422
1468
|
}, [open]);
|
|
1423
|
-
|
|
1469
|
+
React12.useEffect(() => {
|
|
1424
1470
|
if (!config.contactLookup || clientSearch.length < 1 || clientId) {
|
|
1425
1471
|
setClientOptions([]);
|
|
1426
1472
|
setClientSearching(false);
|
|
@@ -1438,7 +1484,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
|
|
|
1438
1484
|
}, 250);
|
|
1439
1485
|
return () => clearTimeout(timer);
|
|
1440
1486
|
}, [clientSearch, clientId]);
|
|
1441
|
-
|
|
1487
|
+
React12.useEffect(() => {
|
|
1442
1488
|
if (!config.serviceLookup || serviceSearch.length < 1) {
|
|
1443
1489
|
setServiceOptions([]);
|
|
1444
1490
|
return;
|
|
@@ -1454,19 +1500,19 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
|
|
|
1454
1500
|
return () => clearTimeout(timer);
|
|
1455
1501
|
}, [serviceSearch]);
|
|
1456
1502
|
const serviceDuration = services.reduce((sum, s) => sum + s.durationMinutes, 0);
|
|
1457
|
-
const selectionDuration =
|
|
1503
|
+
const selectionDuration = React12.useMemo(() => {
|
|
1458
1504
|
if (!prefill?.startsAt || !prefill?.endsAt) return 0;
|
|
1459
1505
|
return Math.round((new Date(prefill.endsAt).getTime() - new Date(prefill.startsAt).getTime()) / 6e4);
|
|
1460
1506
|
}, [prefill?.startsAt, prefill?.endsAt]);
|
|
1461
1507
|
const totalDuration = serviceDuration > 0 ? serviceDuration : selectionDuration;
|
|
1462
1508
|
const totalPrice = services.reduce((sum, s) => sum + s.price, 0);
|
|
1463
1509
|
const { currency } = config;
|
|
1464
|
-
const endTime =
|
|
1510
|
+
const endTime = React12.useMemo(() => {
|
|
1465
1511
|
const [h, m] = startTime.split(":").map(Number);
|
|
1466
1512
|
const total = h * 60 + m + totalDuration;
|
|
1467
1513
|
return `${String(Math.floor(total / 60) % 24).padStart(2, "0")}:${String(total % 60).padStart(2, "0")}`;
|
|
1468
1514
|
}, [startTime, totalDuration]);
|
|
1469
|
-
|
|
1515
|
+
React12.useEffect(() => {
|
|
1470
1516
|
if (!professionalId || totalDuration === 0) {
|
|
1471
1517
|
setConflict(false);
|
|
1472
1518
|
return;
|
|
@@ -1510,10 +1556,11 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
|
|
|
1510
1556
|
locationId: locationId || void 0,
|
|
1511
1557
|
startsAt,
|
|
1512
1558
|
notes: notes || void 0,
|
|
1559
|
+
title: activeType.fields.title ? title.trim() : void 0,
|
|
1513
1560
|
services: services.map((s) => ({ serviceId: s.serviceId, name: s.name, durationMinutes: s.durationMinutes, price: s.price }))
|
|
1514
1561
|
});
|
|
1515
1562
|
onClose();
|
|
1516
|
-
onCreated?.(newBooking.id);
|
|
1563
|
+
onCreated?.(newBooking.id, externalSource);
|
|
1517
1564
|
} else if (bookingId) {
|
|
1518
1565
|
await updateBooking(bookingId, {
|
|
1519
1566
|
clientId,
|
|
@@ -1522,8 +1569,17 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
|
|
|
1522
1569
|
startsAt,
|
|
1523
1570
|
notes: notes || void 0,
|
|
1524
1571
|
status,
|
|
1572
|
+
title: activeType.fields.title ? title.trim() : void 0,
|
|
1525
1573
|
services: services.map((s) => ({ serviceId: s.serviceId, name: s.name, durationMinutes: s.durationMinutes, price: s.price }))
|
|
1526
1574
|
});
|
|
1575
|
+
if (status === "cancelled" && config.cancellationDetails && cancellationReasonId) {
|
|
1576
|
+
await config.cancellationDetails.saveCancellationDetails({
|
|
1577
|
+
bookingId,
|
|
1578
|
+
reasonId: cancellationReasonId,
|
|
1579
|
+
notes: cancellationNotes.trim() || void 0,
|
|
1580
|
+
cancelledAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1527
1583
|
onClose();
|
|
1528
1584
|
}
|
|
1529
1585
|
} catch {
|
|
@@ -1541,12 +1597,15 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
|
|
|
1541
1597
|
setDeleting(false);
|
|
1542
1598
|
}
|
|
1543
1599
|
const isPaidBooking = editPaymentStatus === "paid";
|
|
1544
|
-
const
|
|
1545
|
-
|
|
1546
|
-
|
|
1600
|
+
const selectedCancellationReason = cancellationReasons.find((reason) => reason.id === cancellationReasonId);
|
|
1601
|
+
const needsCancellationReason = mode === "edit" && status === "cancelled" && !!config.cancellationDetails;
|
|
1602
|
+
const needsCancellationNotes = needsCancellationReason && !!selectedCancellationReason?.requiresNotes;
|
|
1603
|
+
const formSnapshot = React12.useMemo(
|
|
1604
|
+
() => 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 })) }),
|
|
1605
|
+
[title, clientId, professionalId, locationId, date, startTime, status, notes, cancellationReasonId, cancellationNotes, services]
|
|
1547
1606
|
);
|
|
1548
1607
|
const isDirty = mode === "create" || formSnapshot !== initialSnapshot.current;
|
|
1549
|
-
const canSubmit = (!activeType.requiresClient || clientId) && (!activeType.fields.professional || professionalId) && (!activeType.requiresServices || services.length > 0) && !conflict && !saving;
|
|
1608
|
+
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;
|
|
1550
1609
|
const showTabs = config.bookingTypes.length > 1;
|
|
1551
1610
|
return /* @__PURE__ */ jsxRuntime.jsx(ui.Modal, { open, onOpenChange: (v) => {
|
|
1552
1611
|
if (!v) onClose();
|
|
@@ -1667,8 +1726,8 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
|
|
|
1667
1726
|
onCancel: () => setClientEditing(false),
|
|
1668
1727
|
onSubmit: async (data) => {
|
|
1669
1728
|
try {
|
|
1670
|
-
const { getSupabaseClientOptional:
|
|
1671
|
-
const supabase =
|
|
1729
|
+
const { getSupabaseClientOptional: getSupabaseClientOptional3 } = await import('@fayz-ai/core');
|
|
1730
|
+
const supabase = getSupabaseClientOptional3();
|
|
1672
1731
|
if (!supabase) return;
|
|
1673
1732
|
const dbData = {};
|
|
1674
1733
|
for (const [k, v] of Object.entries(data)) {
|
|
@@ -1721,6 +1780,20 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
|
|
|
1721
1780
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-8 flex-1 rounded-lg bg-muted animate-pulse" })
|
|
1722
1781
|
] }, i)) }),
|
|
1723
1782
|
!loading && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1783
|
+
activeType.fields.title && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 py-1", children: [
|
|
1784
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Calendar, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
1785
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1786
|
+
"input",
|
|
1787
|
+
{
|
|
1788
|
+
type: "text",
|
|
1789
|
+
value: title,
|
|
1790
|
+
onChange: (e) => setTitle(e.target.value),
|
|
1791
|
+
placeholder: t("agenda.appointment.addTitle"),
|
|
1792
|
+
autoFocus: mode === "create",
|
|
1793
|
+
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"
|
|
1794
|
+
}
|
|
1795
|
+
)
|
|
1796
|
+
] }),
|
|
1724
1797
|
activeType.fields.client && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 py-1", children: [
|
|
1725
1798
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.User, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
1726
1799
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: clientId && !quickCreate ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 rounded-md bg-muted/30 px-2.5 py-1.5 text-sm", children: [
|
|
@@ -1815,10 +1888,10 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
|
|
|
1815
1888
|
onClick: async () => {
|
|
1816
1889
|
setCreatingClient(true);
|
|
1817
1890
|
try {
|
|
1818
|
-
const { getSupabaseClientOptional:
|
|
1819
|
-
const { getAgendaTenantId
|
|
1820
|
-
const supabase =
|
|
1821
|
-
const tenantId =
|
|
1891
|
+
const { getSupabaseClientOptional: getSupabaseClientOptional3 } = await import('@fayz-ai/core');
|
|
1892
|
+
const { getAgendaTenantId } = await import('./tenant-222EHYUC.cjs');
|
|
1893
|
+
const supabase = getSupabaseClientOptional3();
|
|
1894
|
+
const tenantId = getAgendaTenantId();
|
|
1822
1895
|
if (!supabase || !tenantId) throw new Error("Not initialized");
|
|
1823
1896
|
const { data: personRow, error: personError } = await supabase.schema("saas_core").from("persons").insert({
|
|
1824
1897
|
tenant_id: tenantId,
|
|
@@ -1933,6 +2006,37 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, onClose,
|
|
|
1933
2006
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.CircleDot, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
1934
2007
|
/* @__PURE__ */ jsxRuntime.jsx(StatusSelect, { value: status, onChange: setStatus, statuses: config.statuses, bookingDate: date })
|
|
1935
2008
|
] }),
|
|
2009
|
+
needsCancellationReason && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-3 py-1", children: [
|
|
2010
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Ban, { className: "h-4 w-4 mt-2 text-muted-foreground shrink-0" }),
|
|
2011
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 space-y-2", children: [
|
|
2012
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2013
|
+
"select",
|
|
2014
|
+
{
|
|
2015
|
+
value: cancellationReasonId,
|
|
2016
|
+
onChange: (event) => setCancellationReasonId(event.target.value),
|
|
2017
|
+
disabled: cancellationReasonsLoading,
|
|
2018
|
+
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",
|
|
2019
|
+
children: [
|
|
2020
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: cancellationReasonsLoading ? t("agenda.cancellation.loadingReasons") : t("agenda.cancellation.selectReason") }),
|
|
2021
|
+
cancellationReasons.map((reason) => /* @__PURE__ */ jsxRuntime.jsxs("option", { value: reason.id, children: [
|
|
2022
|
+
reason.label,
|
|
2023
|
+
reason.requiresNotes ? " *" : ""
|
|
2024
|
+
] }, reason.id))
|
|
2025
|
+
]
|
|
2026
|
+
}
|
|
2027
|
+
),
|
|
2028
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2029
|
+
"textarea",
|
|
2030
|
+
{
|
|
2031
|
+
value: cancellationNotes,
|
|
2032
|
+
onChange: (event) => setCancellationNotes(event.target.value),
|
|
2033
|
+
placeholder: needsCancellationNotes ? t("agenda.cancellation.notesRequired") : t("agenda.cancellation.notesOptional"),
|
|
2034
|
+
rows: 2,
|
|
2035
|
+
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"
|
|
2036
|
+
}
|
|
2037
|
+
)
|
|
2038
|
+
] })
|
|
2039
|
+
] }),
|
|
1936
2040
|
!showNotes ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 py-1", children: [
|
|
1937
2041
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
1938
2042
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -2060,19 +2164,19 @@ function FloatingPanel({
|
|
|
2060
2164
|
children,
|
|
2061
2165
|
zIndex = 100
|
|
2062
2166
|
}) {
|
|
2063
|
-
const ref =
|
|
2064
|
-
const [phase, setPhase] =
|
|
2065
|
-
const phaseRef =
|
|
2167
|
+
const ref = React12.useRef(null);
|
|
2168
|
+
const [phase, setPhase] = React12.useState("visible");
|
|
2169
|
+
const phaseRef = React12.useRef("visible");
|
|
2066
2170
|
phaseRef.current = phase;
|
|
2067
|
-
const onCloseRef =
|
|
2171
|
+
const onCloseRef = React12.useRef(onClose);
|
|
2068
2172
|
onCloseRef.current = onClose;
|
|
2069
|
-
const expandCallbackRef =
|
|
2070
|
-
const dismiss =
|
|
2173
|
+
const expandCallbackRef = React12.useRef(null);
|
|
2174
|
+
const dismiss = React12.useCallback(() => {
|
|
2071
2175
|
if (phaseRef.current !== "visible") return;
|
|
2072
2176
|
setPhase("dismissing");
|
|
2073
2177
|
setTimeout(() => onCloseRef.current(), 150);
|
|
2074
2178
|
}, []);
|
|
2075
|
-
const expandThen =
|
|
2179
|
+
const expandThen = React12.useCallback((callback) => {
|
|
2076
2180
|
if (phaseRef.current !== "visible") return;
|
|
2077
2181
|
expandCallbackRef.current = callback;
|
|
2078
2182
|
setPhase("expanding");
|
|
@@ -2081,7 +2185,7 @@ function FloatingPanel({
|
|
|
2081
2185
|
onCloseRef.current();
|
|
2082
2186
|
}, 220);
|
|
2083
2187
|
}, []);
|
|
2084
|
-
|
|
2188
|
+
React12.useEffect(() => {
|
|
2085
2189
|
function handleClick(e) {
|
|
2086
2190
|
if (phaseRef.current !== "visible") return;
|
|
2087
2191
|
if (ref.current && !ref.current.contains(e.target)) {
|
|
@@ -2132,10 +2236,10 @@ function fmtCurrency2(amount, locale, code) {
|
|
|
2132
2236
|
return new Intl.NumberFormat(locale, { style: "currency", currency: code }).format(amount);
|
|
2133
2237
|
}
|
|
2134
2238
|
function PopoverStatusSelect({ value, statuses, bookingStartsAt, onChange }) {
|
|
2135
|
-
const [open, setOpen] =
|
|
2136
|
-
const ref =
|
|
2239
|
+
const [open, setOpen] = React12.useState(false);
|
|
2240
|
+
const ref = React12.useRef(null);
|
|
2137
2241
|
const current = statuses.find((s) => s.value === value);
|
|
2138
|
-
|
|
2242
|
+
React12.useEffect(() => {
|
|
2139
2243
|
if (!open) return;
|
|
2140
2244
|
const h = (e) => {
|
|
2141
2245
|
if (ref.current && !ref.current.contains(e.target)) setOpen(false);
|
|
@@ -2188,8 +2292,8 @@ function AppointmentPopover({ booking, position, onClose, onEdit }) {
|
|
|
2188
2292
|
const deleteBooking = useAgendaStore((s) => s.deleteBooking);
|
|
2189
2293
|
const updateStatus = useAgendaStore((s) => s.updateBookingStatus);
|
|
2190
2294
|
const openModal = useAgendaStore((s) => s.openAppointmentModal);
|
|
2191
|
-
const [confirmingDelete, setConfirmingDelete] =
|
|
2192
|
-
const [deleting, setDeleting] =
|
|
2295
|
+
const [confirmingDelete, setConfirmingDelete] = React12.useState(false);
|
|
2296
|
+
const [deleting, setDeleting] = React12.useState(false);
|
|
2193
2297
|
const statusConfig = config.statuses.find((s) => s.value === booking.status);
|
|
2194
2298
|
const color = statusConfig?.color ?? "#6b7280";
|
|
2195
2299
|
const timeRange = `${fmtTime(booking.startsAt)}${booking.endsAt ? " \u2013 " + fmtTime(booking.endsAt) : ""}`;
|
|
@@ -2247,7 +2351,7 @@ function AppointmentPopover({ booking, position, onClose, onEdit }) {
|
|
|
2247
2351
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-3", children: [
|
|
2248
2352
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-1.5 h-3 w-3 rounded shrink-0", style: { backgroundColor: color } }),
|
|
2249
2353
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0", children: [
|
|
2250
|
-
/* @__PURE__ */ jsxRuntime.jsx(saas.PersonLink, { personId: booking.clientId, name: booking.clientName ?? "Unknown", className: "text-base" }),
|
|
2354
|
+
config.capabilities.client ? /* @__PURE__ */ jsxRuntime.jsx(saas.PersonLink, { personId: booking.clientId, name: booking.clientName ?? "Unknown", className: "text-base" }) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-base font-medium truncate", children: booking.clientName || t("agenda.appointment.untitled") }),
|
|
2251
2355
|
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-muted-foreground mt-0.5", children: [
|
|
2252
2356
|
fmtDate(booking.startsAt),
|
|
2253
2357
|
" \xB7 ",
|
|
@@ -2267,7 +2371,7 @@ function AppointmentPopover({ booking, position, onClose, onEdit }) {
|
|
|
2267
2371
|
] })
|
|
2268
2372
|
] }, svc.id)) })
|
|
2269
2373
|
] }),
|
|
2270
|
-
booking.professionalName && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
2374
|
+
config.capabilities.professional && booking.professionalName && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
2271
2375
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.User, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
2272
2376
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm", children: booking.professionalName })
|
|
2273
2377
|
] }),
|
|
@@ -2318,7 +2422,7 @@ function AppointmentPopover({ booking, position, onClose, onEdit }) {
|
|
|
2318
2422
|
)
|
|
2319
2423
|
] });
|
|
2320
2424
|
})(),
|
|
2321
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "pt-2 border-t fayz-glass-divider", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2425
|
+
config.capabilities.status && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pt-2 border-t fayz-glass-divider", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2322
2426
|
PopoverStatusSelect,
|
|
2323
2427
|
{
|
|
2324
2428
|
value: booking.status,
|
|
@@ -2347,9 +2451,9 @@ function Tooltip2({ anchor, label }) {
|
|
|
2347
2451
|
);
|
|
2348
2452
|
}
|
|
2349
2453
|
function StatusDot({ color, label, isActive, disabled, onClick }) {
|
|
2350
|
-
const [hover, setHover] =
|
|
2351
|
-
const btnRef =
|
|
2352
|
-
const [rect, setRect] =
|
|
2454
|
+
const [hover, setHover] = React12.useState(false);
|
|
2455
|
+
const btnRef = React12.useRef(null);
|
|
2456
|
+
const [rect, setRect] = React12.useState(null);
|
|
2353
2457
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2354
2458
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2355
2459
|
"button",
|
|
@@ -2376,8 +2480,8 @@ function EventContextMenu({ booking, position, onClose, onEdit }) {
|
|
|
2376
2480
|
const config = useAgendaConfig();
|
|
2377
2481
|
const updateStatus = useAgendaStore((s) => s.updateBookingStatus);
|
|
2378
2482
|
const deleteBooking = useAgendaStore((s) => s.deleteBooking);
|
|
2379
|
-
const [confirming, setConfirming] =
|
|
2380
|
-
const [deleting, setDeleting] =
|
|
2483
|
+
const [confirming, setConfirming] = React12.useState(false);
|
|
2484
|
+
const [deleting, setDeleting] = React12.useState(false);
|
|
2381
2485
|
async function handleDelete() {
|
|
2382
2486
|
setDeleting(true);
|
|
2383
2487
|
try {
|
|
@@ -2435,7 +2539,7 @@ function EventContextMenu({ booking, position, onClose, onEdit }) {
|
|
|
2435
2539
|
}
|
|
2436
2540
|
)
|
|
2437
2541
|
] }),
|
|
2438
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-t px-4 py-3", children: [
|
|
2542
|
+
config.capabilities.status && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-t px-4 py-3", children: [
|
|
2439
2543
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[11px] font-medium text-muted-foreground mb-2", children: t("agenda.contextMenu.changeStatus") }),
|
|
2440
2544
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-1.5", children: config.statuses.map((s) => {
|
|
2441
2545
|
const available = isStatusAvailable(s, booking.startsAt);
|
|
@@ -2459,6 +2563,66 @@ function EventContextMenu({ booking, position, onClose, onEdit }) {
|
|
|
2459
2563
|
] })
|
|
2460
2564
|
] }) });
|
|
2461
2565
|
}
|
|
2566
|
+
function startOfDay(d) {
|
|
2567
|
+
const x = new Date(d);
|
|
2568
|
+
x.setHours(0, 0, 0, 0);
|
|
2569
|
+
return x;
|
|
2570
|
+
}
|
|
2571
|
+
function timeLabel(iso) {
|
|
2572
|
+
const d = new Date(iso);
|
|
2573
|
+
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
2574
|
+
}
|
|
2575
|
+
function AgendaMobileList({ bookings, statusColors, currencyCode, onSelect }) {
|
|
2576
|
+
const t = core.useTranslation();
|
|
2577
|
+
const locale = core.getCurrentLocale() === "pt-BR" ? "pt-BR" : "en";
|
|
2578
|
+
const groups = React12.useMemo(() => {
|
|
2579
|
+
const todayStart = startOfDay(/* @__PURE__ */ new Date()).getTime();
|
|
2580
|
+
const sorted = bookings.filter((b) => !!b.startsAt).slice().sort((a, b) => a.startsAt < b.startsAt ? -1 : a.startsAt > b.startsAt ? 1 : 0);
|
|
2581
|
+
const map = /* @__PURE__ */ new Map();
|
|
2582
|
+
for (const b of sorted) {
|
|
2583
|
+
const day = startOfDay(new Date(b.startsAt));
|
|
2584
|
+
if (day.getTime() < todayStart) continue;
|
|
2585
|
+
const key = day.toISOString().slice(0, 10);
|
|
2586
|
+
const list = map.get(key) ?? [];
|
|
2587
|
+
list.push(b);
|
|
2588
|
+
map.set(key, list);
|
|
2589
|
+
}
|
|
2590
|
+
return Array.from(map.entries()).map(([key, items]) => ({ key, date: /* @__PURE__ */ new Date(key + "T00:00:00"), items }));
|
|
2591
|
+
}, [bookings]);
|
|
2592
|
+
function dayLabel(date) {
|
|
2593
|
+
const today = startOfDay(/* @__PURE__ */ new Date());
|
|
2594
|
+
const diff = Math.round((startOfDay(date).getTime() - today.getTime()) / 864e5);
|
|
2595
|
+
if (diff === 0) return t("agenda.list.today");
|
|
2596
|
+
if (diff === 1) return t("agenda.list.tomorrow");
|
|
2597
|
+
return date.toLocaleDateString(locale, { weekday: "long", day: "numeric", month: "long" });
|
|
2598
|
+
}
|
|
2599
|
+
if (groups.length === 0) {
|
|
2600
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 flex flex-col items-center justify-center px-6 py-16 text-center text-muted-foreground", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm", children: t("agenda.list.empty") }) });
|
|
2601
|
+
}
|
|
2602
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 overflow-y-auto px-3 py-3 space-y-5", children: groups.map((g) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
2603
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "px-1 pb-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: dayLabel(g.date) }),
|
|
2604
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1.5", children: g.items.map((b) => {
|
|
2605
|
+
const color = statusColors[b.status] ?? "#6b7280";
|
|
2606
|
+
const hasAmount = typeof b.orderTotal === "number" && b.orderTotal > 0;
|
|
2607
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2608
|
+
"button",
|
|
2609
|
+
{
|
|
2610
|
+
onClick: () => onSelect(b),
|
|
2611
|
+
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",
|
|
2612
|
+
children: [
|
|
2613
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-2.5 w-2.5 shrink-0 rounded-full", style: { backgroundColor: color } }),
|
|
2614
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
2615
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate text-sm font-medium text-foreground", children: b.clientName ?? "\u2014" }),
|
|
2616
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: timeLabel(b.startsAt) })
|
|
2617
|
+
] }),
|
|
2618
|
+
hasAmount && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 text-sm font-semibold tabular-nums text-foreground", children: core.formatCurrency(b.orderTotal, currencyCode) })
|
|
2619
|
+
]
|
|
2620
|
+
},
|
|
2621
|
+
b.id
|
|
2622
|
+
);
|
|
2623
|
+
}) })
|
|
2624
|
+
] }, g.key)) });
|
|
2625
|
+
}
|
|
2462
2626
|
function bookingToEvent(booking, statusColors) {
|
|
2463
2627
|
const color = statusColors[booking.status] ?? "#6b7280";
|
|
2464
2628
|
const serviceNames = booking.services?.map((s) => s.name).join(", ") ?? "";
|
|
@@ -2581,18 +2745,44 @@ function CalendarSkeleton({ profCount = 2 }) {
|
|
|
2581
2745
|
] }, h)) })
|
|
2582
2746
|
] });
|
|
2583
2747
|
}
|
|
2584
|
-
var
|
|
2748
|
+
var RESOURCE_VIEW_OPTIONS = [
|
|
2585
2749
|
{ key: "resourceTimeGridDay", labelKey: "agenda.calendar.viewDay" },
|
|
2586
2750
|
{ key: "resourceTimeGridWeek", labelKey: "agenda.calendar.viewWeek" },
|
|
2587
2751
|
{ key: "dayGridMonth", labelKey: "agenda.calendar.viewMonth" }
|
|
2588
2752
|
];
|
|
2753
|
+
var PLAIN_VIEW_OPTIONS = [
|
|
2754
|
+
{ key: "timeGridDay", labelKey: "agenda.calendar.viewDay" },
|
|
2755
|
+
{ key: "timeGridWeek", labelKey: "agenda.calendar.viewWeek" },
|
|
2756
|
+
{ key: "dayGridMonth", labelKey: "agenda.calendar.viewMonth" },
|
|
2757
|
+
{ key: "listMonth", labelKey: "agenda.calendar.viewList" }
|
|
2758
|
+
];
|
|
2759
|
+
var MOBILE_LIST_VIEW = "__mobileList__";
|
|
2760
|
+
function viewForNoResources(view) {
|
|
2761
|
+
return view.startsWith("resource") ? "dayGridMonth" : view;
|
|
2762
|
+
}
|
|
2763
|
+
function viewForResources(view) {
|
|
2764
|
+
if (view === "timeGridWeek") return "resourceTimeGridWeek";
|
|
2765
|
+
if (view === "timeGridDay") return "resourceTimeGridDay";
|
|
2766
|
+
return view;
|
|
2767
|
+
}
|
|
2768
|
+
function useIsMobile() {
|
|
2769
|
+
const [isMobile, setIsMobile] = React12.useState(() => typeof window !== "undefined" && window.matchMedia("(max-width: 767px)").matches);
|
|
2770
|
+
React12.useEffect(() => {
|
|
2771
|
+
if (typeof window === "undefined") return;
|
|
2772
|
+
const mq = window.matchMedia("(max-width: 767px)");
|
|
2773
|
+
const onChange = () => setIsMobile(mq.matches);
|
|
2774
|
+
mq.addEventListener("change", onChange);
|
|
2775
|
+
return () => mq.removeEventListener("change", onChange);
|
|
2776
|
+
}, []);
|
|
2777
|
+
return isMobile;
|
|
2778
|
+
}
|
|
2589
2779
|
function CalendarView() {
|
|
2590
2780
|
injectCalendarStyles();
|
|
2591
2781
|
const config = useAgendaConfig();
|
|
2592
2782
|
const t = core.useTranslation();
|
|
2593
2783
|
const currentLocale = core.getCurrentLocale();
|
|
2594
2784
|
const fcLocale = currentLocale === "pt-BR" ? ptBrLocale__default.default : void 0;
|
|
2595
|
-
const calendarRef =
|
|
2785
|
+
const calendarRef = React12.useRef(null);
|
|
2596
2786
|
const prefs = saas.usePluginPrefs("agenda", { calendarView: config.defaultCalendarView ?? "resourceTimeGridWeek" });
|
|
2597
2787
|
const agendaSettings = useAgendaSettings();
|
|
2598
2788
|
const effectiveBusinessHours = {
|
|
@@ -2618,28 +2808,40 @@ function CalendarView() {
|
|
|
2618
2808
|
const _openModal = useAgendaStore((s) => s.openAppointmentModal);
|
|
2619
2809
|
const closeModal = useAgendaStore((s) => s.closeAppointmentModal);
|
|
2620
2810
|
const modalState = useAgendaStore((s) => s.appointmentModal);
|
|
2621
|
-
const
|
|
2622
|
-
const
|
|
2623
|
-
const
|
|
2624
|
-
const
|
|
2625
|
-
const
|
|
2811
|
+
const isMobile = useIsMobile();
|
|
2812
|
+
const resourceCapable = professionals.length > 0;
|
|
2813
|
+
const viewOptions = resourceCapable ? RESOURCE_VIEW_OPTIONS : PLAIN_VIEW_OPTIONS;
|
|
2814
|
+
const [mobileView, setMobileView] = React12.useState(MOBILE_LIST_VIEW);
|
|
2815
|
+
const mobileViewOptions = React12.useMemo(
|
|
2816
|
+
() => [
|
|
2817
|
+
...viewOptions.filter((o) => o.key !== "listMonth"),
|
|
2818
|
+
{ key: MOBILE_LIST_VIEW, labelKey: "agenda.calendar.viewList" }
|
|
2819
|
+
],
|
|
2820
|
+
[viewOptions]
|
|
2821
|
+
);
|
|
2822
|
+
const mobileGridActive = isMobile && mobileView !== MOBILE_LIST_VIEW;
|
|
2823
|
+
const [selectedBooking, setSelectedBooking] = React12.useState(null);
|
|
2824
|
+
const [popoverAnchor, setPopoverAnchor] = React12.useState(null);
|
|
2825
|
+
const [popoverVisible, _setPopoverVisible] = React12.useState(false);
|
|
2826
|
+
const popoverVisibleRef = React12.useRef(false);
|
|
2827
|
+
const setPopoverVisible = React12.useCallback((v) => {
|
|
2626
2828
|
popoverVisibleRef.current = v;
|
|
2627
2829
|
_setPopoverVisible(v);
|
|
2628
2830
|
}, []);
|
|
2629
|
-
const [contextMenu, setContextMenu] =
|
|
2630
|
-
const [contextMenuVisible, _setContextMenuVisible] =
|
|
2631
|
-
const contextMenuVisibleRef =
|
|
2632
|
-
const setContextMenuVisible =
|
|
2831
|
+
const [contextMenu, setContextMenu] = React12.useState(null);
|
|
2832
|
+
const [contextMenuVisible, _setContextMenuVisible] = React12.useState(false);
|
|
2833
|
+
const contextMenuVisibleRef = React12.useRef(false);
|
|
2834
|
+
const setContextMenuVisible = React12.useCallback((v) => {
|
|
2633
2835
|
contextMenuVisibleRef.current = v;
|
|
2634
2836
|
_setContextMenuVisible(v);
|
|
2635
2837
|
}, []);
|
|
2636
|
-
const [scheduleConfirm, setScheduleConfirm] =
|
|
2637
|
-
const [calendarTitle, setCalendarTitle] =
|
|
2638
|
-
const [selectedDate, setSelectedDate] =
|
|
2639
|
-
const [profSectionOpen, setProfSectionOpen] =
|
|
2640
|
-
const [statusSectionOpen, setStatusSectionOpen] =
|
|
2641
|
-
const [visibleRange, setVisibleRange] =
|
|
2642
|
-
const openModal =
|
|
2838
|
+
const [scheduleConfirm, setScheduleConfirm] = React12.useState(null);
|
|
2839
|
+
const [calendarTitle, setCalendarTitle] = React12.useState("");
|
|
2840
|
+
const [selectedDate, setSelectedDate] = React12.useState(/* @__PURE__ */ new Date());
|
|
2841
|
+
const [profSectionOpen, setProfSectionOpen] = React12.useState(true);
|
|
2842
|
+
const [statusSectionOpen, setStatusSectionOpen] = React12.useState(false);
|
|
2843
|
+
const [visibleRange, setVisibleRange] = React12.useState(null);
|
|
2844
|
+
const openModal = React12.useCallback((...args) => {
|
|
2643
2845
|
setPopoverVisible(false);
|
|
2644
2846
|
setSelectedBooking(null);
|
|
2645
2847
|
setPopoverAnchor(null);
|
|
@@ -2647,17 +2849,35 @@ function CalendarView() {
|
|
|
2647
2849
|
setContextMenu(null);
|
|
2648
2850
|
_openModal(...args);
|
|
2649
2851
|
}, [_openModal]);
|
|
2650
|
-
const statusColors =
|
|
2852
|
+
const statusColors = React12.useMemo(() => {
|
|
2651
2853
|
const map = {};
|
|
2652
2854
|
for (const s of config.statuses) map[s.value] = s.color;
|
|
2653
2855
|
return map;
|
|
2654
2856
|
}, [config.statuses]);
|
|
2655
|
-
|
|
2857
|
+
React12.useEffect(() => {
|
|
2656
2858
|
fetchProfessionals();
|
|
2657
2859
|
fetchSchedules();
|
|
2658
2860
|
if (config.modules.locationSelection) fetchLocations();
|
|
2659
2861
|
}, []);
|
|
2660
|
-
|
|
2862
|
+
React12.useEffect(() => {
|
|
2863
|
+
const api = calendarRef.current?.getApi();
|
|
2864
|
+
if (!api) return;
|
|
2865
|
+
if (!resourceCapable && currentView.startsWith("resource")) {
|
|
2866
|
+
api.changeView("dayGridMonth");
|
|
2867
|
+
} else if (resourceCapable && (currentView === "timeGridWeek" || currentView === "timeGridDay")) {
|
|
2868
|
+
api.changeView(viewForResources(currentView));
|
|
2869
|
+
}
|
|
2870
|
+
}, [resourceCapable, currentView]);
|
|
2871
|
+
React12.useEffect(() => {
|
|
2872
|
+
if (!isMobile) return;
|
|
2873
|
+
const start = /* @__PURE__ */ new Date();
|
|
2874
|
+
start.setHours(0, 0, 0, 0);
|
|
2875
|
+
start.setDate(start.getDate() - 1);
|
|
2876
|
+
const end = new Date(start);
|
|
2877
|
+
end.setDate(end.getDate() + 91);
|
|
2878
|
+
fetchBookings({ start: start.toISOString(), end: end.toISOString() });
|
|
2879
|
+
}, [isMobile, fetchBookings]);
|
|
2880
|
+
const resources = React12.useMemo(() => {
|
|
2661
2881
|
const filtered = selectedProfIds.length > 0 ? professionals.filter((p) => selectedProfIds.includes(p.id)) : professionals;
|
|
2662
2882
|
return filtered.map((p) => ({
|
|
2663
2883
|
id: p.id,
|
|
@@ -2665,29 +2885,31 @@ function CalendarView() {
|
|
|
2665
2885
|
extendedProps: { avatarUrl: p.avatarUrl, locationName: p.locationName }
|
|
2666
2886
|
}));
|
|
2667
2887
|
}, [professionals, selectedProfIds]);
|
|
2668
|
-
const bookingEvents =
|
|
2669
|
-
const allLocations =
|
|
2888
|
+
const bookingEvents = React12.useMemo(() => bookings.map((b) => bookingToEvent(b, statusColors)), [bookings, statusColors]);
|
|
2889
|
+
const allLocations = React12.useMemo(() => {
|
|
2670
2890
|
const map = /* @__PURE__ */ new Map();
|
|
2671
2891
|
for (const loc of config.locations) map.set(loc.id, loc.name);
|
|
2672
2892
|
for (const loc of storeLocations) map.set(loc.id, loc.name);
|
|
2673
2893
|
return map;
|
|
2674
2894
|
}, [config.locations, storeLocations]);
|
|
2675
|
-
const locationMap =
|
|
2895
|
+
const locationMap = React12.useMemo(() => {
|
|
2676
2896
|
if (!config.modules.locationSelection || allLocations.size === 0) return void 0;
|
|
2677
2897
|
return allLocations;
|
|
2678
2898
|
}, [config.modules.locationSelection, allLocations]);
|
|
2679
|
-
const availabilityEvents =
|
|
2899
|
+
const availabilityEvents = React12.useMemo(
|
|
2680
2900
|
() => generateAvailabilityEvents(schedules, professionals, visibleRange, effectiveBusinessHours, locationMap),
|
|
2681
2901
|
[schedules, professionals, visibleRange, effectiveBusinessHours, locationMap]
|
|
2682
2902
|
);
|
|
2683
|
-
const events =
|
|
2684
|
-
const
|
|
2903
|
+
const events = React12.useMemo(() => [...bookingEvents, ...availabilityEvents], [bookingEvents, availabilityEvents]);
|
|
2904
|
+
const storedView = prefs.get("calendarView");
|
|
2905
|
+
const effectiveInitialView = resourceCapable ? viewForResources(storedView) : viewForNoResources(storedView);
|
|
2906
|
+
const handleDatesSet = React12.useCallback((arg) => {
|
|
2685
2907
|
setCalendarTitle(arg.view.title);
|
|
2686
2908
|
setView(arg.view.type);
|
|
2687
2909
|
setVisibleRange({ start: arg.startStr, end: arg.endStr });
|
|
2688
2910
|
fetchBookings({ start: arg.startStr, end: arg.endStr });
|
|
2689
2911
|
}, [fetchBookings, setView]);
|
|
2690
|
-
const handleEventDidMount =
|
|
2912
|
+
const handleEventDidMount = React12.useCallback((arg) => {
|
|
2691
2913
|
const booking = arg.event.extendedProps?.booking;
|
|
2692
2914
|
if (booking) arg.el.setAttribute("data-booking-id", booking.id);
|
|
2693
2915
|
arg.el.addEventListener("dblclick", (e) => {
|
|
@@ -2714,9 +2936,9 @@ function CalendarView() {
|
|
|
2714
2936
|
setPopoverAnchor(null);
|
|
2715
2937
|
});
|
|
2716
2938
|
}, []);
|
|
2717
|
-
const skipClickRef =
|
|
2718
|
-
const hoverTimerRef =
|
|
2719
|
-
const handleEventMouseEnter =
|
|
2939
|
+
const skipClickRef = React12.useRef(false);
|
|
2940
|
+
const hoverTimerRef = React12.useRef(null);
|
|
2941
|
+
const handleEventMouseEnter = React12.useCallback((arg) => {
|
|
2720
2942
|
if (arg.event.display === "background") return;
|
|
2721
2943
|
if (arg.event.extendedProps?.isLocationLabel) return;
|
|
2722
2944
|
const booking = arg.event.extendedProps.booking;
|
|
@@ -2735,13 +2957,13 @@ function CalendarView() {
|
|
|
2735
2957
|
setPopoverVisible(true);
|
|
2736
2958
|
}, 3e3);
|
|
2737
2959
|
}, []);
|
|
2738
|
-
const handleEventMouseLeave =
|
|
2960
|
+
const handleEventMouseLeave = React12.useCallback(() => {
|
|
2739
2961
|
if (hoverTimerRef.current) {
|
|
2740
2962
|
clearTimeout(hoverTimerRef.current);
|
|
2741
2963
|
hoverTimerRef.current = null;
|
|
2742
2964
|
}
|
|
2743
2965
|
}, []);
|
|
2744
|
-
const handleEventClick =
|
|
2966
|
+
const handleEventClick = React12.useCallback((arg) => {
|
|
2745
2967
|
if (skipClickRef.current) {
|
|
2746
2968
|
skipClickRef.current = false;
|
|
2747
2969
|
return;
|
|
@@ -2764,7 +2986,7 @@ function CalendarView() {
|
|
|
2764
2986
|
setPopoverAnchor({ x, y });
|
|
2765
2987
|
setPopoverVisible(true);
|
|
2766
2988
|
}, []);
|
|
2767
|
-
const findScheduleForSlot =
|
|
2989
|
+
const findScheduleForSlot = React12.useCallback((professionalId, dateTimeStr) => {
|
|
2768
2990
|
if (!professionalId) return null;
|
|
2769
2991
|
const dt = new Date(dateTimeStr);
|
|
2770
2992
|
const dateStr = dt.toISOString().slice(0, 10);
|
|
@@ -2781,7 +3003,7 @@ function CalendarView() {
|
|
|
2781
3003
|
}
|
|
2782
3004
|
return null;
|
|
2783
3005
|
}, [schedules]);
|
|
2784
|
-
const handleSelect =
|
|
3006
|
+
const handleSelect = React12.useCallback((arg) => {
|
|
2785
3007
|
calendarRef.current?.getApi().unselect();
|
|
2786
3008
|
if (popoverVisibleRef.current || contextMenuVisibleRef.current) {
|
|
2787
3009
|
setPopoverVisible(false);
|
|
@@ -2797,13 +3019,13 @@ function CalendarView() {
|
|
|
2797
3019
|
const locId = schedule?.locationId ?? schedule?.metadata?.locationId;
|
|
2798
3020
|
openModal("create", { prefill: { startsAt: arg.startStr, endsAt: arg.endStr, professionalId, locationId: locId || void 0 } });
|
|
2799
3021
|
};
|
|
2800
|
-
if (!schedule) {
|
|
3022
|
+
if (config.modules.workingHours && !schedule) {
|
|
2801
3023
|
setScheduleConfirm({ onConfirm: proceed });
|
|
2802
3024
|
return;
|
|
2803
3025
|
}
|
|
2804
3026
|
proceed();
|
|
2805
|
-
}, [openModal, findScheduleForSlot]);
|
|
2806
|
-
const handleEventDrop =
|
|
3027
|
+
}, [openModal, findScheduleForSlot, config.modules.workingHours]);
|
|
3028
|
+
const handleEventDrop = React12.useCallback(async (arg) => {
|
|
2807
3029
|
const booking = arg.event.extendedProps.booking;
|
|
2808
3030
|
try {
|
|
2809
3031
|
await rescheduleBooking(booking.id, arg.event.startStr, arg.event.endStr, arg.newResource?.id ?? void 0);
|
|
@@ -2811,13 +3033,19 @@ function CalendarView() {
|
|
|
2811
3033
|
arg.revert();
|
|
2812
3034
|
}
|
|
2813
3035
|
}, [rescheduleBooking]);
|
|
2814
|
-
const handleViewChange =
|
|
3036
|
+
const handleViewChange = React12.useCallback((v) => {
|
|
2815
3037
|
calendarRef.current?.getApi().changeView(v);
|
|
2816
3038
|
prefs.set("calendarView", v);
|
|
2817
3039
|
}, [prefs]);
|
|
2818
|
-
const
|
|
2819
|
-
|
|
2820
|
-
|
|
3040
|
+
const handleMobileViewChange = React12.useCallback((v) => {
|
|
3041
|
+
setMobileView(v);
|
|
3042
|
+
if (v === MOBILE_LIST_VIEW) return;
|
|
3043
|
+
setView(v);
|
|
3044
|
+
calendarRef.current?.getApi()?.changeView(v);
|
|
3045
|
+
}, [setView]);
|
|
3046
|
+
const handlePrev = React12.useCallback(() => calendarRef.current?.getApi().prev(), []);
|
|
3047
|
+
const handleNext = React12.useCallback(() => calendarRef.current?.getApi().next(), []);
|
|
3048
|
+
const handleToday = React12.useCallback(() => {
|
|
2821
3049
|
calendarRef.current?.getApi().today();
|
|
2822
3050
|
setSelectedDate(/* @__PURE__ */ new Date());
|
|
2823
3051
|
}, []);
|
|
@@ -2833,9 +3061,9 @@ function CalendarView() {
|
|
|
2833
3061
|
const next = selectedStatuses.includes(value) ? selectedStatuses.filter((s) => s !== value) : [...selectedStatuses, value];
|
|
2834
3062
|
setFilters({ statuses: next });
|
|
2835
3063
|
}
|
|
2836
|
-
const wrapperRef =
|
|
2837
|
-
const [dayHeaderCompact, setDayHeaderCompact] =
|
|
2838
|
-
|
|
3064
|
+
const wrapperRef = React12.useRef(null);
|
|
3065
|
+
const [dayHeaderCompact, setDayHeaderCompact] = React12.useState(false);
|
|
3066
|
+
React12.useEffect(() => {
|
|
2839
3067
|
const container = wrapperRef.current;
|
|
2840
3068
|
if (!container) return;
|
|
2841
3069
|
let frame = 0;
|
|
@@ -2878,7 +3106,7 @@ function CalendarView() {
|
|
|
2878
3106
|
ro.disconnect();
|
|
2879
3107
|
};
|
|
2880
3108
|
}, [resources, currentView, visibleRange]);
|
|
2881
|
-
const applyResourceStriping =
|
|
3109
|
+
const applyResourceStriping = React12.useCallback(() => {
|
|
2882
3110
|
const container = wrapperRef.current;
|
|
2883
3111
|
if (!container) return;
|
|
2884
3112
|
const resourceIds = resources.map((r) => r.id);
|
|
@@ -2889,7 +3117,7 @@ function CalendarView() {
|
|
|
2889
3117
|
el.classList.toggle("fc-resource-alt", idx % 2 === 1);
|
|
2890
3118
|
});
|
|
2891
3119
|
}, [resources]);
|
|
2892
|
-
|
|
3120
|
+
React12.useEffect(() => {
|
|
2893
3121
|
applyResourceStriping();
|
|
2894
3122
|
const t1 = setTimeout(applyResourceStriping, 100);
|
|
2895
3123
|
const t2 = setTimeout(applyResourceStriping, 300);
|
|
@@ -2898,15 +3126,28 @@ function CalendarView() {
|
|
|
2898
3126
|
clearTimeout(t2);
|
|
2899
3127
|
};
|
|
2900
3128
|
}, [applyResourceStriping, currentView, visibleRange, bookings]);
|
|
2901
|
-
|
|
3129
|
+
React12.useEffect(() => {
|
|
2902
3130
|
function handleOpenBooking(e) {
|
|
2903
|
-
const
|
|
2904
|
-
|
|
3131
|
+
const detail = e.detail ?? {};
|
|
3132
|
+
const externalSource = detail.source ? { type: String(detail.source), id: detail.waitlistId, waitlistId: detail.waitlistId } : void 0;
|
|
3133
|
+
if (detail.bookingId) {
|
|
3134
|
+
openModal("edit", { bookingId: detail.bookingId, externalSource });
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
3137
|
+
openModal("create", {
|
|
3138
|
+
prefill: detail.prefill ?? {
|
|
3139
|
+
clientId: detail.clientId,
|
|
3140
|
+
professionalId: detail.professionalId,
|
|
3141
|
+
locationId: detail.locationId,
|
|
3142
|
+
startsAt: detail.startsAt
|
|
3143
|
+
},
|
|
3144
|
+
externalSource
|
|
3145
|
+
});
|
|
2905
3146
|
}
|
|
2906
3147
|
window.addEventListener("agenda:open-booking", handleOpenBooking);
|
|
2907
3148
|
return () => window.removeEventListener("agenda:open-booking", handleOpenBooking);
|
|
2908
3149
|
}, [openModal]);
|
|
2909
|
-
const dayHeaderContent =
|
|
3150
|
+
const dayHeaderContent = React12.useCallback((arg) => {
|
|
2910
3151
|
const date = arg.date;
|
|
2911
3152
|
const dateLocale = currentLocale === "pt-BR" ? "pt-BR" : "en";
|
|
2912
3153
|
const wrapStyle = { display: "flex", flexDirection: "column", alignItems: "center", lineHeight: 1.15 };
|
|
@@ -2924,7 +3165,7 @@ function CalendarView() {
|
|
|
2924
3165
|
/* @__PURE__ */ jsxRuntime.jsx("span", { style: dateStyle, children: `${date.getMonth() + 1}/${date.getDate()}` })
|
|
2925
3166
|
] });
|
|
2926
3167
|
}, [dayHeaderCompact, currentLocale]);
|
|
2927
|
-
const resourceLabelContent =
|
|
3168
|
+
const resourceLabelContent = React12.useCallback((arg) => {
|
|
2928
3169
|
const { id, title, extendedProps } = arg.resource;
|
|
2929
3170
|
const initials = title.split(" ").map((n) => n[0]).join("").slice(0, 2);
|
|
2930
3171
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 px-2 py-1.5", children: [
|
|
@@ -2944,7 +3185,7 @@ function CalendarView() {
|
|
|
2944
3185
|
] })
|
|
2945
3186
|
] });
|
|
2946
3187
|
}, []);
|
|
2947
|
-
const eventContent =
|
|
3188
|
+
const eventContent = React12.useCallback((arg) => {
|
|
2948
3189
|
if (arg.event.extendedProps?.isLocationLabel) {
|
|
2949
3190
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-1.5 py-0.5 overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] font-medium truncate", children: arg.event.title }) });
|
|
2950
3191
|
}
|
|
@@ -2975,110 +3216,135 @@ function CalendarView() {
|
|
|
2975
3216
|
}
|
|
2976
3217
|
),
|
|
2977
3218
|
/* @__PURE__ */ jsxRuntime.jsx(MiniCalendar, { selectedDate, onDateSelect: handleMiniDateSelect }),
|
|
2978
|
-
/* @__PURE__ */ jsxRuntime.
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
/* @__PURE__ */ jsxRuntime.
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
{
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3219
|
+
config.capabilities.professional && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3220
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-px bg-border my-4" }),
|
|
3221
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3222
|
+
"button",
|
|
3223
|
+
{
|
|
3224
|
+
onClick: () => setProfSectionOpen((p) => !p),
|
|
3225
|
+
className: "flex items-center justify-between w-full text-xs font-medium text-muted-foreground hover:text-foreground py-1 transition-colors",
|
|
3226
|
+
children: [
|
|
3227
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "Agendas" }),
|
|
3228
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { className: `h-3.5 w-3.5 transition-transform ${profSectionOpen ? "" : "-rotate-90"}` })
|
|
3229
|
+
]
|
|
3230
|
+
}
|
|
3231
|
+
),
|
|
3232
|
+
profSectionOpen && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-1.5 space-y-0.5", children: professionals.map((prof) => {
|
|
3233
|
+
const active = selectedProfIds.length === 0 || selectedProfIds.includes(prof.id);
|
|
3234
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-2.5 rounded px-1 py-1 cursor-pointer hover:bg-muted/50 transition-colors", children: [
|
|
3235
|
+
/* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx("svg", { className: "h-2.5 w-2.5 text-primary-foreground", viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M2 6l3 3 5-5", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }),
|
|
3236
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: `text-xs truncate ${active ? "text-foreground" : "text-muted-foreground"}`, children: prof.name }),
|
|
3237
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", className: "sr-only", checked: active, onChange: () => toggleProfessional(prof.id) })
|
|
3238
|
+
] }, prof.id);
|
|
3239
|
+
}) })
|
|
3240
|
+
] }),
|
|
3241
|
+
config.capabilities.status && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3242
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-px bg-border my-4" }),
|
|
3243
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3244
|
+
"button",
|
|
3245
|
+
{
|
|
3246
|
+
onClick: () => setStatusSectionOpen((p) => !p),
|
|
3247
|
+
className: "flex items-center justify-between w-full text-xs font-medium text-muted-foreground hover:text-foreground py-1 transition-colors",
|
|
3248
|
+
children: [
|
|
3249
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "Status" }),
|
|
3250
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { className: `h-3.5 w-3.5 transition-transform ${statusSectionOpen ? "" : "-rotate-90"}` })
|
|
3251
|
+
]
|
|
3252
|
+
}
|
|
3253
|
+
),
|
|
3254
|
+
statusSectionOpen && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-1.5 space-y-0.5", children: config.statuses.map((st) => {
|
|
3255
|
+
const active = selectedStatuses.length === 0 || selectedStatuses.includes(st.value);
|
|
3256
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-2.5 rounded px-1 py-1 cursor-pointer hover:bg-muted/50 transition-colors", children: [
|
|
3257
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3258
|
+
"span",
|
|
3259
|
+
{
|
|
3260
|
+
className: "flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border-2 transition-colors",
|
|
3261
|
+
style: { borderColor: active ? st.color : st.color + "40", backgroundColor: active ? st.color : "transparent" },
|
|
3262
|
+
children: active && /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "h-2.5 w-2.5 text-white", viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M2 6l3 3 5-5", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) })
|
|
3263
|
+
}
|
|
3264
|
+
),
|
|
3265
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: `text-xs ${active ? "text-foreground" : "text-muted-foreground"}`, children: st.label }),
|
|
3266
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", className: "sr-only", checked: active, onChange: () => toggleStatus(st.value) })
|
|
3267
|
+
] }, st.value);
|
|
3268
|
+
}) })
|
|
3269
|
+
] })
|
|
3025
3270
|
] }),
|
|
3026
3271
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex flex-col min-w-0 overflow-hidden", children: [
|
|
3027
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "
|
|
3028
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center
|
|
3029
|
-
/* @__PURE__ */ jsxRuntime.
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
"
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3272
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 border-b", children: [
|
|
3273
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between pl-4 pr-14 md:px-4 h-12", children: [
|
|
3274
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [
|
|
3275
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3276
|
+
"button",
|
|
3277
|
+
{
|
|
3278
|
+
onClick: () => openModal("create"),
|
|
3279
|
+
className: "md:hidden flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground",
|
|
3280
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-4 w-4" })
|
|
3281
|
+
}
|
|
3282
|
+
),
|
|
3283
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${mobileGridActive ? "flex" : "hidden"} md:flex items-center gap-3`, children: [
|
|
3284
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3285
|
+
"button",
|
|
3286
|
+
{
|
|
3287
|
+
onClick: handleToday,
|
|
3288
|
+
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",
|
|
3289
|
+
children: t("agenda.toolbar.today")
|
|
3290
|
+
}
|
|
3291
|
+
),
|
|
3292
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5", children: [
|
|
3293
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: handlePrev, className: "flex h-7 w-7 items-center justify-center rounded-full hover:bg-muted transition-colors", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronLeft, { className: "h-4 w-4" }) }),
|
|
3294
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: handleNext, className: "flex h-7 w-7 items-center justify-center rounded-full hover:bg-muted transition-colors", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRight, { className: "h-4 w-4" }) })
|
|
3295
|
+
] })
|
|
3296
|
+
] }),
|
|
3297
|
+
/* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-base sm:text-lg font-normal truncate", children: isMobile && !mobileGridActive ? t("agenda.calendar.viewAgenda") : calendarTitle })
|
|
3048
3298
|
] }),
|
|
3049
|
-
/* @__PURE__ */ jsxRuntime.
|
|
3299
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 shrink-0", children: [
|
|
3300
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden md:flex items-center gap-1", children: viewOptions.map((opt) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
3301
|
+
"button",
|
|
3302
|
+
{
|
|
3303
|
+
onClick: () => handleViewChange(opt.key),
|
|
3304
|
+
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"}`,
|
|
3305
|
+
children: t(opt.labelKey)
|
|
3306
|
+
},
|
|
3307
|
+
opt.key
|
|
3308
|
+
)) }),
|
|
3309
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3310
|
+
ui.Button,
|
|
3311
|
+
{
|
|
3312
|
+
variant: "outline",
|
|
3313
|
+
size: "icon",
|
|
3314
|
+
onClick: () => window.location.hash = "/settings/agenda",
|
|
3315
|
+
title: "Agenda Settings",
|
|
3316
|
+
className: "ml-1",
|
|
3317
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Settings, { className: "h-4 w-4" })
|
|
3318
|
+
}
|
|
3319
|
+
)
|
|
3320
|
+
] })
|
|
3050
3321
|
] }),
|
|
3051
|
-
/* @__PURE__ */ jsxRuntime.
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
)) }),
|
|
3061
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3062
|
-
ui.Button,
|
|
3063
|
-
{
|
|
3064
|
-
variant: "outline",
|
|
3065
|
-
size: "icon",
|
|
3066
|
-
onClick: () => window.location.hash = "/settings/agenda",
|
|
3067
|
-
title: "Agenda Settings",
|
|
3068
|
-
className: "ml-1",
|
|
3069
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Settings, { className: "h-4 w-4" })
|
|
3070
|
-
}
|
|
3071
|
-
)
|
|
3072
|
-
] })
|
|
3322
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex md:hidden items-center gap-1 overflow-x-auto px-3 pb-2", children: mobileViewOptions.map((opt) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
3323
|
+
"button",
|
|
3324
|
+
{
|
|
3325
|
+
onClick: () => handleMobileViewChange(opt.key),
|
|
3326
|
+
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"}`,
|
|
3327
|
+
children: t(opt.labelKey)
|
|
3328
|
+
},
|
|
3329
|
+
opt.key
|
|
3330
|
+
)) })
|
|
3073
3331
|
] }),
|
|
3074
|
-
|
|
3332
|
+
isMobile && !mobileGridActive ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
3333
|
+
AgendaMobileList,
|
|
3334
|
+
{
|
|
3335
|
+
bookings,
|
|
3336
|
+
statusColors,
|
|
3337
|
+
currencyCode: config.currency.code,
|
|
3338
|
+
onSelect: (b) => openModal("edit", { bookingId: b.id })
|
|
3339
|
+
}
|
|
3340
|
+
) : professionalsLoading && professionals.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(CalendarSkeleton, { profCount: 2 }) : /* @__PURE__ */ jsxRuntime.jsx("div", { ref: wrapperRef, className: "flex-1 overflow-hidden agenda-calendar", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3075
3341
|
FullCalendar__default.default,
|
|
3076
3342
|
{
|
|
3077
3343
|
ref: calendarRef,
|
|
3078
3344
|
plugins: [dayGridPlugin__default.default, timeGridPlugin__default.default, resourceTimeGridPlugin__default.default, interactionPlugin__default.default, listPlugin__default.default],
|
|
3079
|
-
initialView:
|
|
3345
|
+
initialView: mobileGridActive ? mobileView : effectiveInitialView,
|
|
3080
3346
|
schedulerLicenseKey: "CC-Attribution-NonCommercial-NoDerivatives",
|
|
3081
|
-
resources,
|
|
3347
|
+
resources: resourceCapable ? resources : void 0,
|
|
3082
3348
|
events,
|
|
3083
3349
|
headerToolbar: false,
|
|
3084
3350
|
locale: fcLocale,
|
|
@@ -3168,8 +3434,14 @@ function CalendarView() {
|
|
|
3168
3434
|
bookingId: modalState.bookingId,
|
|
3169
3435
|
prefill: modalState.prefill,
|
|
3170
3436
|
initialTab: modalState.initialTab,
|
|
3437
|
+
externalSource: modalState.externalSource,
|
|
3171
3438
|
onClose: closeModal,
|
|
3172
|
-
onCreated: (newId) => {
|
|
3439
|
+
onCreated: (newId, source) => {
|
|
3440
|
+
if (config.onBookingCreated) {
|
|
3441
|
+
void Promise.resolve(config.onBookingCreated({ bookingId: newId, source })).catch((error) => {
|
|
3442
|
+
console.error("Failed to run agenda booking-created bridge", error);
|
|
3443
|
+
});
|
|
3444
|
+
}
|
|
3173
3445
|
setTimeout(() => {
|
|
3174
3446
|
const booking = bookings.find((b) => b.id === newId);
|
|
3175
3447
|
if (!booking) return;
|
|
@@ -3195,862 +3467,12 @@ function CalendarView() {
|
|
|
3195
3467
|
function AgendaPage({ config, provider: provider2, store }) {
|
|
3196
3468
|
const runtime = core.usePluginRuntimeOptional();
|
|
3197
3469
|
const tenantId = runtime?.context.tenant?.id;
|
|
3198
|
-
|
|
3470
|
+
React12__default.default.useEffect(() => {
|
|
3199
3471
|
chunkSYDBDI3Q_cjs.setAgendaTenantId(tenantId);
|
|
3200
3472
|
return () => chunkSYDBDI3Q_cjs.setAgendaTenantId(void 0);
|
|
3201
3473
|
}, [tenantId]);
|
|
3202
3474
|
return /* @__PURE__ */ jsxRuntime.jsx(AgendaContextProvider, { config, provider: provider2, store, children: /* @__PURE__ */ jsxRuntime.jsx(CalendarView, {}) });
|
|
3203
3475
|
}
|
|
3204
|
-
function getTenantId() {
|
|
3205
|
-
return chunkSYDBDI3Q_cjs.getAgendaTenantId() ?? core.getActiveTenantId();
|
|
3206
|
-
}
|
|
3207
|
-
function snakeToCamel(obj) {
|
|
3208
|
-
const result = {};
|
|
3209
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
3210
|
-
result[key.replace(/_([a-z])/g, (_, c) => c.toUpperCase())] = value;
|
|
3211
|
-
}
|
|
3212
|
-
return result;
|
|
3213
|
-
}
|
|
3214
|
-
function getClients() {
|
|
3215
|
-
const supabase = core.getSupabaseClientOptional();
|
|
3216
|
-
if (!supabase) throw new Error("Supabase not initialized");
|
|
3217
|
-
return { core: supabase.schema("saas_core"), pub: supabase };
|
|
3218
|
-
}
|
|
3219
|
-
function mapBooking(row) {
|
|
3220
|
-
return snakeToCamel(row);
|
|
3221
|
-
}
|
|
3222
|
-
function mapSchedule(row) {
|
|
3223
|
-
return snakeToCamel(row);
|
|
3224
|
-
}
|
|
3225
|
-
function addMinutes(iso, minutes) {
|
|
3226
|
-
const d = new Date(iso);
|
|
3227
|
-
d.setMinutes(d.getMinutes() + minutes);
|
|
3228
|
-
return d.toISOString();
|
|
3229
|
-
}
|
|
3230
|
-
function unique(values) {
|
|
3231
|
-
return Array.from(new Set(values.filter((value) => !!value)));
|
|
3232
|
-
}
|
|
3233
|
-
function indexById(rows) {
|
|
3234
|
-
return new Map((rows ?? []).filter((row) => row?.id).map((row) => [row.id, row]));
|
|
3235
|
-
}
|
|
3236
|
-
function groupBy(rows, getKey) {
|
|
3237
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
3238
|
-
for (const row of rows) {
|
|
3239
|
-
const key = getKey(row);
|
|
3240
|
-
if (!key) continue;
|
|
3241
|
-
grouped.set(key, [...grouped.get(key) ?? [], row]);
|
|
3242
|
-
}
|
|
3243
|
-
return grouped;
|
|
3244
|
-
}
|
|
3245
|
-
function getDurationMinutes(startsAt, endsAt) {
|
|
3246
|
-
if (!endsAt) return null;
|
|
3247
|
-
return Math.round((new Date(endsAt).getTime() - new Date(startsAt).getTime()) / 6e4);
|
|
3248
|
-
}
|
|
3249
|
-
function mergeBookings(viewBookings, canonicalBookings) {
|
|
3250
|
-
const canonicalIds = new Set(canonicalBookings.map((booking) => booking.id));
|
|
3251
|
-
const canonicalOrderIds = new Set(canonicalBookings.map((booking) => booking.orderId).filter(Boolean));
|
|
3252
|
-
const merged = [
|
|
3253
|
-
...canonicalBookings,
|
|
3254
|
-
...viewBookings.filter((booking) => {
|
|
3255
|
-
if (canonicalIds.has(booking.id)) return false;
|
|
3256
|
-
if (booking.orderId && canonicalOrderIds.has(booking.orderId)) return false;
|
|
3257
|
-
if (canonicalOrderIds.has(booking.id)) return false;
|
|
3258
|
-
return true;
|
|
3259
|
-
})
|
|
3260
|
-
];
|
|
3261
|
-
return merged.sort((a, b) => new Date(a.startsAt).getTime() - new Date(b.startsAt).getTime());
|
|
3262
|
-
}
|
|
3263
|
-
async function hydrateCanonicalBookings(core, bookings) {
|
|
3264
|
-
const personIds = unique(bookings.flatMap((booking) => [booking.party_id, booking.assignee_id]));
|
|
3265
|
-
const locationIds = unique(bookings.map((booking) => booking.location_id));
|
|
3266
|
-
const orderIds = unique(bookings.map((booking) => booking.order_id));
|
|
3267
|
-
unique(bookings.map((booking) => booking.id));
|
|
3268
|
-
const [
|
|
3269
|
-
{ data: persons, error: personsError },
|
|
3270
|
-
{ data: locations, error: locationsError },
|
|
3271
|
-
{ data: orders, error: ordersError },
|
|
3272
|
-
{ data: items, error: itemsError }
|
|
3273
|
-
] = await Promise.all([
|
|
3274
|
-
personIds.length ? core.from("persons").select("id, name, phone, email, avatar_url").in("id", personIds) : Promise.resolve({ data: [], error: null }),
|
|
3275
|
-
locationIds.length ? core.from("locations").select("id, name").in("id", locationIds) : Promise.resolve({ data: [], error: null }),
|
|
3276
|
-
orderIds.length ? core.from("orders").select("id, total, status").in("id", orderIds) : Promise.resolve({ data: [], error: null }),
|
|
3277
|
-
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 })
|
|
3278
|
-
]);
|
|
3279
|
-
if (personsError) throw personsError;
|
|
3280
|
-
if (locationsError) throw locationsError;
|
|
3281
|
-
if (ordersError) throw ordersError;
|
|
3282
|
-
if (itemsError) throw itemsError;
|
|
3283
|
-
const personsById = indexById(persons);
|
|
3284
|
-
const locationsById = indexById(locations);
|
|
3285
|
-
const ordersById = indexById(orders);
|
|
3286
|
-
const itemsByOrderId = groupBy(items ?? [], (item) => item.order_id);
|
|
3287
|
-
return bookings.map((booking) => {
|
|
3288
|
-
const client = booking.party_id ? personsById.get(booking.party_id) : null;
|
|
3289
|
-
const professional = booking.assignee_id ? personsById.get(booking.assignee_id) : null;
|
|
3290
|
-
const location = booking.location_id ? locationsById.get(booking.location_id) : null;
|
|
3291
|
-
const order = booking.order_id ? ordersById.get(booking.order_id) : null;
|
|
3292
|
-
const bookingItems = ((booking.order_id ? itemsByOrderId.get(booking.order_id) : null) ?? []).sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)).map((item) => ({
|
|
3293
|
-
id: item.id,
|
|
3294
|
-
serviceId: item.service_id,
|
|
3295
|
-
name: item.name,
|
|
3296
|
-
durationMinutes: item.duration_minutes,
|
|
3297
|
-
price: item.unit_price,
|
|
3298
|
-
assigneeId: item.assignee_id
|
|
3299
|
-
}));
|
|
3300
|
-
const itemDuration = bookingItems.reduce((sum, item) => sum + (item.durationMinutes ?? 0), 0);
|
|
3301
|
-
return {
|
|
3302
|
-
id: booking.id,
|
|
3303
|
-
tenantId: booking.tenant_id,
|
|
3304
|
-
kind: booking.kind,
|
|
3305
|
-
startsAt: booking.starts_at,
|
|
3306
|
-
endsAt: booking.ends_at,
|
|
3307
|
-
status: booking.status,
|
|
3308
|
-
notes: booking.notes,
|
|
3309
|
-
orderId: booking.order_id,
|
|
3310
|
-
locationId: booking.location_id,
|
|
3311
|
-
metadata: booking.metadata ?? {},
|
|
3312
|
-
clientId: booking.party_id,
|
|
3313
|
-
clientName: client?.name ?? null,
|
|
3314
|
-
clientPhone: client?.phone ?? null,
|
|
3315
|
-
clientEmail: client?.email ?? null,
|
|
3316
|
-
clientAvatarUrl: client?.avatar_url ?? null,
|
|
3317
|
-
professionalId: booking.assignee_id,
|
|
3318
|
-
professionalName: professional?.name ?? null,
|
|
3319
|
-
professionalAvatarUrl: professional?.avatar_url ?? null,
|
|
3320
|
-
locationName: location?.name ?? null,
|
|
3321
|
-
orderTotal: order?.total ?? null,
|
|
3322
|
-
orderStatus: order?.status ?? null,
|
|
3323
|
-
services: bookingItems.length ? bookingItems : null,
|
|
3324
|
-
totalDurationMinutes: itemDuration || getDurationMinutes(booking.starts_at, booking.ends_at),
|
|
3325
|
-
createdAt: booking.created_at,
|
|
3326
|
-
updatedAt: booking.updated_at
|
|
3327
|
-
};
|
|
3328
|
-
});
|
|
3329
|
-
}
|
|
3330
|
-
async function getCanonicalBookings(core, query) {
|
|
3331
|
-
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);
|
|
3332
|
-
if (query.kind) q = q.eq("kind", query.kind);
|
|
3333
|
-
if (query.statuses?.length) q = q.in("status", query.statuses);
|
|
3334
|
-
if (query.professionalIds?.length) q = q.in("assignee_id", query.professionalIds);
|
|
3335
|
-
if (query.locationId) q = q.eq("location_id", query.locationId);
|
|
3336
|
-
const { data, error } = await q.order("starts_at", { ascending: true });
|
|
3337
|
-
if (error) throw error;
|
|
3338
|
-
return hydrateCanonicalBookings(core, data ?? []);
|
|
3339
|
-
}
|
|
3340
|
-
async function getCanonicalBookingById(core, id) {
|
|
3341
|
-
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";
|
|
3342
|
-
let { data, error } = await core.from("bookings").select(columns).eq("id", id).maybeSingle();
|
|
3343
|
-
if (error) throw error;
|
|
3344
|
-
if (!data) {
|
|
3345
|
-
const fallback = await core.from("bookings").select(columns).eq("order_id", id).maybeSingle();
|
|
3346
|
-
if (fallback.error) throw fallback.error;
|
|
3347
|
-
data = fallback.data;
|
|
3348
|
-
}
|
|
3349
|
-
if (!data) return null;
|
|
3350
|
-
const [booking] = await hydrateCanonicalBookings(core, [data]);
|
|
3351
|
-
return booking ?? null;
|
|
3352
|
-
}
|
|
3353
|
-
function createSupabaseAgendaProvider() {
|
|
3354
|
-
return {
|
|
3355
|
-
// v_bookings is in public schema (PostgREST default)
|
|
3356
|
-
async getBookings(query) {
|
|
3357
|
-
const { core, pub } = getClients();
|
|
3358
|
-
let q = pub.from("v_bookings").select("*").gte("starts_at", query.dateRange.start).lte("starts_at", query.dateRange.end);
|
|
3359
|
-
if (query.kind) q = q.eq("kind", query.kind);
|
|
3360
|
-
if (query.statuses?.length) q = q.in("status", query.statuses);
|
|
3361
|
-
if (query.professionalIds?.length) q = q.in("professional_id", query.professionalIds);
|
|
3362
|
-
if (query.locationId) q = q.eq("location_id", query.locationId);
|
|
3363
|
-
q = q.order("starts_at", { ascending: true });
|
|
3364
|
-
const { data, error } = await q;
|
|
3365
|
-
if (error) throw error;
|
|
3366
|
-
const viewBookings = (data ?? []).map(mapBooking);
|
|
3367
|
-
const canonicalBookings = await getCanonicalBookings(core, query);
|
|
3368
|
-
return mergeBookings(viewBookings, canonicalBookings);
|
|
3369
|
-
},
|
|
3370
|
-
async getBookingById(id) {
|
|
3371
|
-
const { core, pub } = getClients();
|
|
3372
|
-
const { data, error } = await pub.from("v_bookings").select("*").eq("id", id).maybeSingle();
|
|
3373
|
-
if (error) throw error;
|
|
3374
|
-
if (data) return mapBooking(data);
|
|
3375
|
-
return getCanonicalBookingById(core, id);
|
|
3376
|
-
},
|
|
3377
|
-
async createBooking(input) {
|
|
3378
|
-
const { core } = getClients();
|
|
3379
|
-
const tenantId = getTenantId();
|
|
3380
|
-
if (!tenantId) throw new Error("No active tenant");
|
|
3381
|
-
const hasServices = input.services.length > 0;
|
|
3382
|
-
const totalDuration = input.services.reduce((sum, s) => sum + s.durationMinutes, 0);
|
|
3383
|
-
const totalPrice = input.services.reduce((sum, s) => sum + s.price, 0);
|
|
3384
|
-
const endsAt = addMinutes(input.startsAt, totalDuration || 30);
|
|
3385
|
-
const serviceNames = input.services.map((s) => s.name).join(", ");
|
|
3386
|
-
let contactName;
|
|
3387
|
-
if (input.clientId) {
|
|
3388
|
-
const { data: person } = await core.from("persons").select("name").eq("id", input.clientId).single();
|
|
3389
|
-
contactName = person?.name;
|
|
3390
|
-
}
|
|
3391
|
-
const { data: order, error: orderErr } = await core.from("orders").insert({
|
|
3392
|
-
tenant_id: tenantId,
|
|
3393
|
-
kind: input.kind ?? "appointment",
|
|
3394
|
-
status: "scheduled",
|
|
3395
|
-
party_id: input.clientId || null,
|
|
3396
|
-
assignee_id: input.professionalId,
|
|
3397
|
-
location_id: input.locationId ?? null,
|
|
3398
|
-
subtotal: totalPrice,
|
|
3399
|
-
total: totalPrice,
|
|
3400
|
-
notes: input.notes ?? null,
|
|
3401
|
-
metadata: {
|
|
3402
|
-
source: "agenda",
|
|
3403
|
-
...serviceNames ? { serviceNames, itemsSummary: serviceNames } : {},
|
|
3404
|
-
...contactName ? { contactName } : {}
|
|
3405
|
-
}
|
|
3406
|
-
}).select("id").single();
|
|
3407
|
-
if (orderErr) throw orderErr;
|
|
3408
|
-
const { data: booking, error: bookingErr } = await core.from("bookings").insert({
|
|
3409
|
-
tenant_id: tenantId,
|
|
3410
|
-
kind: input.kind ?? "appointment",
|
|
3411
|
-
party_id: input.clientId || null,
|
|
3412
|
-
assignee_id: input.professionalId,
|
|
3413
|
-
location_id: input.locationId ?? null,
|
|
3414
|
-
order_id: order.id,
|
|
3415
|
-
starts_at: input.startsAt,
|
|
3416
|
-
ends_at: endsAt,
|
|
3417
|
-
status: "scheduled",
|
|
3418
|
-
notes: input.notes ?? null,
|
|
3419
|
-
metadata: {
|
|
3420
|
-
source: "agenda",
|
|
3421
|
-
...serviceNames ? { serviceNames, itemsSummary: serviceNames } : {},
|
|
3422
|
-
...contactName ? { contactName } : {}
|
|
3423
|
-
}
|
|
3424
|
-
}).select("id").single();
|
|
3425
|
-
if (bookingErr) throw bookingErr;
|
|
3426
|
-
if (hasServices) {
|
|
3427
|
-
const orderItems = input.services.map((s, i) => ({
|
|
3428
|
-
order_id: order.id,
|
|
3429
|
-
service_id: s.serviceId,
|
|
3430
|
-
name: s.name,
|
|
3431
|
-
quantity: 1,
|
|
3432
|
-
unit_price: s.price,
|
|
3433
|
-
total: s.price,
|
|
3434
|
-
sort_order: i,
|
|
3435
|
-
duration_minutes: s.durationMinutes,
|
|
3436
|
-
assignee_id: s.assigneeId ?? input.professionalId
|
|
3437
|
-
}));
|
|
3438
|
-
const { error: oiErr } = await core.from("order_items").insert(orderItems);
|
|
3439
|
-
if (oiErr) throw oiErr;
|
|
3440
|
-
}
|
|
3441
|
-
const createdBooking = await this.getBookingById(booking.id);
|
|
3442
|
-
if (!createdBooking) throw new Error("Created booking did not resolve from agenda read model");
|
|
3443
|
-
return createdBooking;
|
|
3444
|
-
},
|
|
3445
|
-
async updateBooking(id, data) {
|
|
3446
|
-
const { core } = getClients();
|
|
3447
|
-
const { data: existing, error: existingErr } = await core.from("bookings").select("id, order_id, starts_at, assignee_id").eq("id", id).single();
|
|
3448
|
-
if (existingErr) throw existingErr;
|
|
3449
|
-
const updates = {};
|
|
3450
|
-
if (data.startsAt) updates.starts_at = data.startsAt;
|
|
3451
|
-
if (data.endsAt) updates.ends_at = data.endsAt;
|
|
3452
|
-
if (data.status) {
|
|
3453
|
-
updates.status = data.status;
|
|
3454
|
-
}
|
|
3455
|
-
if (data.notes !== void 0) updates.notes = data.notes;
|
|
3456
|
-
if (data.professionalId) updates.assignee_id = data.professionalId;
|
|
3457
|
-
if (data.clientId) updates.party_id = data.clientId;
|
|
3458
|
-
if (data.locationId) updates.location_id = data.locationId;
|
|
3459
|
-
if (data.services) {
|
|
3460
|
-
const totalDuration = data.services.reduce((s, svc) => s + svc.durationMinutes, 0);
|
|
3461
|
-
data.services.reduce((s, svc) => s + svc.price, 0);
|
|
3462
|
-
const startsAt = data.startsAt ?? existing?.starts_at;
|
|
3463
|
-
if (startsAt) {
|
|
3464
|
-
const ends = new Date(new Date(startsAt).getTime() + totalDuration * 6e4);
|
|
3465
|
-
updates.ends_at = ends.toISOString();
|
|
3466
|
-
}
|
|
3467
|
-
}
|
|
3468
|
-
const effectiveAssigneeId = data.professionalId ?? existing?.assignee_id ?? null;
|
|
3469
|
-
const { error } = await core.from("bookings").update(updates).eq("id", id);
|
|
3470
|
-
if (error) throw error;
|
|
3471
|
-
const orderId = existing?.order_id;
|
|
3472
|
-
if (orderId) {
|
|
3473
|
-
const orderUpdates = {};
|
|
3474
|
-
if (data.notes !== void 0) orderUpdates.notes = data.notes;
|
|
3475
|
-
if (data.professionalId) orderUpdates.assignee_id = data.professionalId;
|
|
3476
|
-
if (data.clientId) orderUpdates.party_id = data.clientId;
|
|
3477
|
-
if (data.locationId) orderUpdates.location_id = data.locationId;
|
|
3478
|
-
if (data.services) {
|
|
3479
|
-
const totalPrice = data.services.reduce((s, svc) => s + svc.price, 0);
|
|
3480
|
-
orderUpdates.subtotal = totalPrice;
|
|
3481
|
-
orderUpdates.total = totalPrice;
|
|
3482
|
-
}
|
|
3483
|
-
if (Object.keys(orderUpdates).length > 0) {
|
|
3484
|
-
const { error: orderErr } = await core.from("orders").update(orderUpdates).eq("id", orderId);
|
|
3485
|
-
if (orderErr) throw orderErr;
|
|
3486
|
-
}
|
|
3487
|
-
if (data.services) {
|
|
3488
|
-
await core.from("order_items").delete().eq("order_id", orderId);
|
|
3489
|
-
if (data.services.length > 0) {
|
|
3490
|
-
const items = data.services.map((svc, idx) => ({
|
|
3491
|
-
order_id: orderId,
|
|
3492
|
-
service_id: svc.serviceId || null,
|
|
3493
|
-
name: svc.name,
|
|
3494
|
-
quantity: 1,
|
|
3495
|
-
unit_price: svc.price,
|
|
3496
|
-
total: svc.price,
|
|
3497
|
-
sort_order: idx,
|
|
3498
|
-
duration_minutes: svc.durationMinutes,
|
|
3499
|
-
assignee_id: svc.assigneeId ?? effectiveAssigneeId
|
|
3500
|
-
}));
|
|
3501
|
-
const { error: itemErr } = await core.from("order_items").insert(items);
|
|
3502
|
-
if (itemErr) throw itemErr;
|
|
3503
|
-
}
|
|
3504
|
-
}
|
|
3505
|
-
}
|
|
3506
|
-
return await this.getBookingById(id);
|
|
3507
|
-
},
|
|
3508
|
-
async deleteBooking(id) {
|
|
3509
|
-
const { core, pub } = getClients();
|
|
3510
|
-
const { data: booking, error: bookingErr } = await core.from("bookings").select("order_id").eq("id", id).single();
|
|
3511
|
-
if (bookingErr) throw bookingErr;
|
|
3512
|
-
const orderId = booking?.order_id;
|
|
3513
|
-
const { count, error: movementErr } = orderId ? await pub.from("financial_movements").select("id", { count: "exact", head: true }).eq("invoice_id", orderId) : { count: 0, error: null };
|
|
3514
|
-
if (movementErr) throw movementErr;
|
|
3515
|
-
if (orderId && (count ?? 0) > 0) {
|
|
3516
|
-
await pub.from("financial_movements").update({ status: "cancelled" }).eq("invoice_id", orderId);
|
|
3517
|
-
await core.from("orders").update({ status: "cancelled" }).eq("id", orderId);
|
|
3518
|
-
const { error } = await core.from("bookings").update({ status: "cancelled" }).eq("id", id);
|
|
3519
|
-
if (error) throw error;
|
|
3520
|
-
} else {
|
|
3521
|
-
if (orderId) {
|
|
3522
|
-
await pub.from("financial_movements").delete().eq("invoice_id", orderId);
|
|
3523
|
-
}
|
|
3524
|
-
const { error } = await core.from("bookings").delete().eq("id", id);
|
|
3525
|
-
if (error) throw error;
|
|
3526
|
-
if (orderId) {
|
|
3527
|
-
const { error: orderErr } = await core.from("orders").delete().eq("id", orderId);
|
|
3528
|
-
if (orderErr) throw orderErr;
|
|
3529
|
-
}
|
|
3530
|
-
}
|
|
3531
|
-
},
|
|
3532
|
-
async updateBookingStatus(id, status) {
|
|
3533
|
-
return this.updateBooking(id, { status });
|
|
3534
|
-
},
|
|
3535
|
-
async checkConflict(params) {
|
|
3536
|
-
const { core } = getClients();
|
|
3537
|
-
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);
|
|
3538
|
-
if (params.excludeBookingId) {
|
|
3539
|
-
q = q.neq("id", params.excludeBookingId);
|
|
3540
|
-
}
|
|
3541
|
-
const { count, error } = await q;
|
|
3542
|
-
if (error) throw error;
|
|
3543
|
-
return (count ?? 0) > 0;
|
|
3544
|
-
},
|
|
3545
|
-
async getAvailableSlots(params) {
|
|
3546
|
-
const { pub } = getClients();
|
|
3547
|
-
const { data, error } = await pub.rpc("get_available_slots", {
|
|
3548
|
-
p_tenant_id: getTenantId(),
|
|
3549
|
-
p_assignee_id: params.assigneeId,
|
|
3550
|
-
p_date: params.date,
|
|
3551
|
-
p_duration_minutes: params.durationMinutes,
|
|
3552
|
-
p_slot_interval: params.slotInterval ?? 30
|
|
3553
|
-
});
|
|
3554
|
-
if (error) throw error;
|
|
3555
|
-
return (data ?? []).map((row) => ({ start: row.slot_start, end: row.slot_end }));
|
|
3556
|
-
},
|
|
3557
|
-
async getSchedules(professionalId) {
|
|
3558
|
-
const { core } = getClients();
|
|
3559
|
-
let q = core.from("schedules").select("*").eq("kind", "working_hours");
|
|
3560
|
-
if (professionalId) q = q.eq("assignee_id", professionalId);
|
|
3561
|
-
q = q.order("day_of_week").order("starts_at");
|
|
3562
|
-
const { data, error } = await q;
|
|
3563
|
-
if (error) throw error;
|
|
3564
|
-
return (data ?? []).map(mapSchedule);
|
|
3565
|
-
},
|
|
3566
|
-
async saveSchedule(input) {
|
|
3567
|
-
const { core } = getClients();
|
|
3568
|
-
const tenantId = getTenantId();
|
|
3569
|
-
if (!tenantId) throw new Error("No active tenant");
|
|
3570
|
-
const { data, error } = await core.from("schedules").insert({
|
|
3571
|
-
tenant_id: tenantId,
|
|
3572
|
-
kind: "working_hours",
|
|
3573
|
-
assignee_id: input.assigneeId,
|
|
3574
|
-
location_id: input.locationId ?? null,
|
|
3575
|
-
day_of_week: input.dayOfWeek ?? null,
|
|
3576
|
-
specific_date: input.specificDate ?? null,
|
|
3577
|
-
starts_at: input.startsAt,
|
|
3578
|
-
ends_at: input.endsAt,
|
|
3579
|
-
is_active: input.isActive !== false,
|
|
3580
|
-
metadata: input.metadata ?? {}
|
|
3581
|
-
}).select("*").single();
|
|
3582
|
-
if (error) throw error;
|
|
3583
|
-
return mapSchedule(data);
|
|
3584
|
-
},
|
|
3585
|
-
async deleteSchedule(id) {
|
|
3586
|
-
const { core } = getClients();
|
|
3587
|
-
const { error } = await core.from("schedules").delete().eq("id", id);
|
|
3588
|
-
if (error) throw error;
|
|
3589
|
-
},
|
|
3590
|
-
async getProfessionals() {
|
|
3591
|
-
const { core } = getClients();
|
|
3592
|
-
const { data, error } = await core.from("persons").select("id, name, avatar_url, is_active").eq("kind", "staff").eq("is_active", true).order("name");
|
|
3593
|
-
if (error) throw error;
|
|
3594
|
-
return (data ?? []).map((p) => ({
|
|
3595
|
-
id: p.id,
|
|
3596
|
-
name: p.name,
|
|
3597
|
-
avatarUrl: p.avatar_url,
|
|
3598
|
-
locationId: null,
|
|
3599
|
-
locationName: null,
|
|
3600
|
-
isActive: p.is_active
|
|
3601
|
-
}));
|
|
3602
|
-
},
|
|
3603
|
-
async getLocations() {
|
|
3604
|
-
const { core } = getClients();
|
|
3605
|
-
const { data, error } = await core.from("locations").select("id, name").eq("is_active", true).order("name");
|
|
3606
|
-
if (error) throw error;
|
|
3607
|
-
return (data ?? []).map((l) => ({ id: l.id, name: l.name }));
|
|
3608
|
-
},
|
|
3609
|
-
async getConfirmationsPending(daysAhead = 2) {
|
|
3610
|
-
const { pub } = getClients();
|
|
3611
|
-
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
3612
|
-
const cutoff = /* @__PURE__ */ new Date();
|
|
3613
|
-
cutoff.setDate(cutoff.getDate() + daysAhead);
|
|
3614
|
-
const { data, error } = await pub.from("v_bookings").select("*").eq("status", "scheduled").gte("starts_at", now2).lte("starts_at", cutoff.toISOString()).order("starts_at");
|
|
3615
|
-
if (error) throw error;
|
|
3616
|
-
return (data ?? []).map(mapBooking);
|
|
3617
|
-
},
|
|
3618
|
-
async sendConfirmation(bookingId, channel) {
|
|
3619
|
-
const { core } = getClients();
|
|
3620
|
-
const { data: booking } = await core.from("bookings").select("metadata").eq("id", bookingId).single();
|
|
3621
|
-
const metadata = booking?.metadata ?? {};
|
|
3622
|
-
const confirmations = metadata.confirmations ?? [];
|
|
3623
|
-
confirmations.push({ channel, sentAt: (/* @__PURE__ */ new Date()).toISOString(), status: "sent" });
|
|
3624
|
-
await core.from("bookings").update({ metadata: { ...metadata, confirmations } }).eq("id", bookingId);
|
|
3625
|
-
},
|
|
3626
|
-
async completeBooking(bookingId) {
|
|
3627
|
-
const { core } = getClients();
|
|
3628
|
-
const { data: existing, error: existingErr } = await core.from("bookings").select("order_id").eq("id", bookingId).single();
|
|
3629
|
-
if (existingErr) throw existingErr;
|
|
3630
|
-
const { error } = await core.from("bookings").update({ status: "completed" }).eq("id", bookingId);
|
|
3631
|
-
if (error) throw error;
|
|
3632
|
-
const orderId = existing?.order_id;
|
|
3633
|
-
const booking = await this.getBookingById(bookingId);
|
|
3634
|
-
if (!booking) throw new Error("Booking not found");
|
|
3635
|
-
return { booking, orderId: orderId ?? bookingId };
|
|
3636
|
-
}
|
|
3637
|
-
};
|
|
3638
|
-
}
|
|
3639
|
-
|
|
3640
|
-
// src/data/mock.ts
|
|
3641
|
-
var nextId = 1;
|
|
3642
|
-
function uid() {
|
|
3643
|
-
return `mock-${nextId++}`;
|
|
3644
|
-
}
|
|
3645
|
-
function now() {
|
|
3646
|
-
return (/* @__PURE__ */ new Date()).toISOString();
|
|
3647
|
-
}
|
|
3648
|
-
function addMinutes2(date, minutes) {
|
|
3649
|
-
const d = new Date(date);
|
|
3650
|
-
d.setMinutes(d.getMinutes() + minutes);
|
|
3651
|
-
return d.toISOString();
|
|
3652
|
-
}
|
|
3653
|
-
function timeOverlaps(aStart, aEnd, bStart, bEnd) {
|
|
3654
|
-
return aStart < bEnd && aEnd > bStart;
|
|
3655
|
-
}
|
|
3656
|
-
var SAMPLE_PROFESSIONALS = [
|
|
3657
|
-
{ id: "prof-1", name: "Ana Silva", avatarUrl: null, locationId: "loc-1", locationName: "Main Branch", isActive: true },
|
|
3658
|
-
{ id: "prof-2", name: "Carlos Santos", avatarUrl: null, locationId: "loc-1", locationName: "Main Branch", isActive: true },
|
|
3659
|
-
{ id: "prof-3", name: "Maria Oliveira", avatarUrl: null, locationId: "loc-1", locationName: "Main Branch", isActive: true }
|
|
3660
|
-
];
|
|
3661
|
-
function createSampleBookings() {
|
|
3662
|
-
const today = /* @__PURE__ */ new Date();
|
|
3663
|
-
const year = today.getFullYear();
|
|
3664
|
-
const month = today.getMonth();
|
|
3665
|
-
const day = today.getDate();
|
|
3666
|
-
function dt(hour, minute = 0) {
|
|
3667
|
-
return new Date(year, month, day, hour, minute).toISOString();
|
|
3668
|
-
}
|
|
3669
|
-
return [
|
|
3670
|
-
{
|
|
3671
|
-
id: "bk-1",
|
|
3672
|
-
tenantId: "mock",
|
|
3673
|
-
kind: "appointment",
|
|
3674
|
-
startsAt: dt(9, 0),
|
|
3675
|
-
endsAt: dt(10, 30),
|
|
3676
|
-
status: "confirmed",
|
|
3677
|
-
notes: null,
|
|
3678
|
-
orderId: "ord-1",
|
|
3679
|
-
locationId: "loc-1",
|
|
3680
|
-
metadata: {},
|
|
3681
|
-
clientId: "cl-1",
|
|
3682
|
-
clientName: "Sarah Johnson",
|
|
3683
|
-
clientPhone: "(11) 99999-1111",
|
|
3684
|
-
clientEmail: "sarah@email.com",
|
|
3685
|
-
clientAvatarUrl: null,
|
|
3686
|
-
professionalId: "prof-1",
|
|
3687
|
-
professionalName: "Ana Silva",
|
|
3688
|
-
professionalAvatarUrl: null,
|
|
3689
|
-
locationName: "Main Branch",
|
|
3690
|
-
orderTotal: 250,
|
|
3691
|
-
orderStatus: "draft",
|
|
3692
|
-
services: [{ id: "bi-1", serviceId: "svc-1", name: "Balayage", durationMinutes: 90, price: 250, assigneeId: "prof-1" }],
|
|
3693
|
-
totalDurationMinutes: 90,
|
|
3694
|
-
createdAt: now(),
|
|
3695
|
-
updatedAt: now()
|
|
3696
|
-
},
|
|
3697
|
-
{
|
|
3698
|
-
id: "bk-2",
|
|
3699
|
-
tenantId: "mock",
|
|
3700
|
-
kind: "appointment",
|
|
3701
|
-
startsAt: dt(10, 0),
|
|
3702
|
-
endsAt: dt(11, 0),
|
|
3703
|
-
status: "scheduled",
|
|
3704
|
-
notes: null,
|
|
3705
|
-
orderId: "ord-2",
|
|
3706
|
-
locationId: "loc-1",
|
|
3707
|
-
metadata: {},
|
|
3708
|
-
clientId: "cl-2",
|
|
3709
|
-
clientName: "Emily Carter",
|
|
3710
|
-
clientPhone: "(11) 99999-2222",
|
|
3711
|
-
clientEmail: "emily@email.com",
|
|
3712
|
-
clientAvatarUrl: null,
|
|
3713
|
-
professionalId: "prof-2",
|
|
3714
|
-
professionalName: "Carlos Santos",
|
|
3715
|
-
professionalAvatarUrl: null,
|
|
3716
|
-
locationName: "Main Branch",
|
|
3717
|
-
orderTotal: 80,
|
|
3718
|
-
orderStatus: "draft",
|
|
3719
|
-
services: [{ id: "bi-2", serviceId: "svc-2", name: "Haircut", durationMinutes: 60, price: 80, assigneeId: "prof-2" }],
|
|
3720
|
-
totalDurationMinutes: 60,
|
|
3721
|
-
createdAt: now(),
|
|
3722
|
-
updatedAt: now()
|
|
3723
|
-
},
|
|
3724
|
-
{
|
|
3725
|
-
id: "bk-3",
|
|
3726
|
-
tenantId: "mock",
|
|
3727
|
-
kind: "appointment",
|
|
3728
|
-
startsAt: dt(14, 0),
|
|
3729
|
-
endsAt: dt(15, 0),
|
|
3730
|
-
status: "confirmed",
|
|
3731
|
-
notes: "First visit",
|
|
3732
|
-
orderId: "ord-3",
|
|
3733
|
-
locationId: "loc-1",
|
|
3734
|
-
metadata: {},
|
|
3735
|
-
clientId: "cl-3",
|
|
3736
|
-
clientName: "Rachel Kim",
|
|
3737
|
-
clientPhone: "(11) 99999-3333",
|
|
3738
|
-
clientEmail: null,
|
|
3739
|
-
clientAvatarUrl: null,
|
|
3740
|
-
professionalId: "prof-3",
|
|
3741
|
-
professionalName: "Maria Oliveira",
|
|
3742
|
-
professionalAvatarUrl: null,
|
|
3743
|
-
locationName: "Main Branch",
|
|
3744
|
-
orderTotal: 120,
|
|
3745
|
-
orderStatus: "draft",
|
|
3746
|
-
services: [{ id: "bi-3", serviceId: "svc-3", name: "Facial Treatment", durationMinutes: 60, price: 120, assigneeId: "prof-3" }],
|
|
3747
|
-
totalDurationMinutes: 60,
|
|
3748
|
-
createdAt: now(),
|
|
3749
|
-
updatedAt: now()
|
|
3750
|
-
},
|
|
3751
|
-
{
|
|
3752
|
-
id: "bk-4",
|
|
3753
|
-
tenantId: "mock",
|
|
3754
|
-
kind: "appointment",
|
|
3755
|
-
startsAt: dt(11, 0),
|
|
3756
|
-
endsAt: dt(12, 0),
|
|
3757
|
-
status: "scheduled",
|
|
3758
|
-
notes: null,
|
|
3759
|
-
orderId: "ord-4",
|
|
3760
|
-
locationId: "loc-1",
|
|
3761
|
-
metadata: {},
|
|
3762
|
-
clientId: "cl-4",
|
|
3763
|
-
clientName: "Jessica Lima",
|
|
3764
|
-
clientPhone: "(11) 99999-4444",
|
|
3765
|
-
clientEmail: null,
|
|
3766
|
-
clientAvatarUrl: null,
|
|
3767
|
-
professionalId: "prof-1",
|
|
3768
|
-
professionalName: "Ana Silva",
|
|
3769
|
-
professionalAvatarUrl: null,
|
|
3770
|
-
locationName: "Main Branch",
|
|
3771
|
-
orderTotal: 60,
|
|
3772
|
-
orderStatus: "draft",
|
|
3773
|
-
services: [{ id: "bi-4", serviceId: "svc-4", name: "Manicure", durationMinutes: 60, price: 60, assigneeId: "prof-1" }],
|
|
3774
|
-
totalDurationMinutes: 60,
|
|
3775
|
-
createdAt: now(),
|
|
3776
|
-
updatedAt: now()
|
|
3777
|
-
},
|
|
3778
|
-
{
|
|
3779
|
-
id: "bk-5",
|
|
3780
|
-
tenantId: "mock",
|
|
3781
|
-
kind: "appointment",
|
|
3782
|
-
startsAt: dt(15, 30),
|
|
3783
|
-
endsAt: dt(17, 0),
|
|
3784
|
-
status: "confirmed",
|
|
3785
|
-
notes: null,
|
|
3786
|
-
orderId: "ord-5",
|
|
3787
|
-
locationId: "loc-1",
|
|
3788
|
-
metadata: {},
|
|
3789
|
-
clientId: "cl-5",
|
|
3790
|
-
clientName: "Amanda Wright",
|
|
3791
|
-
clientPhone: "(11) 99999-5555",
|
|
3792
|
-
clientEmail: "amanda@email.com",
|
|
3793
|
-
clientAvatarUrl: null,
|
|
3794
|
-
professionalId: "prof-2",
|
|
3795
|
-
professionalName: "Carlos Santos",
|
|
3796
|
-
professionalAvatarUrl: null,
|
|
3797
|
-
locationName: "Main Branch",
|
|
3798
|
-
orderTotal: 350,
|
|
3799
|
-
orderStatus: "draft",
|
|
3800
|
-
services: [
|
|
3801
|
-
{ id: "bi-5", serviceId: "svc-1", name: "Highlights", durationMinutes: 60, price: 200, assigneeId: "prof-2" },
|
|
3802
|
-
{ id: "bi-6", serviceId: "svc-2", name: "Haircut", durationMinutes: 30, price: 150, assigneeId: "prof-2" }
|
|
3803
|
-
],
|
|
3804
|
-
totalDurationMinutes: 90,
|
|
3805
|
-
createdAt: now(),
|
|
3806
|
-
updatedAt: now()
|
|
3807
|
-
}
|
|
3808
|
-
];
|
|
3809
|
-
}
|
|
3810
|
-
function createSampleSchedules() {
|
|
3811
|
-
const schedules = [];
|
|
3812
|
-
for (const prof of SAMPLE_PROFESSIONALS) {
|
|
3813
|
-
for (let day = 1; day <= 5; day++) {
|
|
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: "08:00",
|
|
3823
|
-
endsAt: "12:00",
|
|
3824
|
-
isActive: true,
|
|
3825
|
-
metadata: {}
|
|
3826
|
-
});
|
|
3827
|
-
schedules.push({
|
|
3828
|
-
id: uid(),
|
|
3829
|
-
tenantId: "mock",
|
|
3830
|
-
kind: "working_hours",
|
|
3831
|
-
assigneeId: prof.id,
|
|
3832
|
-
locationId: prof.locationId,
|
|
3833
|
-
dayOfWeek: day,
|
|
3834
|
-
specificDate: null,
|
|
3835
|
-
startsAt: "13:00",
|
|
3836
|
-
endsAt: "18:00",
|
|
3837
|
-
isActive: true,
|
|
3838
|
-
metadata: {}
|
|
3839
|
-
});
|
|
3840
|
-
}
|
|
3841
|
-
schedules.push({
|
|
3842
|
-
id: uid(),
|
|
3843
|
-
tenantId: "mock",
|
|
3844
|
-
kind: "working_hours",
|
|
3845
|
-
assigneeId: prof.id,
|
|
3846
|
-
locationId: prof.locationId,
|
|
3847
|
-
dayOfWeek: 6,
|
|
3848
|
-
specificDate: null,
|
|
3849
|
-
startsAt: "08:00",
|
|
3850
|
-
endsAt: "14:00",
|
|
3851
|
-
isActive: true,
|
|
3852
|
-
metadata: {}
|
|
3853
|
-
});
|
|
3854
|
-
}
|
|
3855
|
-
return schedules;
|
|
3856
|
-
}
|
|
3857
|
-
function createMockAgendaProvider() {
|
|
3858
|
-
const bookings = createSampleBookings();
|
|
3859
|
-
const schedules = createSampleSchedules();
|
|
3860
|
-
const professionals = [...SAMPLE_PROFESSIONALS];
|
|
3861
|
-
return {
|
|
3862
|
-
async getLocations() {
|
|
3863
|
-
return [];
|
|
3864
|
-
},
|
|
3865
|
-
async getBookings(query) {
|
|
3866
|
-
return bookings.filter((b) => {
|
|
3867
|
-
if (query.kind && b.kind !== query.kind) return false;
|
|
3868
|
-
if (query.statuses?.length && !query.statuses.includes(b.status)) return false;
|
|
3869
|
-
if (query.professionalIds?.length && !query.professionalIds.includes(b.professionalId ?? "")) return false;
|
|
3870
|
-
if (query.locationId && b.locationId !== query.locationId) return false;
|
|
3871
|
-
if (query.dateRange) {
|
|
3872
|
-
if (b.startsAt > query.dateRange.end || (b.endsAt ?? b.startsAt) < query.dateRange.start) return false;
|
|
3873
|
-
}
|
|
3874
|
-
return true;
|
|
3875
|
-
});
|
|
3876
|
-
},
|
|
3877
|
-
async getBookingById(id) {
|
|
3878
|
-
return bookings.find((b) => b.id === id) ?? null;
|
|
3879
|
-
},
|
|
3880
|
-
async createBooking(input) {
|
|
3881
|
-
const totalDuration = input.services.reduce((sum, s) => sum + s.durationMinutes, 0);
|
|
3882
|
-
const totalPrice = input.services.reduce((sum, s) => sum + s.price, 0);
|
|
3883
|
-
const booking = {
|
|
3884
|
-
id: uid(),
|
|
3885
|
-
tenantId: "mock",
|
|
3886
|
-
kind: input.kind ?? "appointment",
|
|
3887
|
-
startsAt: input.startsAt,
|
|
3888
|
-
endsAt: addMinutes2(input.startsAt, totalDuration || 30),
|
|
3889
|
-
status: "scheduled",
|
|
3890
|
-
notes: input.notes ?? null,
|
|
3891
|
-
orderId: input.services.length > 0 ? uid() : null,
|
|
3892
|
-
locationId: input.locationId ?? null,
|
|
3893
|
-
metadata: {},
|
|
3894
|
-
clientId: input.clientId,
|
|
3895
|
-
clientName: "New Client",
|
|
3896
|
-
clientPhone: null,
|
|
3897
|
-
clientEmail: null,
|
|
3898
|
-
clientAvatarUrl: null,
|
|
3899
|
-
professionalId: input.professionalId,
|
|
3900
|
-
professionalName: professionals.find((p) => p.id === input.professionalId)?.name ?? "Unknown",
|
|
3901
|
-
professionalAvatarUrl: null,
|
|
3902
|
-
locationName: null,
|
|
3903
|
-
orderTotal: totalPrice,
|
|
3904
|
-
orderStatus: "draft",
|
|
3905
|
-
services: input.services.map((s) => ({
|
|
3906
|
-
id: uid(),
|
|
3907
|
-
serviceId: s.serviceId,
|
|
3908
|
-
name: s.name,
|
|
3909
|
-
durationMinutes: s.durationMinutes,
|
|
3910
|
-
price: s.price,
|
|
3911
|
-
assigneeId: s.assigneeId ?? input.professionalId
|
|
3912
|
-
})),
|
|
3913
|
-
totalDurationMinutes: totalDuration,
|
|
3914
|
-
createdAt: now(),
|
|
3915
|
-
updatedAt: now()
|
|
3916
|
-
};
|
|
3917
|
-
bookings.push(booking);
|
|
3918
|
-
return booking;
|
|
3919
|
-
},
|
|
3920
|
-
async updateBooking(id, data) {
|
|
3921
|
-
const idx = bookings.findIndex((b) => b.id === id);
|
|
3922
|
-
if (idx === -1) throw new Error("Booking not found");
|
|
3923
|
-
const existing = bookings[idx];
|
|
3924
|
-
const serviceUpdates = {};
|
|
3925
|
-
if (data.services) {
|
|
3926
|
-
serviceUpdates.services = data.services.map((s) => ({
|
|
3927
|
-
id: uid(),
|
|
3928
|
-
serviceId: s.serviceId,
|
|
3929
|
-
name: s.name,
|
|
3930
|
-
durationMinutes: s.durationMinutes,
|
|
3931
|
-
price: s.price,
|
|
3932
|
-
assigneeId: s.assigneeId ?? existing.professionalId ?? ""
|
|
3933
|
-
}));
|
|
3934
|
-
serviceUpdates.totalDurationMinutes = data.services.reduce((sum, s) => sum + s.durationMinutes, 0);
|
|
3935
|
-
serviceUpdates.orderTotal = data.services.reduce((sum, s) => sum + s.price, 0);
|
|
3936
|
-
}
|
|
3937
|
-
const updated = {
|
|
3938
|
-
...existing,
|
|
3939
|
-
...data.startsAt && { startsAt: data.startsAt },
|
|
3940
|
-
...data.endsAt && { endsAt: data.endsAt },
|
|
3941
|
-
...data.status && { status: data.status },
|
|
3942
|
-
...data.notes !== void 0 && { notes: data.notes ?? null },
|
|
3943
|
-
...data.professionalId && { professionalId: data.professionalId },
|
|
3944
|
-
...data.clientId && { clientId: data.clientId },
|
|
3945
|
-
...data.locationId && { locationId: data.locationId },
|
|
3946
|
-
...serviceUpdates,
|
|
3947
|
-
updatedAt: now()
|
|
3948
|
-
};
|
|
3949
|
-
bookings[idx] = updated;
|
|
3950
|
-
return updated;
|
|
3951
|
-
},
|
|
3952
|
-
async deleteBooking(id) {
|
|
3953
|
-
const idx = bookings.findIndex((b) => b.id === id);
|
|
3954
|
-
if (idx !== -1) bookings.splice(idx, 1);
|
|
3955
|
-
},
|
|
3956
|
-
async updateBookingStatus(id, status) {
|
|
3957
|
-
const idx = bookings.findIndex((b) => b.id === id);
|
|
3958
|
-
if (idx === -1) throw new Error("Booking not found");
|
|
3959
|
-
bookings[idx] = { ...bookings[idx], status, updatedAt: now() };
|
|
3960
|
-
return bookings[idx];
|
|
3961
|
-
},
|
|
3962
|
-
async checkConflict(params) {
|
|
3963
|
-
return bookings.some((b) => {
|
|
3964
|
-
if (params.excludeBookingId && b.id === params.excludeBookingId) return false;
|
|
3965
|
-
if (b.professionalId !== params.assigneeId) return false;
|
|
3966
|
-
if (b.status === "cancelled" || b.status === "no_show") return false;
|
|
3967
|
-
return timeOverlaps(b.startsAt, b.endsAt ?? b.startsAt, params.startsAt, params.endsAt);
|
|
3968
|
-
});
|
|
3969
|
-
},
|
|
3970
|
-
async getAvailableSlots(params) {
|
|
3971
|
-
const date = new Date(params.date);
|
|
3972
|
-
const dayOfWeek = date.getDay();
|
|
3973
|
-
const interval = params.slotInterval ?? 30;
|
|
3974
|
-
const daySchedules = schedules.filter(
|
|
3975
|
-
(s) => s.assigneeId === params.assigneeId && s.isActive && (s.dayOfWeek === dayOfWeek || s.specificDate === params.date)
|
|
3976
|
-
);
|
|
3977
|
-
const slots = [];
|
|
3978
|
-
for (const sched of daySchedules) {
|
|
3979
|
-
const [startH, startM] = sched.startsAt.split(":").map(Number);
|
|
3980
|
-
const [endH, endM] = sched.endsAt.split(":").map(Number);
|
|
3981
|
-
const schedStart = new Date(date);
|
|
3982
|
-
schedStart.setHours(startH, startM, 0, 0);
|
|
3983
|
-
const schedEnd = new Date(date);
|
|
3984
|
-
schedEnd.setHours(endH, endM, 0, 0);
|
|
3985
|
-
let slotStart = new Date(schedStart);
|
|
3986
|
-
while (true) {
|
|
3987
|
-
const slotEnd = new Date(slotStart.getTime() + params.durationMinutes * 6e4);
|
|
3988
|
-
if (slotEnd > schedEnd) break;
|
|
3989
|
-
const startIso = slotStart.toISOString();
|
|
3990
|
-
const endIso = slotEnd.toISOString();
|
|
3991
|
-
const hasConflict = bookings.some((b) => {
|
|
3992
|
-
if (b.professionalId !== params.assigneeId) return false;
|
|
3993
|
-
if (b.status === "cancelled" || b.status === "no_show") return false;
|
|
3994
|
-
return timeOverlaps(b.startsAt, b.endsAt ?? b.startsAt, startIso, endIso);
|
|
3995
|
-
});
|
|
3996
|
-
if (!hasConflict) {
|
|
3997
|
-
slots.push({ start: startIso, end: endIso });
|
|
3998
|
-
}
|
|
3999
|
-
slotStart = new Date(slotStart.getTime() + interval * 6e4);
|
|
4000
|
-
}
|
|
4001
|
-
}
|
|
4002
|
-
return slots;
|
|
4003
|
-
},
|
|
4004
|
-
async getSchedules(professionalId) {
|
|
4005
|
-
if (professionalId) return schedules.filter((s) => s.assigneeId === professionalId);
|
|
4006
|
-
return [...schedules];
|
|
4007
|
-
},
|
|
4008
|
-
async saveSchedule(input) {
|
|
4009
|
-
const schedule = {
|
|
4010
|
-
id: uid(),
|
|
4011
|
-
tenantId: "mock",
|
|
4012
|
-
kind: "working_hours",
|
|
4013
|
-
assigneeId: input.assigneeId,
|
|
4014
|
-
locationId: input.locationId ?? null,
|
|
4015
|
-
dayOfWeek: input.dayOfWeek ?? null,
|
|
4016
|
-
specificDate: input.specificDate ?? null,
|
|
4017
|
-
startsAt: input.startsAt,
|
|
4018
|
-
endsAt: input.endsAt,
|
|
4019
|
-
isActive: input.isActive !== false,
|
|
4020
|
-
metadata: input.metadata ?? {}
|
|
4021
|
-
};
|
|
4022
|
-
schedules.push(schedule);
|
|
4023
|
-
return schedule;
|
|
4024
|
-
},
|
|
4025
|
-
async deleteSchedule(id) {
|
|
4026
|
-
const idx = schedules.findIndex((s) => s.id === id);
|
|
4027
|
-
if (idx !== -1) schedules.splice(idx, 1);
|
|
4028
|
-
},
|
|
4029
|
-
async getProfessionals() {
|
|
4030
|
-
return [...professionals];
|
|
4031
|
-
},
|
|
4032
|
-
async getConfirmationsPending(daysAhead = 2) {
|
|
4033
|
-
const cutoff = /* @__PURE__ */ new Date();
|
|
4034
|
-
cutoff.setDate(cutoff.getDate() + daysAhead);
|
|
4035
|
-
return bookings.filter(
|
|
4036
|
-
(b) => b.status === "scheduled" && new Date(b.startsAt) <= cutoff && new Date(b.startsAt) >= /* @__PURE__ */ new Date()
|
|
4037
|
-
);
|
|
4038
|
-
},
|
|
4039
|
-
async sendConfirmation(bookingId, channel) {
|
|
4040
|
-
const booking = bookings.find((b) => b.id === bookingId);
|
|
4041
|
-
if (!booking) throw new Error("Booking not found");
|
|
4042
|
-
const confirmations = booking.metadata.confirmations ?? [];
|
|
4043
|
-
confirmations.push({ channel, sentAt: now(), status: "sent" });
|
|
4044
|
-
booking.metadata = { ...booking.metadata, confirmations };
|
|
4045
|
-
},
|
|
4046
|
-
async completeBooking(bookingId) {
|
|
4047
|
-
const idx = bookings.findIndex((b) => b.id === bookingId);
|
|
4048
|
-
if (idx === -1) throw new Error("Booking not found");
|
|
4049
|
-
bookings[idx] = { ...bookings[idx], status: "completed", updatedAt: now() };
|
|
4050
|
-
return { booking: bookings[idx], orderId: bookings[idx].orderId ?? "" };
|
|
4051
|
-
}
|
|
4052
|
-
};
|
|
4053
|
-
}
|
|
4054
3476
|
function getCurrentBookingQuery(state, dateRange = state.dateRange) {
|
|
4055
3477
|
return {
|
|
4056
3478
|
dateRange,
|
|
@@ -4246,7 +3668,7 @@ function createAgendaStore(provider2, financialBridge) {
|
|
|
4246
3668
|
set({ selectedDate: date });
|
|
4247
3669
|
},
|
|
4248
3670
|
openAppointmentModal(mode, options) {
|
|
4249
|
-
set({ appointmentModal: { open: true, mode, bookingId: options?.bookingId, prefill: options?.prefill, initialTab: options?.initialTab } });
|
|
3671
|
+
set({ appointmentModal: { open: true, mode, bookingId: options?.bookingId, prefill: options?.prefill, initialTab: options?.initialTab, externalSource: options?.externalSource } });
|
|
4250
3672
|
},
|
|
4251
3673
|
closeAppointmentModal() {
|
|
4252
3674
|
set((state) => ({ appointmentModal: { ...state.appointmentModal, open: false } }));
|
|
@@ -4437,8 +3859,12 @@ var en = {
|
|
|
4437
3859
|
"agenda.calendar.today": "Today",
|
|
4438
3860
|
"agenda.calendar.viewAgenda": "Agenda",
|
|
4439
3861
|
"agenda.calendar.viewDay": "Day",
|
|
3862
|
+
"agenda.calendar.viewList": "List",
|
|
4440
3863
|
"agenda.calendar.viewMonth": "Month",
|
|
4441
3864
|
"agenda.calendar.viewWeek": "Week",
|
|
3865
|
+
"agenda.list.today": "Today",
|
|
3866
|
+
"agenda.list.tomorrow": "Tomorrow",
|
|
3867
|
+
"agenda.list.empty": "No upcoming items",
|
|
4442
3868
|
"agenda.confirmations.confirmed": "Confirmed:",
|
|
4443
3869
|
"agenda.confirmations.empty": "No pending confirmations",
|
|
4444
3870
|
"agenda.confirmations.emptySubtitle": "All upcoming appointments have been confirmed",
|
|
@@ -4449,6 +3875,10 @@ var en = {
|
|
|
4449
3875
|
"agenda.confirmations.call": "Call",
|
|
4450
3876
|
"agenda.confirmations.markConfirmed": "Mark Confirmed",
|
|
4451
3877
|
"agenda.confirmations.markCancelled": "Mark Cancelled",
|
|
3878
|
+
"agenda.cancellation.loadingReasons": "Loading reasons...",
|
|
3879
|
+
"agenda.cancellation.notesOptional": "Cancellation notes (optional)",
|
|
3880
|
+
"agenda.cancellation.notesRequired": "Cancellation notes required",
|
|
3881
|
+
"agenda.cancellation.selectReason": "Select cancellation reason",
|
|
4452
3882
|
"agenda.contextMenu.changeStatus": "Change status",
|
|
4453
3883
|
"agenda.contextMenu.delete": "Delete",
|
|
4454
3884
|
"agenda.contextMenu.edit": "Edit",
|
|
@@ -4549,7 +3979,11 @@ var en = {
|
|
|
4549
3979
|
"agenda.appointment.addNotes": "Add notes",
|
|
4550
3980
|
"agenda.appointment.duration": "Duration (minutes)",
|
|
4551
3981
|
"agenda.appointment.price": "Price",
|
|
3982
|
+
"agenda.appointment.title": "Title",
|
|
3983
|
+
"agenda.appointment.addTitle": "Add a title",
|
|
3984
|
+
"agenda.appointment.untitled": "(no title)",
|
|
4552
3985
|
"agenda.bookingType.appointment": "Appointment",
|
|
3986
|
+
"agenda.bookingType.event": "Event",
|
|
4553
3987
|
"agenda.bookingType.task": "Task",
|
|
4554
3988
|
"agenda.bookingType.out_of_office": "Out of Office",
|
|
4555
3989
|
"agenda.bookingType.block": "Block",
|
|
@@ -4645,8 +4079,12 @@ var ptBR = {
|
|
|
4645
4079
|
"agenda.calendar.today": "Hoje",
|
|
4646
4080
|
"agenda.calendar.viewAgenda": "Agenda",
|
|
4647
4081
|
"agenda.calendar.viewDay": "Dia",
|
|
4082
|
+
"agenda.calendar.viewList": "Lista",
|
|
4648
4083
|
"agenda.calendar.viewMonth": "M\xEAs",
|
|
4649
4084
|
"agenda.calendar.viewWeek": "Semana",
|
|
4085
|
+
"agenda.list.today": "Hoje",
|
|
4086
|
+
"agenda.list.tomorrow": "Amanh\xE3",
|
|
4087
|
+
"agenda.list.empty": "Nenhum item pr\xF3ximo",
|
|
4650
4088
|
"agenda.confirmations.confirmed": "Confirmado:",
|
|
4651
4089
|
"agenda.confirmations.empty": "Nenhuma confirma\xE7\xE3o pendente",
|
|
4652
4090
|
"agenda.confirmations.emptySubtitle": "Todos os agendamentos futuros foram confirmados",
|
|
@@ -4657,6 +4095,10 @@ var ptBR = {
|
|
|
4657
4095
|
"agenda.confirmations.call": "Ligar",
|
|
4658
4096
|
"agenda.confirmations.markConfirmed": "Marcar Confirmado",
|
|
4659
4097
|
"agenda.confirmations.markCancelled": "Marcar Cancelado",
|
|
4098
|
+
"agenda.cancellation.loadingReasons": "Carregando motivos...",
|
|
4099
|
+
"agenda.cancellation.notesOptional": "Observa\xE7\xF5es do cancelamento (opcional)",
|
|
4100
|
+
"agenda.cancellation.notesRequired": "Observa\xE7\xF5es do cancelamento obrigat\xF3rias",
|
|
4101
|
+
"agenda.cancellation.selectReason": "Selecionar motivo do cancelamento",
|
|
4660
4102
|
"agenda.contextMenu.changeStatus": "Alterar status",
|
|
4661
4103
|
"agenda.contextMenu.delete": "Excluir",
|
|
4662
4104
|
"agenda.contextMenu.edit": "Editar",
|
|
@@ -4757,7 +4199,11 @@ var ptBR = {
|
|
|
4757
4199
|
"agenda.appointment.addNotes": "Adicionar observa\xE7\xF5es",
|
|
4758
4200
|
"agenda.appointment.duration": "Dura\xE7\xE3o (minutos)",
|
|
4759
4201
|
"agenda.appointment.price": "Pre\xE7o",
|
|
4202
|
+
"agenda.appointment.title": "T\xEDtulo",
|
|
4203
|
+
"agenda.appointment.addTitle": "Adicionar um t\xEDtulo",
|
|
4204
|
+
"agenda.appointment.untitled": "(sem t\xEDtulo)",
|
|
4760
4205
|
"agenda.bookingType.appointment": "Agendamento",
|
|
4206
|
+
"agenda.bookingType.event": "Evento",
|
|
4761
4207
|
"agenda.bookingType.task": "Tarefa",
|
|
4762
4208
|
"agenda.bookingType.out_of_office": "Aus\xEAncia",
|
|
4763
4209
|
"agenda.bookingType.block": "Bloqueio",
|
|
@@ -5039,11 +4485,11 @@ function createGoogleCalendarProvider() {
|
|
|
5039
4485
|
}
|
|
5040
4486
|
var provider = createGoogleCalendarProvider();
|
|
5041
4487
|
function GoogleCalendarExtraPanel() {
|
|
5042
|
-
const [connected, setConnected] =
|
|
5043
|
-
const [calendarId, setCalendarId] =
|
|
5044
|
-
const [syncing, setSyncing] =
|
|
5045
|
-
const [log, setLog] =
|
|
5046
|
-
const [loaded, setLoaded] =
|
|
4488
|
+
const [connected, setConnected] = React12.useState(false);
|
|
4489
|
+
const [calendarId, setCalendarId] = React12.useState("primary");
|
|
4490
|
+
const [syncing, setSyncing] = React12.useState(false);
|
|
4491
|
+
const [log, setLog] = React12.useState([]);
|
|
4492
|
+
const [loaded, setLoaded] = React12.useState(false);
|
|
5047
4493
|
async function load() {
|
|
5048
4494
|
const integ = await provider.getIntegration();
|
|
5049
4495
|
setConnected(!!integ?.connected);
|
|
@@ -5053,7 +4499,7 @@ function GoogleCalendarExtraPanel() {
|
|
|
5053
4499
|
}
|
|
5054
4500
|
setLoaded(true);
|
|
5055
4501
|
}
|
|
5056
|
-
|
|
4502
|
+
React12.useEffect(() => {
|
|
5057
4503
|
void load();
|
|
5058
4504
|
}, []);
|
|
5059
4505
|
if (!loaded || !connected) return null;
|
|
@@ -5189,10 +4635,11 @@ function createAgendaPlugin(options) {
|
|
|
5189
4635
|
const config = resolveConfig(options);
|
|
5190
4636
|
core.registerTranslations(agendaLocales);
|
|
5191
4637
|
const provider2 = options?.dataProvider ?? core.createSafeDataProvider(
|
|
5192
|
-
() => createSupabaseAgendaProvider(),
|
|
5193
|
-
() => createMockAgendaProvider()
|
|
4638
|
+
() => chunkGAJVIEXM_cjs.createSupabaseAgendaProvider(),
|
|
4639
|
+
() => chunkGAJVIEXM_cjs.createMockAgendaProvider()
|
|
5194
4640
|
);
|
|
5195
4641
|
const store = createAgendaStore(provider2, options?.financialBridge);
|
|
4642
|
+
const registries = [...agendaRegistries, ...options?.settingsRegistries ?? []];
|
|
5196
4643
|
saas.setScheduleBlockConfig({
|
|
5197
4644
|
defaults: config.scheduleBlockDefaults,
|
|
5198
4645
|
showServices: !!config.serviceLookup,
|
|
@@ -5208,7 +4655,7 @@ function createAgendaPlugin(options) {
|
|
|
5208
4655
|
return locations;
|
|
5209
4656
|
} : void 0
|
|
5210
4657
|
});
|
|
5211
|
-
const PageComponent = () =>
|
|
4658
|
+
const PageComponent = () => React12__default.default.createElement(AgendaPage, { config, provider: provider2, store });
|
|
5212
4659
|
return {
|
|
5213
4660
|
id: "agenda",
|
|
5214
4661
|
name: config.labels.pageTitle,
|
|
@@ -5216,6 +4663,9 @@ function createAgendaPlugin(options) {
|
|
|
5216
4663
|
version: "1.0.0",
|
|
5217
4664
|
scope: options?.scope ?? "universal",
|
|
5218
4665
|
verticalId: options?.verticalId,
|
|
4666
|
+
// Booking/scheduling is business-ops (clinic, salon, services) — targets
|
|
4667
|
+
// the saas/admin Panel world, not the ecommerce storefront.
|
|
4668
|
+
scaffolds: ["saas"],
|
|
5219
4669
|
defaultEnabled: true,
|
|
5220
4670
|
dependencies: [],
|
|
5221
4671
|
declaredFeatures: [
|
|
@@ -5308,18 +4758,18 @@ function createAgendaPlugin(options) {
|
|
|
5308
4758
|
permission: { feature: "appointments", action: "read" }
|
|
5309
4759
|
}
|
|
5310
4760
|
],
|
|
5311
|
-
registries
|
|
4761
|
+
registries,
|
|
5312
4762
|
settings: [
|
|
5313
4763
|
{
|
|
5314
4764
|
id: "agenda",
|
|
5315
4765
|
label: "Agenda",
|
|
5316
4766
|
icon: "Calendar",
|
|
5317
|
-
// Standard layout: General | Integrations (Google Calendar addon) | Properties
|
|
4767
|
+
// Standard layout: General | Integrations (Google Calendar addon) | Properties.
|
|
5318
4768
|
component: (() => {
|
|
5319
|
-
const AgendaSettingsTab = () =>
|
|
4769
|
+
const AgendaSettingsTab = () => React12__default.default.createElement(saas.PluginSettingsPanel, {
|
|
5320
4770
|
title: "Agenda",
|
|
5321
|
-
generalSettings:
|
|
5322
|
-
registries
|
|
4771
|
+
generalSettings: React12__default.default.createElement(AgendaGeneralSettings),
|
|
4772
|
+
registries,
|
|
5323
4773
|
hostPluginId: "agenda",
|
|
5324
4774
|
routeBase: "/settings/agenda"
|
|
5325
4775
|
});
|
|
@@ -5334,6 +4784,10 @@ function createAgendaPlugin(options) {
|
|
|
5334
4784
|
};
|
|
5335
4785
|
}
|
|
5336
4786
|
|
|
4787
|
+
Object.defineProperty(exports, "createMockAgendaProvider", {
|
|
4788
|
+
enumerable: true,
|
|
4789
|
+
get: function () { return chunkGAJVIEXM_cjs.createMockAgendaProvider; }
|
|
4790
|
+
});
|
|
5337
4791
|
exports.bookingToEvent = bookingToEvent2;
|
|
5338
4792
|
exports.computeCommissionAmount = computeCommissionAmount;
|
|
5339
4793
|
exports.createAgendaPlugin = createAgendaPlugin;
|