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