@marco.colia/react-fast-scheduler 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -22
- package/README.md +279 -251
- package/dist/index.cjs +724 -379
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +724 -379
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +86 -86
package/dist/index.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var React = require('react');
|
|
4
|
-
var dateFns = require('date-fns');
|
|
5
3
|
var jsxRuntime = require('react/jsx-runtime');
|
|
4
|
+
var dateFns = require('date-fns');
|
|
5
|
+
var React5 = require('react');
|
|
6
6
|
|
|
7
7
|
function _interopNamespace(e) {
|
|
8
8
|
if (e && e.__esModule) return e;
|
|
@@ -22,9 +22,238 @@ function _interopNamespace(e) {
|
|
|
22
22
|
return Object.freeze(n);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
var
|
|
25
|
+
var React5__namespace = /*#__PURE__*/_interopNamespace(React5);
|
|
26
26
|
|
|
27
|
-
// src/components/
|
|
27
|
+
// src/components/defaultRenderAppointment/index.tsx
|
|
28
|
+
function defaultRenderAppointment({
|
|
29
|
+
appointment,
|
|
30
|
+
onPointerDown,
|
|
31
|
+
onResizePointerDown,
|
|
32
|
+
appointmentAppearance,
|
|
33
|
+
appointmentBackgroundColor
|
|
34
|
+
}) {
|
|
35
|
+
const rawMaybe = appointment.raw;
|
|
36
|
+
const description = typeof rawMaybe.description === "string" ? rawMaybe.description : void 0;
|
|
37
|
+
const legacyClassName = typeof appointmentBackgroundColor === "string" ? appointmentBackgroundColor : void 0;
|
|
38
|
+
const backgroundClassName = appointmentAppearance?.className ?? legacyClassName ?? "bg-muted/40 dark:bg-muted/30";
|
|
39
|
+
const className = `relative h-full cursor-grab overflow-hidden rounded-md border border-border p-2 pb-5 text-foreground ${backgroundClassName}`;
|
|
40
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { onPointerDown, className, children: [
|
|
41
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs font-semibold", children: appointment.title }),
|
|
42
|
+
description ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-1 text-[10px] font-medium text-muted-foreground", children: description }) : null,
|
|
43
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
44
|
+
"div",
|
|
45
|
+
{
|
|
46
|
+
role: "button",
|
|
47
|
+
"aria-label": "Resize appointment",
|
|
48
|
+
onPointerDown: (event) => {
|
|
49
|
+
event.stopPropagation();
|
|
50
|
+
onResizePointerDown(event);
|
|
51
|
+
},
|
|
52
|
+
className: "absolute inset-x-1 bottom-1 h-2 cursor-ns-resize rounded-full bg-border/90 transition-colors hover:bg-border"
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
] });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/lib/cn.ts
|
|
59
|
+
function cn(...inputs) {
|
|
60
|
+
return inputs.filter(Boolean).join(" ");
|
|
61
|
+
}
|
|
62
|
+
var variantClasses = {
|
|
63
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
64
|
+
outline: "border border-border bg-card text-foreground hover:bg-muted",
|
|
65
|
+
ghost: "text-foreground hover:bg-muted"
|
|
66
|
+
};
|
|
67
|
+
var sizeClasses = {
|
|
68
|
+
default: "h-9 px-4 py-2",
|
|
69
|
+
sm: "h-8 px-3 text-xs",
|
|
70
|
+
icon: "h-8 w-8"
|
|
71
|
+
};
|
|
72
|
+
var Button = React5__namespace.forwardRef(function Button2({ className, variant = "default", size = "default", type = "button", ...props }, ref) {
|
|
73
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
74
|
+
"button",
|
|
75
|
+
{
|
|
76
|
+
ref,
|
|
77
|
+
type,
|
|
78
|
+
className: cn(
|
|
79
|
+
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
80
|
+
variantClasses[variant],
|
|
81
|
+
sizeClasses[size],
|
|
82
|
+
className
|
|
83
|
+
),
|
|
84
|
+
...props
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
function Calendar({
|
|
89
|
+
selected,
|
|
90
|
+
onSelect
|
|
91
|
+
}) {
|
|
92
|
+
const [month, setMonth] = React5__namespace.useState(selected ?? /* @__PURE__ */ new Date());
|
|
93
|
+
React5__namespace.useEffect(() => {
|
|
94
|
+
if (selected) {
|
|
95
|
+
setMonth(selected);
|
|
96
|
+
}
|
|
97
|
+
}, [selected]);
|
|
98
|
+
const monthStart = dateFns.startOfMonth(month);
|
|
99
|
+
const calendarStart = dateFns.startOfWeek(monthStart, { weekStartsOn: 1 });
|
|
100
|
+
const calendarEnd = dateFns.endOfWeek(dateFns.endOfMonth(month), { weekStartsOn: 1 });
|
|
101
|
+
const days = [];
|
|
102
|
+
for (let day = calendarStart; day <= calendarEnd; day = dateFns.addDays(day, 1)) {
|
|
103
|
+
days.push(day);
|
|
104
|
+
}
|
|
105
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-72 rounded-md bg-popover", children: [
|
|
106
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-2 flex items-center justify-between", children: [
|
|
107
|
+
/* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "ghost", size: "icon", "aria-label": "Previous month", onClick: () => setMonth(dateFns.addMonths(month, -1)), children: "<" }),
|
|
108
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-medium", children: dateFns.format(month, "MMMM yyyy") }),
|
|
109
|
+
/* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "ghost", size: "icon", "aria-label": "Next month", onClick: () => setMonth(dateFns.addMonths(month, 1)), children: ">" })
|
|
110
|
+
] }),
|
|
111
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-7 gap-1 text-center text-xs text-muted-foreground", children: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"].map((label) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "py-1", children: label }, label)) }),
|
|
112
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-7 gap-1", children: days.map((day) => {
|
|
113
|
+
const isSelected = selected ? dateFns.isSameDay(day, selected) : false;
|
|
114
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
115
|
+
Button,
|
|
116
|
+
{
|
|
117
|
+
variant: isSelected ? "default" : "ghost",
|
|
118
|
+
size: "icon",
|
|
119
|
+
className: cn(
|
|
120
|
+
"text-xs",
|
|
121
|
+
!dateFns.isSameMonth(day, month) && "text-muted-foreground opacity-60",
|
|
122
|
+
dateFns.isToday(day) && !isSelected && "border border-border"
|
|
123
|
+
),
|
|
124
|
+
onClick: () => onSelect?.(day),
|
|
125
|
+
children: dateFns.format(day, "d")
|
|
126
|
+
},
|
|
127
|
+
day.toISOString()
|
|
128
|
+
);
|
|
129
|
+
}) })
|
|
130
|
+
] });
|
|
131
|
+
}
|
|
132
|
+
var PopoverContext = React5__namespace.createContext(null);
|
|
133
|
+
function usePopoverContext() {
|
|
134
|
+
const context = React5__namespace.useContext(PopoverContext);
|
|
135
|
+
if (!context) {
|
|
136
|
+
throw new Error("Popover components must be used within Popover.");
|
|
137
|
+
}
|
|
138
|
+
return context;
|
|
139
|
+
}
|
|
140
|
+
function Popover({
|
|
141
|
+
children,
|
|
142
|
+
open: controlledOpen,
|
|
143
|
+
onOpenChange
|
|
144
|
+
}) {
|
|
145
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React5__namespace.useState(false);
|
|
146
|
+
const triggerRef = React5__namespace.useRef(null);
|
|
147
|
+
const contentRef = React5__namespace.useRef(null);
|
|
148
|
+
const open = controlledOpen ?? uncontrolledOpen;
|
|
149
|
+
const setOpen = React5__namespace.useCallback(
|
|
150
|
+
(value) => {
|
|
151
|
+
const nextValue = typeof value === "function" ? value(open) : value;
|
|
152
|
+
if (controlledOpen === void 0) {
|
|
153
|
+
setUncontrolledOpen(nextValue);
|
|
154
|
+
}
|
|
155
|
+
onOpenChange?.(nextValue);
|
|
156
|
+
},
|
|
157
|
+
[controlledOpen, onOpenChange, open]
|
|
158
|
+
);
|
|
159
|
+
React5__namespace.useEffect(() => {
|
|
160
|
+
if (!open) {
|
|
161
|
+
return void 0;
|
|
162
|
+
}
|
|
163
|
+
const handlePointerDown = (event) => {
|
|
164
|
+
const target = event.target;
|
|
165
|
+
if (triggerRef.current?.contains(target) || contentRef.current?.contains(target)) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
setOpen(false);
|
|
169
|
+
};
|
|
170
|
+
const handleKeyDown = (event) => {
|
|
171
|
+
if (event.key === "Escape") {
|
|
172
|
+
setOpen(false);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
document.addEventListener("mousedown", handlePointerDown);
|
|
176
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
177
|
+
return () => {
|
|
178
|
+
document.removeEventListener("mousedown", handlePointerDown);
|
|
179
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
180
|
+
};
|
|
181
|
+
}, [open, setOpen]);
|
|
182
|
+
return /* @__PURE__ */ jsxRuntime.jsx(PopoverContext.Provider, { value: { open, setOpen, triggerRef, contentRef }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative inline-flex", children }) });
|
|
183
|
+
}
|
|
184
|
+
function PopoverTrigger({
|
|
185
|
+
children,
|
|
186
|
+
asChild = false
|
|
187
|
+
}) {
|
|
188
|
+
const { open, setOpen, triggerRef } = usePopoverContext();
|
|
189
|
+
if (asChild && React5__namespace.isValidElement(children)) {
|
|
190
|
+
const child = children;
|
|
191
|
+
return React5__namespace.cloneElement(child, {
|
|
192
|
+
ref: triggerRef,
|
|
193
|
+
"aria-expanded": open,
|
|
194
|
+
onClick: (event) => {
|
|
195
|
+
child.props.onClick?.(event);
|
|
196
|
+
setOpen((current) => !current);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
201
|
+
"button",
|
|
202
|
+
{
|
|
203
|
+
ref: triggerRef,
|
|
204
|
+
type: "button",
|
|
205
|
+
"aria-expanded": open,
|
|
206
|
+
onClick: () => setOpen((current) => !current),
|
|
207
|
+
children
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
function PopoverContent({
|
|
212
|
+
children,
|
|
213
|
+
className,
|
|
214
|
+
align = "start"
|
|
215
|
+
}) {
|
|
216
|
+
const { open, contentRef } = usePopoverContext();
|
|
217
|
+
if (!open) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
221
|
+
"div",
|
|
222
|
+
{
|
|
223
|
+
ref: contentRef,
|
|
224
|
+
className: cn(
|
|
225
|
+
"absolute top-full z-50 mt-2 min-w-72 rounded-md border border-border bg-popover p-2 text-popover-foreground shadow-md outline-none",
|
|
226
|
+
align === "start" && "left-0",
|
|
227
|
+
align === "center" && "left-1/2 -translate-x-1/2",
|
|
228
|
+
align === "end" && "right-0",
|
|
229
|
+
className
|
|
230
|
+
),
|
|
231
|
+
children
|
|
232
|
+
}
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
function defaultRenderDatePicker({ selectedDate, onSelectedDateChange }) {
|
|
236
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(Popover, { children: [
|
|
237
|
+
/* @__PURE__ */ jsxRuntime.jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "outline", size: "sm", className: "min-w-40 justify-start text-left font-normal", children: dateFns.format(selectedDate, "PPP") }) }),
|
|
238
|
+
/* @__PURE__ */ jsxRuntime.jsx(PopoverContent, { className: "w-auto p-2", align: "center", children: /* @__PURE__ */ jsxRuntime.jsx(Calendar, { selected: selectedDate, onSelect: onSelectedDateChange }) })
|
|
239
|
+
] });
|
|
240
|
+
}
|
|
241
|
+
function defaultRenderResourceHeader({ resource }) {
|
|
242
|
+
return /* @__PURE__ */ jsxRuntime.jsx("strong", { children: resource.label });
|
|
243
|
+
}
|
|
244
|
+
function defaultRenderToolbar({
|
|
245
|
+
goToPreviousDay,
|
|
246
|
+
goToNextDay,
|
|
247
|
+
defaultDatePicker,
|
|
248
|
+
prevButtonLabel = "Prev",
|
|
249
|
+
nextButtonLabel = "Next"
|
|
250
|
+
}) {
|
|
251
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-toolbar", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 rounded-lg border border-border bg-card p-1 shadow-xs", children: [
|
|
252
|
+
/* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "outline", size: "sm", onClick: goToPreviousDay, children: prevButtonLabel }),
|
|
253
|
+
defaultDatePicker,
|
|
254
|
+
/* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "outline", size: "sm", onClick: goToNextDay, children: nextButtonLabel })
|
|
255
|
+
] }) });
|
|
256
|
+
}
|
|
28
257
|
|
|
29
258
|
// src/components/constants.ts
|
|
30
259
|
var STEP_MIN = 15;
|
|
@@ -34,26 +263,57 @@ var GUTTER_W = 72;
|
|
|
34
263
|
var TOP_PAD = 12;
|
|
35
264
|
var BOTTOM_PAD = 24;
|
|
36
265
|
var RESOURCE_MIN_W = 420;
|
|
37
|
-
|
|
38
|
-
|
|
266
|
+
function isNameLike(resource) {
|
|
267
|
+
if (!resource || typeof resource !== "object") {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
const maybe = resource;
|
|
271
|
+
return typeof maybe.firstName === "string" || typeof maybe.lastName === "string";
|
|
272
|
+
}
|
|
273
|
+
var DEFAULT_APPOINTMENT_COLOR_TOKEN_CLASS_MAP = {
|
|
274
|
+
slate: "bg-slate-100 dark:bg-slate-900/40",
|
|
275
|
+
gray: "bg-gray-100 dark:bg-gray-900/40",
|
|
276
|
+
zinc: "bg-zinc-100 dark:bg-zinc-900/40",
|
|
277
|
+
neutral: "bg-neutral-100 dark:bg-neutral-900/40",
|
|
278
|
+
stone: "bg-stone-100 dark:bg-stone-900/40",
|
|
279
|
+
red: "bg-red-100 dark:bg-red-950/40",
|
|
280
|
+
orange: "bg-orange-100 dark:bg-orange-950/40",
|
|
281
|
+
amber: "bg-amber-100 dark:bg-amber-950/40",
|
|
282
|
+
yellow: "bg-yellow-100 dark:bg-yellow-950/40",
|
|
283
|
+
lime: "bg-lime-100 dark:bg-lime-950/40",
|
|
284
|
+
green: "bg-green-100 dark:bg-green-950/40",
|
|
285
|
+
emerald: "bg-emerald-100 dark:bg-emerald-950/40",
|
|
286
|
+
teal: "bg-teal-100 dark:bg-teal-950/40",
|
|
287
|
+
cyan: "bg-cyan-100 dark:bg-cyan-950/40",
|
|
288
|
+
sky: "bg-sky-100 dark:bg-sky-950/40",
|
|
289
|
+
blue: "bg-blue-100 dark:bg-blue-950/40",
|
|
290
|
+
indigo: "bg-indigo-100 dark:bg-indigo-950/40",
|
|
291
|
+
violet: "bg-violet-100 dark:bg-violet-950/40",
|
|
292
|
+
purple: "bg-purple-100 dark:bg-purple-950/40",
|
|
293
|
+
fuchsia: "bg-fuchsia-100 dark:bg-fuchsia-950/40",
|
|
294
|
+
pink: "bg-pink-100 dark:bg-pink-950/40",
|
|
295
|
+
rose: "bg-rose-100 dark:bg-rose-950/40"
|
|
39
296
|
};
|
|
40
|
-
|
|
297
|
+
function clamp(n, min, max) {
|
|
298
|
+
return Math.max(min, Math.min(max, n));
|
|
299
|
+
}
|
|
300
|
+
function snap(mins, stepMin = STEP_MIN) {
|
|
41
301
|
return Math.round(mins / stepMin) * stepMin;
|
|
42
|
-
}
|
|
43
|
-
|
|
302
|
+
}
|
|
303
|
+
function parseHHMM(hhmm) {
|
|
44
304
|
const [h, m] = hhmm.split(":").map(Number);
|
|
45
305
|
return h * 60 + m;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return `${
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return dateFns.format(
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return new Date(
|
|
55
|
-
}
|
|
56
|
-
|
|
306
|
+
}
|
|
307
|
+
function fullName(resource) {
|
|
308
|
+
return `${resource.firstName ?? ""} ${resource.lastName ?? ""}`.trim();
|
|
309
|
+
}
|
|
310
|
+
function dayISO(date) {
|
|
311
|
+
return dateFns.format(date, "yyyy-MM-dd");
|
|
312
|
+
}
|
|
313
|
+
function shiftDays(date, delta) {
|
|
314
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate() + delta);
|
|
315
|
+
}
|
|
316
|
+
function dateAtMinute(selectedDate, dayStartAbs, minuteFromStart) {
|
|
57
317
|
const base = new Date(
|
|
58
318
|
selectedDate.getFullYear(),
|
|
59
319
|
selectedDate.getMonth(),
|
|
@@ -68,311 +328,294 @@ var dateAtMinute = (selectedDate, dayStartAbs, minuteFromStart) => {
|
|
|
68
328
|
const mm = abs % 60;
|
|
69
329
|
base.setHours(hh, mm, 0, 0);
|
|
70
330
|
return base;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return
|
|
74
|
-
}
|
|
75
|
-
|
|
331
|
+
}
|
|
332
|
+
function minutesFromDayStart(dayStartAbs, date) {
|
|
333
|
+
return date.getHours() * 60 + date.getMinutes() - dayStartAbs;
|
|
334
|
+
}
|
|
335
|
+
function overlaps(a, b) {
|
|
76
336
|
return a.startMin < b.endMin && b.startMin < a.endMin;
|
|
77
|
-
}
|
|
78
|
-
|
|
337
|
+
}
|
|
338
|
+
function layoutOverlaps(blocks) {
|
|
79
339
|
const sorted = [...blocks].sort((a, b) => a.startMin - b.startMin || a.endMin - b.endMin);
|
|
80
340
|
const groups = [];
|
|
81
341
|
let active = [];
|
|
82
342
|
let current = [];
|
|
83
|
-
for (const
|
|
84
|
-
active = active.filter((
|
|
85
|
-
const conflicts = active.some((
|
|
343
|
+
for (const block of sorted) {
|
|
344
|
+
active = active.filter((item) => item.endMin > block.startMin);
|
|
345
|
+
const conflicts = active.some((item) => overlaps(item, block));
|
|
86
346
|
if (!conflicts && current.length > 0) {
|
|
87
347
|
groups.push(current);
|
|
88
348
|
current = [];
|
|
89
349
|
}
|
|
90
|
-
current.push(
|
|
91
|
-
active.push(
|
|
350
|
+
current.push(block);
|
|
351
|
+
active.push(block);
|
|
92
352
|
}
|
|
93
353
|
if (current.length) {
|
|
94
354
|
groups.push(current);
|
|
95
355
|
}
|
|
96
|
-
const
|
|
356
|
+
const output = [];
|
|
97
357
|
for (const group of groups) {
|
|
98
|
-
const
|
|
358
|
+
const sortedGroup = [...group].sort((a, b) => a.startMin - b.startMin || a.endMin - b.endMin);
|
|
99
359
|
const laneEnd = [];
|
|
100
360
|
const laneOf = /* @__PURE__ */ new Map();
|
|
101
|
-
for (const
|
|
361
|
+
for (const block of sortedGroup) {
|
|
102
362
|
let placed = false;
|
|
103
363
|
for (let i = 0; i < laneEnd.length; i++) {
|
|
104
|
-
if (laneEnd[i] <=
|
|
105
|
-
laneEnd[i] =
|
|
106
|
-
laneOf.set(
|
|
364
|
+
if (laneEnd[i] <= block.startMin) {
|
|
365
|
+
laneEnd[i] = block.endMin;
|
|
366
|
+
laneOf.set(block.id, i);
|
|
107
367
|
placed = true;
|
|
108
368
|
break;
|
|
109
369
|
}
|
|
110
370
|
}
|
|
111
371
|
if (!placed) {
|
|
112
|
-
laneEnd.push(
|
|
113
|
-
laneOf.set(
|
|
372
|
+
laneEnd.push(block.endMin);
|
|
373
|
+
laneOf.set(block.id, laneEnd.length - 1);
|
|
114
374
|
}
|
|
115
375
|
}
|
|
116
376
|
const lanes = laneEnd.length;
|
|
117
|
-
for (const
|
|
118
|
-
|
|
377
|
+
for (const block of sortedGroup) {
|
|
378
|
+
output.push({ ...block, lane: laneOf.get(block.id) ?? 0, lanes });
|
|
119
379
|
}
|
|
120
380
|
}
|
|
121
|
-
return
|
|
122
|
-
};
|
|
123
|
-
function isNameLike(resource) {
|
|
124
|
-
if (!resource || typeof resource !== "object") {
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
|
-
const maybe = resource;
|
|
128
|
-
return typeof maybe.firstName === "string" || typeof maybe.lastName === "string";
|
|
381
|
+
return output;
|
|
129
382
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) {
|
|
142
|
-
return null;
|
|
383
|
+
|
|
384
|
+
// src/utils/scheduler-data.utils.ts
|
|
385
|
+
function buildResourceMap({
|
|
386
|
+
fallback,
|
|
387
|
+
getValue,
|
|
388
|
+
resources,
|
|
389
|
+
toKey
|
|
390
|
+
}) {
|
|
391
|
+
const map = /* @__PURE__ */ new Map();
|
|
392
|
+
for (const resource of resources) {
|
|
393
|
+
map.set(toKey(resource), getValue(resource) ?? fallback);
|
|
143
394
|
}
|
|
144
|
-
return
|
|
395
|
+
return map;
|
|
145
396
|
}
|
|
146
|
-
|
|
147
|
-
slate: "bg-slate-100 dark:bg-slate-900/40",
|
|
148
|
-
gray: "bg-gray-100 dark:bg-gray-900/40",
|
|
149
|
-
zinc: "bg-zinc-100 dark:bg-zinc-900/40",
|
|
150
|
-
neutral: "bg-neutral-100 dark:bg-neutral-900/40",
|
|
151
|
-
stone: "bg-stone-100 dark:bg-stone-900/40",
|
|
152
|
-
red: "bg-red-100 dark:bg-red-950/40",
|
|
153
|
-
orange: "bg-orange-100 dark:bg-orange-950/40",
|
|
154
|
-
amber: "bg-amber-100 dark:bg-amber-950/40",
|
|
155
|
-
yellow: "bg-yellow-100 dark:bg-yellow-950/40",
|
|
156
|
-
lime: "bg-lime-100 dark:bg-lime-950/40",
|
|
157
|
-
green: "bg-green-100 dark:bg-green-950/40",
|
|
158
|
-
emerald: "bg-emerald-100 dark:bg-emerald-950/40",
|
|
159
|
-
teal: "bg-teal-100 dark:bg-teal-950/40",
|
|
160
|
-
cyan: "bg-cyan-100 dark:bg-cyan-950/40",
|
|
161
|
-
sky: "bg-sky-100 dark:bg-sky-950/40",
|
|
162
|
-
blue: "bg-blue-100 dark:bg-blue-950/40",
|
|
163
|
-
indigo: "bg-indigo-100 dark:bg-indigo-950/40",
|
|
164
|
-
violet: "bg-violet-100 dark:bg-violet-950/40",
|
|
165
|
-
purple: "bg-purple-100 dark:bg-purple-950/40",
|
|
166
|
-
fuchsia: "bg-fuchsia-100 dark:bg-fuchsia-950/40",
|
|
167
|
-
pink: "bg-pink-100 dark:bg-pink-950/40",
|
|
168
|
-
rose: "bg-rose-100 dark:bg-rose-950/40"
|
|
169
|
-
};
|
|
170
|
-
function Scheduler({
|
|
171
|
-
resources,
|
|
172
|
-
appointments,
|
|
173
|
-
selectedDate,
|
|
174
|
-
onSelectedDateChange,
|
|
397
|
+
function normalizeAppointments({
|
|
175
398
|
adapter,
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
resourceAppointmentClassMap,
|
|
181
|
-
getResourceAppointmentAppearance,
|
|
182
|
-
getResourceAppointmentColorToken,
|
|
183
|
-
appointmentColorTokenClassMap,
|
|
184
|
-
getResourceAppointmentBackground,
|
|
185
|
-
renderDatePicker,
|
|
186
|
-
prevButtonLabel = "Prev",
|
|
187
|
-
nextButtonLabel = "Next",
|
|
188
|
-
dayStart = "09:00",
|
|
189
|
-
dayEnd = "18:00"
|
|
399
|
+
appointments,
|
|
400
|
+
dayMinutes,
|
|
401
|
+
dayStartAbs,
|
|
402
|
+
selectedISO
|
|
190
403
|
}) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const appointmentMap = React__namespace.useMemo(() => {
|
|
217
|
-
return new Map(renderAppts.map((appointment) => [appointment.id, appointment]));
|
|
218
|
-
}, [renderAppts]);
|
|
219
|
-
const effectiveAppts = React__namespace.useMemo(() => {
|
|
220
|
-
if (drag.kind === "none") {
|
|
221
|
-
return renderAppts;
|
|
222
|
-
}
|
|
223
|
-
return renderAppts.map((appointment) => {
|
|
224
|
-
if (appointment.id !== drag.appointmentId) {
|
|
225
|
-
return appointment;
|
|
226
|
-
}
|
|
227
|
-
return {
|
|
228
|
-
...appointment,
|
|
229
|
-
resourceId: drag.resourceId,
|
|
230
|
-
startMin: drag.startMin,
|
|
231
|
-
endMin: drag.endMin,
|
|
232
|
-
start: dateAtMinute(selectedDate, dayStartAbs, drag.startMin),
|
|
233
|
-
end: dateAtMinute(selectedDate, dayStartAbs, drag.endMin)
|
|
234
|
-
};
|
|
235
|
-
});
|
|
236
|
-
}, [drag, renderAppts, selectedDate, dayStartAbs]);
|
|
237
|
-
const laidOutByResource = React__namespace.useMemo(() => {
|
|
238
|
-
const byResource = /* @__PURE__ */ new Map();
|
|
239
|
-
for (const resource of resources) {
|
|
240
|
-
const events = effectiveAppts.filter((appointment) => appointment.resourceId === resource.id);
|
|
241
|
-
byResource.set(resource.id, layoutOverlaps(events));
|
|
242
|
-
}
|
|
243
|
-
return byResource;
|
|
244
|
-
}, [effectiveAppts, resources]);
|
|
245
|
-
const appointmentBgByResource = React__namespace.useMemo(() => {
|
|
246
|
-
const map = /* @__PURE__ */ new Map();
|
|
247
|
-
if (!getResourceAppointmentBackground) {
|
|
248
|
-
return map;
|
|
249
|
-
}
|
|
250
|
-
for (const resource of resources) {
|
|
251
|
-
map.set(resource.id, getResourceAppointmentBackground(resource));
|
|
404
|
+
return appointments.map((appointment) => {
|
|
405
|
+
const start = new Date(adapter.getStart(appointment));
|
|
406
|
+
const end = new Date(adapter.getEnd(appointment));
|
|
407
|
+
return {
|
|
408
|
+
raw: appointment,
|
|
409
|
+
id: adapter.getId(appointment),
|
|
410
|
+
resourceId: adapter.getResourceId(appointment),
|
|
411
|
+
start,
|
|
412
|
+
end,
|
|
413
|
+
startMin: clamp(minutesFromDayStart(dayStartAbs, start), 0, dayMinutes),
|
|
414
|
+
endMin: clamp(minutesFromDayStart(dayStartAbs, end), 0, dayMinutes),
|
|
415
|
+
title: adapter.getTitle(appointment)
|
|
416
|
+
};
|
|
417
|
+
}).filter((appointment) => dayISO(appointment.start) === selectedISO);
|
|
418
|
+
}
|
|
419
|
+
function buildAppointmentMap(appointments) {
|
|
420
|
+
return new Map(appointments.map((appointment) => [appointment.id, appointment]));
|
|
421
|
+
}
|
|
422
|
+
function applyDragToAppointments(appointments, drag, selectedDate, dayStartAbs) {
|
|
423
|
+
if (drag.kind === "none") {
|
|
424
|
+
return appointments;
|
|
425
|
+
}
|
|
426
|
+
return appointments.map((appointment) => {
|
|
427
|
+
if (appointment.id !== drag.appointmentId) {
|
|
428
|
+
return appointment;
|
|
252
429
|
}
|
|
253
|
-
return
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
430
|
+
return {
|
|
431
|
+
...appointment,
|
|
432
|
+
resourceId: drag.resourceId,
|
|
433
|
+
startMin: drag.startMin,
|
|
434
|
+
endMin: drag.endMin,
|
|
435
|
+
start: dateAtMinute(selectedDate, dayStartAbs, drag.startMin),
|
|
436
|
+
end: dateAtMinute(selectedDate, dayStartAbs, drag.endMin)
|
|
260
437
|
};
|
|
261
|
-
|
|
262
|
-
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
function buildLaidOutByResource(resources, appointments) {
|
|
441
|
+
const appointmentsByResource = /* @__PURE__ */ new Map();
|
|
442
|
+
for (const appointment of appointments) {
|
|
443
|
+
const current = appointmentsByResource.get(appointment.resourceId) ?? [];
|
|
444
|
+
current.push(appointment);
|
|
445
|
+
appointmentsByResource.set(appointment.resourceId, current);
|
|
446
|
+
}
|
|
447
|
+
const result = /* @__PURE__ */ new Map();
|
|
448
|
+
for (const resource of resources) {
|
|
449
|
+
result.set(resource.id, layoutOverlaps(appointmentsByResource.get(resource.id) ?? []));
|
|
450
|
+
}
|
|
451
|
+
return result;
|
|
452
|
+
}
|
|
453
|
+
function buildAppointmentAppearanceByResource(resources, resourceAppointmentClassMap, getResourceAppointmentAppearance, appointmentColorTokenClassByResource, appointmentBgByResource) {
|
|
454
|
+
const map = /* @__PURE__ */ new Map();
|
|
455
|
+
for (const resource of resources) {
|
|
456
|
+
const classFromIdMap = resourceAppointmentClassMap?.[String(resource.id)];
|
|
457
|
+
if (classFromIdMap) {
|
|
458
|
+
map.set(resource.id, { className: classFromIdMap });
|
|
459
|
+
continue;
|
|
263
460
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
map.set(resource.id,
|
|
461
|
+
const appearance = getResourceAppointmentAppearance?.(resource);
|
|
462
|
+
if (appearance) {
|
|
463
|
+
map.set(resource.id, appearance);
|
|
464
|
+
continue;
|
|
267
465
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
466
|
+
const tokenClass = appointmentColorTokenClassByResource.get(resource.id);
|
|
467
|
+
if (tokenClass) {
|
|
468
|
+
map.set(resource.id, { className: tokenClass });
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
const legacyBackground = appointmentBgByResource.get(resource.id);
|
|
472
|
+
map.set(resource.id, legacyBackground ? { className: legacyBackground } : void 0);
|
|
473
|
+
}
|
|
474
|
+
return map;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/hooks/useSchedulerBaseData.ts
|
|
478
|
+
function useSchedulerBaseData({
|
|
479
|
+
adapter,
|
|
480
|
+
appointments,
|
|
481
|
+
dayEnd = "18:00",
|
|
482
|
+
dayStart = "09:00",
|
|
483
|
+
resources,
|
|
484
|
+
selectedDate
|
|
485
|
+
}) {
|
|
486
|
+
const dayStartAbs = React5__namespace.useMemo(() => parseHHMM(dayStart), [dayStart]);
|
|
487
|
+
const dayEndAbs = React5__namespace.useMemo(() => parseHHMM(dayEnd), [dayEnd]);
|
|
488
|
+
const dayMinutes = React5__namespace.useMemo(() => Math.max(0, dayEndAbs - dayStartAbs), [dayEndAbs, dayStartAbs]);
|
|
489
|
+
const gridHeight = React5__namespace.useMemo(() => TOP_PAD + dayMinutes * PX_PER_MIN + BOTTOM_PAD, [dayMinutes]);
|
|
490
|
+
const gridMinWidth = React5__namespace.useMemo(() => GUTTER_W + Math.max(resources.length, 1) * RESOURCE_MIN_W, [resources.length]);
|
|
491
|
+
const selectedISO = React5__namespace.useMemo(() => dayISO(selectedDate), [selectedDate]);
|
|
492
|
+
const renderAppts = React5__namespace.useMemo(() => {
|
|
493
|
+
return normalizeAppointments({
|
|
494
|
+
adapter,
|
|
495
|
+
appointments,
|
|
496
|
+
dayMinutes,
|
|
497
|
+
dayStartAbs,
|
|
498
|
+
selectedISO
|
|
499
|
+
});
|
|
500
|
+
}, [adapter, appointments, dayMinutes, dayStartAbs, selectedISO]);
|
|
501
|
+
const appointmentMap = React5__namespace.useMemo(() => buildAppointmentMap(renderAppts), [renderAppts]);
|
|
502
|
+
return {
|
|
503
|
+
appointmentMap,
|
|
504
|
+
dayEndAbs,
|
|
505
|
+
dayMinutes,
|
|
506
|
+
dayStartAbs,
|
|
507
|
+
gridHeight,
|
|
508
|
+
gridMinWidth,
|
|
509
|
+
renderAppts
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function useSchedulerInteractions({
|
|
513
|
+
appointmentMap,
|
|
514
|
+
dayMinutes,
|
|
515
|
+
dayStartAbs,
|
|
516
|
+
onAppointmentChange,
|
|
517
|
+
onPersistMoveResize,
|
|
518
|
+
renderAppts,
|
|
519
|
+
resources,
|
|
520
|
+
selectedDate
|
|
521
|
+
}) {
|
|
522
|
+
const [drag, setDrag] = React5__namespace.useState({ kind: "none" });
|
|
523
|
+
const colRefs = React5__namespace.useRef({});
|
|
524
|
+
const suppressClickRef = React5__namespace.useRef(false);
|
|
525
|
+
const pointerToMin = React5__namespace.useCallback(
|
|
526
|
+
(resourceId, clientY) => {
|
|
527
|
+
const col = colRefs.current[String(resourceId)];
|
|
528
|
+
if (!col) {
|
|
529
|
+
return 0;
|
|
277
530
|
}
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
531
|
+
const y = clientY - col.getBoundingClientRect().top - TOP_PAD;
|
|
532
|
+
return clamp(snap(y / PX_PER_MIN), 0, dayMinutes);
|
|
533
|
+
},
|
|
534
|
+
[dayMinutes]
|
|
535
|
+
);
|
|
536
|
+
const pointerToResourceId = React5__namespace.useCallback(
|
|
537
|
+
(clientX, fallbackResourceId) => {
|
|
538
|
+
for (const resource of resources) {
|
|
539
|
+
const col = colRefs.current[String(resource.id)];
|
|
540
|
+
if (!col) {
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const rect = col.getBoundingClientRect();
|
|
544
|
+
if (clientX >= rect.left && clientX <= rect.right) {
|
|
545
|
+
return resource.id;
|
|
546
|
+
}
|
|
282
547
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
548
|
+
return fallbackResourceId;
|
|
549
|
+
},
|
|
550
|
+
[resources]
|
|
551
|
+
);
|
|
552
|
+
const hasOverlap = React5__namespace.useCallback(
|
|
553
|
+
(appointmentId, resourceId, startMin, endMin) => {
|
|
554
|
+
const events = renderAppts.filter((appointment) => appointment.resourceId === resourceId && appointment.id !== appointmentId);
|
|
555
|
+
return events.some((appointment) => overlaps(appointment, { startMin, endMin }));
|
|
556
|
+
},
|
|
557
|
+
[renderAppts]
|
|
558
|
+
);
|
|
559
|
+
const persistDrag = React5__namespace.useCallback(
|
|
560
|
+
async (state) => {
|
|
561
|
+
const target = appointmentMap.get(state.appointmentId);
|
|
562
|
+
if (!target) {
|
|
563
|
+
return;
|
|
287
564
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
map.set(resource.id, void 0);
|
|
291
|
-
continue;
|
|
565
|
+
if (hasOverlap(state.appointmentId, state.resourceId, state.startMin, state.endMin)) {
|
|
566
|
+
return;
|
|
292
567
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
568
|
+
const next = {
|
|
569
|
+
resourceId: state.resourceId,
|
|
570
|
+
start: dateAtMinute(selectedDate, dayStartAbs, state.startMin),
|
|
571
|
+
end: dateAtMinute(selectedDate, dayStartAbs, state.endMin)
|
|
572
|
+
};
|
|
573
|
+
if (onAppointmentChange) {
|
|
574
|
+
await onAppointmentChange({
|
|
575
|
+
kind: state.kind,
|
|
576
|
+
appointment: target.raw,
|
|
577
|
+
previous: {
|
|
578
|
+
resourceId: target.resourceId,
|
|
579
|
+
start: target.start,
|
|
580
|
+
end: target.end
|
|
581
|
+
},
|
|
582
|
+
next
|
|
583
|
+
});
|
|
584
|
+
return;
|
|
310
585
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
586
|
+
if (onPersistMoveResize) {
|
|
587
|
+
await onPersistMoveResize({
|
|
588
|
+
appointment: target.raw,
|
|
589
|
+
resourceId: next.resourceId,
|
|
590
|
+
start: next.start,
|
|
591
|
+
end: next.end
|
|
592
|
+
});
|
|
314
593
|
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (onAppointmentChange) {
|
|
336
|
-
await onAppointmentChange({
|
|
337
|
-
kind: state.kind,
|
|
338
|
-
appointment: target.raw,
|
|
339
|
-
previous: {
|
|
340
|
-
resourceId: target.resourceId,
|
|
341
|
-
start: target.start,
|
|
342
|
-
end: target.end
|
|
343
|
-
},
|
|
344
|
-
next
|
|
345
|
-
});
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
if (onPersistMoveResize) {
|
|
349
|
-
await onPersistMoveResize({
|
|
350
|
-
appointment: target.raw,
|
|
351
|
-
resourceId: next.resourceId,
|
|
352
|
-
start: next.start,
|
|
353
|
-
end: next.end
|
|
594
|
+
},
|
|
595
|
+
[appointmentMap, dayStartAbs, hasOverlap, onAppointmentChange, onPersistMoveResize, selectedDate]
|
|
596
|
+
);
|
|
597
|
+
const onApptPointerDown = React5__namespace.useCallback(
|
|
598
|
+
(event, appointment) => {
|
|
599
|
+
if (event.button !== 0) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
event.stopPropagation();
|
|
603
|
+
suppressClickRef.current = false;
|
|
604
|
+
const pointerMin = pointerToMin(appointment.resourceId, event.clientY);
|
|
605
|
+
setDrag({
|
|
606
|
+
kind: "move",
|
|
607
|
+
appointmentId: appointment.id,
|
|
608
|
+
pointerId: event.pointerId,
|
|
609
|
+
resourceId: appointment.resourceId,
|
|
610
|
+
durationMin: Math.max(MIN_EVENT_MIN, appointment.endMin - appointment.startMin),
|
|
611
|
+
offsetMin: pointerMin - appointment.startMin,
|
|
612
|
+
startMin: appointment.startMin,
|
|
613
|
+
endMin: appointment.endMin
|
|
354
614
|
});
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
event.stopPropagation();
|
|
362
|
-
suppressClickRef.current = false;
|
|
363
|
-
const pointerMin = pointerToMin(appointment.resourceId, event.clientY);
|
|
364
|
-
setDrag({
|
|
365
|
-
kind: "move",
|
|
366
|
-
appointmentId: appointment.id,
|
|
367
|
-
pointerId: event.pointerId,
|
|
368
|
-
resourceId: appointment.resourceId,
|
|
369
|
-
durationMin: Math.max(MIN_EVENT_MIN, appointment.endMin - appointment.startMin),
|
|
370
|
-
offsetMin: pointerMin - appointment.startMin,
|
|
371
|
-
startMin: appointment.startMin,
|
|
372
|
-
endMin: appointment.endMin
|
|
373
|
-
});
|
|
374
|
-
};
|
|
375
|
-
const onResizePointerDown = (event, appointment) => {
|
|
615
|
+
},
|
|
616
|
+
[pointerToMin]
|
|
617
|
+
);
|
|
618
|
+
const onResizePointerDown = React5__namespace.useCallback((event, appointment) => {
|
|
376
619
|
if (event.button !== 0) {
|
|
377
620
|
return;
|
|
378
621
|
}
|
|
@@ -386,39 +629,42 @@ function Scheduler({
|
|
|
386
629
|
startMin: appointment.startMin,
|
|
387
630
|
endMin: appointment.endMin
|
|
388
631
|
});
|
|
389
|
-
};
|
|
390
|
-
const onGlobalPointerMove = (
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
632
|
+
}, []);
|
|
633
|
+
const onGlobalPointerMove = React5__namespace.useCallback(
|
|
634
|
+
(event) => {
|
|
635
|
+
if (drag.kind === "none") {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (drag.kind === "move") {
|
|
639
|
+
const nextResourceId = pointerToResourceId(event.clientX, drag.resourceId);
|
|
640
|
+
const pointerMin2 = pointerToMin(nextResourceId, event.clientY);
|
|
641
|
+
const rawStart = pointerMin2 - drag.offsetMin;
|
|
642
|
+
const nextStartMin = clamp(snap(rawStart), 0, dayMinutes - drag.durationMin);
|
|
643
|
+
const nextEndMin2 = nextStartMin + drag.durationMin;
|
|
644
|
+
if (nextStartMin !== drag.startMin || nextEndMin2 !== drag.endMin || nextResourceId !== drag.resourceId) {
|
|
645
|
+
suppressClickRef.current = true;
|
|
646
|
+
setDrag({
|
|
647
|
+
...drag,
|
|
648
|
+
resourceId: nextResourceId,
|
|
649
|
+
startMin: nextStartMin,
|
|
650
|
+
endMin: nextEndMin2
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const pointerMin = pointerToMin(drag.resourceId, event.clientY);
|
|
656
|
+
const nextEndMin = clamp(snap(pointerMin), drag.startMin + MIN_EVENT_MIN, dayMinutes);
|
|
657
|
+
if (nextEndMin !== drag.endMin) {
|
|
401
658
|
suppressClickRef.current = true;
|
|
402
659
|
setDrag({
|
|
403
660
|
...drag,
|
|
404
|
-
|
|
405
|
-
startMin: nextStartMin,
|
|
406
|
-
endMin: nextEndMin2
|
|
661
|
+
endMin: nextEndMin
|
|
407
662
|
});
|
|
408
663
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
if (nextEndMin !== drag.endMin) {
|
|
414
|
-
suppressClickRef.current = true;
|
|
415
|
-
setDrag({
|
|
416
|
-
...drag,
|
|
417
|
-
endMin: nextEndMin
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
};
|
|
421
|
-
const onGlobalPointerUp = () => {
|
|
664
|
+
},
|
|
665
|
+
[dayMinutes, drag, pointerToMin, pointerToResourceId]
|
|
666
|
+
);
|
|
667
|
+
const onGlobalPointerUp = React5__namespace.useCallback(() => {
|
|
422
668
|
if (drag.kind === "none") {
|
|
423
669
|
return;
|
|
424
670
|
}
|
|
@@ -427,55 +673,174 @@ function Scheduler({
|
|
|
427
673
|
if (suppressClickRef.current) {
|
|
428
674
|
void persistDrag(current);
|
|
429
675
|
}
|
|
676
|
+
}, [drag, persistDrag]);
|
|
677
|
+
return {
|
|
678
|
+
colRefs,
|
|
679
|
+
drag,
|
|
680
|
+
onApptPointerDown,
|
|
681
|
+
onGlobalPointerMove,
|
|
682
|
+
onGlobalPointerUp,
|
|
683
|
+
onResizePointerDown,
|
|
684
|
+
suppressClickRef
|
|
430
685
|
};
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
onResizePointerDown2(event);
|
|
454
|
-
},
|
|
455
|
-
className: "absolute inset-x-1 bottom-1 h-2 cursor-ns-resize rounded-full bg-border/90 transition-colors hover:bg-border"
|
|
456
|
-
}
|
|
457
|
-
)
|
|
458
|
-
] });
|
|
459
|
-
};
|
|
460
|
-
const defaultRenderDatePicker = ({
|
|
461
|
-
selectedDate: value,
|
|
462
|
-
onSelectedDateChange: onDateChange
|
|
463
|
-
}) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
464
|
-
"input",
|
|
465
|
-
{
|
|
466
|
-
type: "date",
|
|
467
|
-
value: toInputDateValue(value),
|
|
468
|
-
onChange: (event) => {
|
|
469
|
-
const nextDate = fromInputDateValue(event.target.value);
|
|
470
|
-
if (nextDate) {
|
|
471
|
-
onDateChange(nextDate);
|
|
472
|
-
}
|
|
473
|
-
},
|
|
474
|
-
className: "h-7 rounded-md border border-border bg-card px-2 text-xs text-foreground",
|
|
475
|
-
"aria-label": "Select date"
|
|
686
|
+
}
|
|
687
|
+
function useSchedulerPresentationData({
|
|
688
|
+
appointmentColorTokenClassMap,
|
|
689
|
+
dayStartAbs,
|
|
690
|
+
drag,
|
|
691
|
+
getResourceAppointmentAppearance,
|
|
692
|
+
getResourceAppointmentBackground,
|
|
693
|
+
getResourceAppointmentColorToken,
|
|
694
|
+
renderAppts,
|
|
695
|
+
resourceAppointmentClassMap,
|
|
696
|
+
resources,
|
|
697
|
+
selectedDate
|
|
698
|
+
}) {
|
|
699
|
+
const effectiveAppts = React5__namespace.useMemo(() => {
|
|
700
|
+
return applyDragToAppointments(renderAppts, drag, selectedDate, dayStartAbs);
|
|
701
|
+
}, [dayStartAbs, drag, renderAppts, selectedDate]);
|
|
702
|
+
const laidOutByResource = React5__namespace.useMemo(() => {
|
|
703
|
+
return buildLaidOutByResource(resources, effectiveAppts);
|
|
704
|
+
}, [effectiveAppts, resources]);
|
|
705
|
+
const appointmentBgByResource = React5__namespace.useMemo(() => {
|
|
706
|
+
if (!getResourceAppointmentBackground) {
|
|
707
|
+
return /* @__PURE__ */ new Map();
|
|
476
708
|
}
|
|
477
|
-
|
|
478
|
-
|
|
709
|
+
return buildResourceMap({
|
|
710
|
+
fallback: void 0,
|
|
711
|
+
getValue: getResourceAppointmentBackground,
|
|
712
|
+
resources,
|
|
713
|
+
toKey: (resource) => resource.id
|
|
714
|
+
});
|
|
715
|
+
}, [getResourceAppointmentBackground, resources]);
|
|
716
|
+
const appointmentColorTokenClassByResource = React5__namespace.useMemo(() => {
|
|
717
|
+
if (!getResourceAppointmentColorToken) {
|
|
718
|
+
return /* @__PURE__ */ new Map();
|
|
719
|
+
}
|
|
720
|
+
const mergedClassMap = {
|
|
721
|
+
...DEFAULT_APPOINTMENT_COLOR_TOKEN_CLASS_MAP,
|
|
722
|
+
...appointmentColorTokenClassMap ?? {}
|
|
723
|
+
};
|
|
724
|
+
return buildResourceMap({
|
|
725
|
+
fallback: void 0,
|
|
726
|
+
getValue: (resource) => {
|
|
727
|
+
const token = getResourceAppointmentColorToken(resource);
|
|
728
|
+
return token ? mergedClassMap[token] : void 0;
|
|
729
|
+
},
|
|
730
|
+
resources,
|
|
731
|
+
toKey: (resource) => resource.id
|
|
732
|
+
});
|
|
733
|
+
}, [appointmentColorTokenClassMap, getResourceAppointmentColorToken, resources]);
|
|
734
|
+
const appointmentAppearanceByResource = React5__namespace.useMemo(() => {
|
|
735
|
+
return buildAppointmentAppearanceByResource(
|
|
736
|
+
resources,
|
|
737
|
+
resourceAppointmentClassMap,
|
|
738
|
+
getResourceAppointmentAppearance,
|
|
739
|
+
appointmentColorTokenClassByResource,
|
|
740
|
+
appointmentBgByResource
|
|
741
|
+
);
|
|
742
|
+
}, [
|
|
743
|
+
appointmentBgByResource,
|
|
744
|
+
appointmentColorTokenClassByResource,
|
|
745
|
+
getResourceAppointmentAppearance,
|
|
746
|
+
resourceAppointmentClassMap,
|
|
747
|
+
resources
|
|
748
|
+
]);
|
|
749
|
+
return {
|
|
750
|
+
appointmentAppearanceByResource,
|
|
751
|
+
appointmentBgByResource,
|
|
752
|
+
effectiveAppts,
|
|
753
|
+
laidOutByResource
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
function Scheduler({
|
|
757
|
+
resources,
|
|
758
|
+
appointments,
|
|
759
|
+
selectedDate,
|
|
760
|
+
onSelectedDateChange,
|
|
761
|
+
adapter,
|
|
762
|
+
onPersistMoveResize,
|
|
763
|
+
onAppointmentChange,
|
|
764
|
+
renderResourceHeader,
|
|
765
|
+
renderAppointment,
|
|
766
|
+
resourceAppointmentClassMap,
|
|
767
|
+
getResourceAppointmentAppearance,
|
|
768
|
+
getResourceAppointmentColorToken,
|
|
769
|
+
appointmentColorTokenClassMap,
|
|
770
|
+
getResourceAppointmentBackground,
|
|
771
|
+
renderToolbar,
|
|
772
|
+
renderDatePicker,
|
|
773
|
+
prevButtonLabel = "Prev",
|
|
774
|
+
nextButtonLabel = "Next",
|
|
775
|
+
dayStart = "09:00",
|
|
776
|
+
dayEnd = "18:00"
|
|
777
|
+
}) {
|
|
778
|
+
const {
|
|
779
|
+
appointmentMap,
|
|
780
|
+
dayMinutes,
|
|
781
|
+
dayStartAbs,
|
|
782
|
+
gridHeight,
|
|
783
|
+
gridMinWidth,
|
|
784
|
+
renderAppts
|
|
785
|
+
} = useSchedulerBaseData({
|
|
786
|
+
adapter,
|
|
787
|
+
appointments,
|
|
788
|
+
dayEnd,
|
|
789
|
+
dayStart,
|
|
790
|
+
resources,
|
|
791
|
+
selectedDate
|
|
792
|
+
});
|
|
793
|
+
const {
|
|
794
|
+
colRefs,
|
|
795
|
+
drag,
|
|
796
|
+
onApptPointerDown,
|
|
797
|
+
onGlobalPointerMove,
|
|
798
|
+
onGlobalPointerUp,
|
|
799
|
+
onResizePointerDown,
|
|
800
|
+
suppressClickRef
|
|
801
|
+
} = useSchedulerInteractions({
|
|
802
|
+
appointmentMap,
|
|
803
|
+
dayMinutes,
|
|
804
|
+
dayStartAbs,
|
|
805
|
+
onAppointmentChange,
|
|
806
|
+
onPersistMoveResize,
|
|
807
|
+
renderAppts,
|
|
808
|
+
resources,
|
|
809
|
+
selectedDate
|
|
810
|
+
});
|
|
811
|
+
const {
|
|
812
|
+
appointmentAppearanceByResource,
|
|
813
|
+
appointmentBgByResource,
|
|
814
|
+
laidOutByResource
|
|
815
|
+
} = useSchedulerPresentationData({
|
|
816
|
+
appointmentColorTokenClassMap,
|
|
817
|
+
dayStartAbs,
|
|
818
|
+
drag,
|
|
819
|
+
getResourceAppointmentAppearance,
|
|
820
|
+
getResourceAppointmentBackground,
|
|
821
|
+
getResourceAppointmentColorToken,
|
|
822
|
+
renderAppts,
|
|
823
|
+
resourceAppointmentClassMap,
|
|
824
|
+
resources,
|
|
825
|
+
selectedDate
|
|
826
|
+
});
|
|
827
|
+
const defaultResourceHeaderRenderer = (resource) => defaultRenderResourceHeader({ resource });
|
|
828
|
+
const goToPreviousDay = () => onSelectedDateChange(shiftDays(selectedDate, -1));
|
|
829
|
+
const goToNextDay = () => onSelectedDateChange(shiftDays(selectedDate, 1));
|
|
830
|
+
const defaultDatePicker = (renderDatePicker ?? defaultRenderDatePicker)({ selectedDate, onSelectedDateChange });
|
|
831
|
+
const toolbar = renderToolbar ? renderToolbar({
|
|
832
|
+
selectedDate,
|
|
833
|
+
onSelectedDateChange,
|
|
834
|
+
goToPreviousDay,
|
|
835
|
+
goToNextDay,
|
|
836
|
+
defaultDatePicker
|
|
837
|
+
}) : defaultRenderToolbar({
|
|
838
|
+
goToPreviousDay,
|
|
839
|
+
goToNextDay,
|
|
840
|
+
defaultDatePicker,
|
|
841
|
+
prevButtonLabel,
|
|
842
|
+
nextButtonLabel
|
|
843
|
+
});
|
|
479
844
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
480
845
|
"section",
|
|
481
846
|
{
|
|
@@ -484,27 +849,7 @@ function Scheduler({
|
|
|
484
849
|
onPointerUp: onGlobalPointerUp,
|
|
485
850
|
onPointerCancel: onGlobalPointerUp,
|
|
486
851
|
children: [
|
|
487
|
-
|
|
488
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
489
|
-
"button",
|
|
490
|
-
{
|
|
491
|
-
className: "rfs-btn border-border bg-card text-foreground hover:bg-muted",
|
|
492
|
-
type: "button",
|
|
493
|
-
onClick: () => onSelectedDateChange(shiftDays(selectedDate, -1)),
|
|
494
|
-
children: prevButtonLabel
|
|
495
|
-
}
|
|
496
|
-
),
|
|
497
|
-
(renderDatePicker ?? defaultRenderDatePicker)({ selectedDate, onSelectedDateChange }),
|
|
498
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
499
|
-
"button",
|
|
500
|
-
{
|
|
501
|
-
className: "rfs-btn border-border bg-card text-foreground hover:bg-muted",
|
|
502
|
-
type: "button",
|
|
503
|
-
onClick: () => onSelectedDateChange(shiftDays(selectedDate, 1)),
|
|
504
|
-
children: nextButtonLabel
|
|
505
|
-
}
|
|
506
|
-
)
|
|
507
|
-
] }),
|
|
852
|
+
toolbar,
|
|
508
853
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-shell border-border bg-card", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { minWidth: gridMinWidth }, children: [
|
|
509
854
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rfs-header border-border bg-card", children: [
|
|
510
855
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-gutter border-border", style: { width: GUTTER_W } }),
|
|
@@ -514,7 +859,7 @@ function Scheduler({
|
|
|
514
859
|
className: "rfs-resource-header border-border bg-card text-foreground",
|
|
515
860
|
style: { minWidth: RESOURCE_MIN_W, flex: 1 },
|
|
516
861
|
children: [
|
|
517
|
-
(renderResourceHeader ??
|
|
862
|
+
(renderResourceHeader ?? defaultResourceHeaderRenderer)(resource),
|
|
518
863
|
isNameLike(resource) ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfs-resource-subtitle text-muted-foreground", children: fullName(resource) }) : null
|
|
519
864
|
]
|
|
520
865
|
},
|
|
@@ -607,7 +952,7 @@ function Scheduler({
|
|
|
607
952
|
);
|
|
608
953
|
}
|
|
609
954
|
|
|
610
|
-
// src/appointment-change.ts
|
|
955
|
+
// src/utils/scheduler-appointment-change.utils.ts
|
|
611
956
|
function applySchedulerAppointmentChange(appointments, change, options) {
|
|
612
957
|
return appointments.map((item) => {
|
|
613
958
|
if (options.getId(item) !== options.getId(change.appointment)) {
|