@marco.colia/react-fast-scheduler 2.1.1 → 2.2.0
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/README.md +115 -66
- package/dist/index.cjs +902 -439
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +902 -439
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +11 -4
package/dist/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
1
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
2
|
import { format, startOfMonth, startOfWeek, endOfWeek, endOfMonth, addDays, addMonths, isSameDay, isToday, isSameMonth } from 'date-fns';
|
|
3
|
-
import * as
|
|
3
|
+
import * as React2 from 'react';
|
|
4
4
|
|
|
5
5
|
// src/components/defaultRenderAppointment/index.tsx
|
|
6
6
|
function defaultRenderAppointment({
|
|
7
7
|
appointment,
|
|
8
|
+
isDropInvalid,
|
|
8
9
|
onPointerDown,
|
|
9
10
|
onResizePointerDown,
|
|
10
11
|
appointmentAppearance,
|
|
@@ -14,224 +15,37 @@ function defaultRenderAppointment({
|
|
|
14
15
|
const description = typeof rawMaybe.description === "string" ? rawMaybe.description : void 0;
|
|
15
16
|
const legacyClassName = typeof appointmentBackgroundColor === "string" ? appointmentBackgroundColor : void 0;
|
|
16
17
|
const backgroundClassName = appointmentAppearance?.className ?? legacyClassName ?? "bg-muted/40 dark:bg-muted/30";
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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(
|
|
18
|
+
const invalidStateClassName = isDropInvalid ? "border-red-500 bg-red-100/80 text-red-950 ring-1 ring-red-500/60 dark:bg-red-950/40 dark:text-red-100" : "";
|
|
19
|
+
const visualStateClassName = appointment.visualState === "ghost" ? "cursor-grab border-dashed opacity-35" : appointment.visualState === "dragging" ? "cursor-grabbing opacity-55 shadow-xl ring-1 ring-border/70" : "cursor-grab";
|
|
20
|
+
const className = `relative h-full overflow-hidden rounded-md border border-border p-2 pb-5 text-foreground ${backgroundClassName} ${invalidStateClassName} ${visualStateClassName}`;
|
|
21
|
+
return /* @__PURE__ */ jsxs(
|
|
199
22
|
"div",
|
|
200
23
|
{
|
|
201
|
-
|
|
202
|
-
className
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
24
|
+
onPointerDown,
|
|
25
|
+
className,
|
|
26
|
+
children: [
|
|
27
|
+
isDropInvalid ? /* @__PURE__ */ jsx("div", { className: "absolute right-1 top-1 flex h-4 w-4 items-center justify-center rounded-full bg-red-600 text-[10px] font-bold text-white", children: "X" }) : null,
|
|
28
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs font-semibold", children: appointment.title }),
|
|
29
|
+
description ? /* @__PURE__ */ jsx("div", { className: "mt-1 text-[10px] font-medium text-muted-foreground", children: description }) : null,
|
|
30
|
+
/* @__PURE__ */ jsx(
|
|
31
|
+
"div",
|
|
32
|
+
{
|
|
33
|
+
role: "button",
|
|
34
|
+
"aria-label": "Resize appointment",
|
|
35
|
+
onPointerDown: (event) => {
|
|
36
|
+
if (appointment.visualState === "ghost") {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
event.stopPropagation();
|
|
40
|
+
onResizePointerDown(event);
|
|
41
|
+
},
|
|
42
|
+
className: "absolute inset-x-1 bottom-1 h-2 cursor-ns-resize rounded-full bg-border/90 transition-colors hover:bg-border"
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
]
|
|
210
46
|
}
|
|
211
47
|
);
|
|
212
48
|
}
|
|
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
|
-
}
|
|
235
49
|
|
|
236
50
|
// src/components/constants.ts
|
|
237
51
|
var STEP_MIN = 15;
|
|
@@ -390,17 +204,22 @@ function normalizeAppointments({
|
|
|
390
204
|
end,
|
|
391
205
|
startMin: clamp(minutesFromDayStart(dayStartAbs, start), 0, dayMinutes),
|
|
392
206
|
endMin: clamp(minutesFromDayStart(dayStartAbs, end), 0, dayMinutes),
|
|
393
|
-
title: adapter.getTitle(appointment)
|
|
207
|
+
title: adapter.getTitle(appointment),
|
|
208
|
+
renderKey: String(adapter.getId(appointment)),
|
|
209
|
+
visualState: "normal"
|
|
394
210
|
};
|
|
395
211
|
}).filter((appointment) => dayISO(appointment.start) === selectedISO);
|
|
396
212
|
}
|
|
397
213
|
function buildAppointmentMap(appointments) {
|
|
398
214
|
return new Map(appointments.map((appointment) => [appointment.id, appointment]));
|
|
399
215
|
}
|
|
400
|
-
function
|
|
216
|
+
function buildPresentationAppointments(appointments, drag, selectedDate, dayStartAbs) {
|
|
401
217
|
if (drag.kind === "none") {
|
|
402
218
|
return appointments;
|
|
403
219
|
}
|
|
220
|
+
if (drag.kind === "move") {
|
|
221
|
+
return appointments;
|
|
222
|
+
}
|
|
404
223
|
return appointments.map((appointment) => {
|
|
405
224
|
if (appointment.id !== drag.appointmentId) {
|
|
406
225
|
return appointment;
|
|
@@ -411,7 +230,8 @@ function applyDragToAppointments(appointments, drag, selectedDate, dayStartAbs)
|
|
|
411
230
|
startMin: drag.startMin,
|
|
412
231
|
endMin: drag.endMin,
|
|
413
232
|
start: dateAtMinute(selectedDate, dayStartAbs, drag.startMin),
|
|
414
|
-
end: dateAtMinute(selectedDate, dayStartAbs, drag.endMin)
|
|
233
|
+
end: dateAtMinute(selectedDate, dayStartAbs, drag.endMin),
|
|
234
|
+
visualState: "dragging"
|
|
415
235
|
};
|
|
416
236
|
});
|
|
417
237
|
}
|
|
@@ -424,10 +244,22 @@ function buildLaidOutByResource(resources, appointments) {
|
|
|
424
244
|
}
|
|
425
245
|
const result = /* @__PURE__ */ new Map();
|
|
426
246
|
for (const resource of resources) {
|
|
427
|
-
result.set(resource.id,
|
|
247
|
+
result.set(resource.id, buildLaidOutAppointments(appointmentsByResource.get(resource.id) ?? []));
|
|
428
248
|
}
|
|
429
249
|
return result;
|
|
430
250
|
}
|
|
251
|
+
function buildLaidOutAppointments(appointments) {
|
|
252
|
+
return layoutOverlaps(
|
|
253
|
+
appointments.map((appointment) => ({
|
|
254
|
+
...appointment,
|
|
255
|
+
appointmentId: appointment.id,
|
|
256
|
+
id: appointment.renderKey
|
|
257
|
+
}))
|
|
258
|
+
).map(({ appointmentId, ...appointment }) => ({
|
|
259
|
+
...appointment,
|
|
260
|
+
id: appointmentId
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
431
263
|
function buildAppointmentAppearanceByResource(resources, resourceAppointmentClassMap, getResourceAppointmentAppearance, appointmentColorTokenClassByResource, appointmentBgByResource) {
|
|
432
264
|
const map = /* @__PURE__ */ new Map();
|
|
433
265
|
for (const resource of resources) {
|
|
@@ -451,8 +283,6 @@ function buildAppointmentAppearanceByResource(resources, resourceAppointmentClas
|
|
|
451
283
|
}
|
|
452
284
|
return map;
|
|
453
285
|
}
|
|
454
|
-
|
|
455
|
-
// src/hooks/useSchedulerBaseData.ts
|
|
456
286
|
function useSchedulerBaseData({
|
|
457
287
|
adapter,
|
|
458
288
|
appointments,
|
|
@@ -461,13 +291,22 @@ function useSchedulerBaseData({
|
|
|
461
291
|
resources,
|
|
462
292
|
selectedDate
|
|
463
293
|
}) {
|
|
464
|
-
const dayStartAbs =
|
|
465
|
-
const dayEndAbs =
|
|
466
|
-
const dayMinutes =
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const
|
|
294
|
+
const dayStartAbs = React2.useMemo(() => parseHHMM(dayStart), [dayStart]);
|
|
295
|
+
const dayEndAbs = React2.useMemo(() => parseHHMM(dayEnd), [dayEnd]);
|
|
296
|
+
const dayMinutes = React2.useMemo(
|
|
297
|
+
() => Math.max(0, dayEndAbs - dayStartAbs),
|
|
298
|
+
[dayEndAbs, dayStartAbs]
|
|
299
|
+
);
|
|
300
|
+
const gridHeight = React2.useMemo(
|
|
301
|
+
() => TOP_PAD + dayMinutes * PX_PER_MIN + BOTTOM_PAD,
|
|
302
|
+
[dayMinutes]
|
|
303
|
+
);
|
|
304
|
+
const gridMinWidth = React2.useMemo(
|
|
305
|
+
() => GUTTER_W + Math.max(resources.length, 1) * RESOURCE_MIN_W,
|
|
306
|
+
[resources.length]
|
|
307
|
+
);
|
|
308
|
+
const selectedISO = React2.useMemo(() => dayISO(selectedDate), [selectedDate]);
|
|
309
|
+
const renderAppts = React2.useMemo(() => {
|
|
471
310
|
return normalizeAppointments({
|
|
472
311
|
adapter,
|
|
473
312
|
appointments,
|
|
@@ -476,7 +315,7 @@ function useSchedulerBaseData({
|
|
|
476
315
|
selectedISO
|
|
477
316
|
});
|
|
478
317
|
}, [adapter, appointments, dayMinutes, dayStartAbs, selectedISO]);
|
|
479
|
-
const appointmentMap =
|
|
318
|
+
const appointmentMap = React2.useMemo(() => buildAppointmentMap(renderAppts), [renderAppts]);
|
|
480
319
|
return {
|
|
481
320
|
appointmentMap,
|
|
482
321
|
dayEndAbs,
|
|
@@ -487,7 +326,157 @@ function useSchedulerBaseData({
|
|
|
487
326
|
renderAppts
|
|
488
327
|
};
|
|
489
328
|
}
|
|
490
|
-
|
|
329
|
+
|
|
330
|
+
// src/hooks/useSchedulerInteractions.helpers.ts
|
|
331
|
+
var DRAG_START_THRESHOLD_PX = 4;
|
|
332
|
+
var updateMovePreviewPosition = (setMovePreview, pointer) => {
|
|
333
|
+
setMovePreview(
|
|
334
|
+
(current) => current ? {
|
|
335
|
+
...current,
|
|
336
|
+
clientX: pointer.clientX,
|
|
337
|
+
clientY: pointer.clientY
|
|
338
|
+
} : current
|
|
339
|
+
);
|
|
340
|
+
};
|
|
341
|
+
var startPendingDragIfThresholdPassed = (args) => {
|
|
342
|
+
const { dragRef, pendingDragRef, pointer, setDrag, setMovePreview, suppressClickRef } = args;
|
|
343
|
+
const pending = pendingDragRef.current;
|
|
344
|
+
if (!pending || pending.drag.pointerId !== pointer.pointerId) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const deltaX = pointer.clientX - pending.startClientX;
|
|
348
|
+
const deltaY = pointer.clientY - pending.startClientY;
|
|
349
|
+
if (Math.hypot(deltaX, deltaY) < DRAG_START_THRESHOLD_PX) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
suppressClickRef.current = true;
|
|
353
|
+
dragRef.current = pending.drag;
|
|
354
|
+
setDrag(pending.drag);
|
|
355
|
+
if (pending.drag.kind === "move") {
|
|
356
|
+
setMovePreview({
|
|
357
|
+
appointment: pending.appointment,
|
|
358
|
+
clientX: pointer.clientX,
|
|
359
|
+
clientY: pointer.clientY,
|
|
360
|
+
width: pending.width,
|
|
361
|
+
height: pending.height,
|
|
362
|
+
offsetX: pending.offsetX,
|
|
363
|
+
offsetY: pending.offsetY
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
pendingDragRef.current = null;
|
|
367
|
+
};
|
|
368
|
+
var buildNextMoveDrag = (drag, pointer, dayMinutes, pointerToResourceId, pointerToMin) => {
|
|
369
|
+
const nextResourceId = pointerToResourceId(pointer.clientX, drag.resourceId);
|
|
370
|
+
const pointerMin = pointerToMin(nextResourceId, pointer.clientY);
|
|
371
|
+
const rawStart = pointerMin - drag.offsetMin;
|
|
372
|
+
const nextStartMin = clamp(snap(rawStart), 0, dayMinutes - drag.durationMin);
|
|
373
|
+
const nextEndMin = nextStartMin + drag.durationMin;
|
|
374
|
+
if (nextStartMin === drag.startMin && nextEndMin === drag.endMin && nextResourceId === drag.resourceId) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
...drag,
|
|
379
|
+
resourceId: nextResourceId,
|
|
380
|
+
startMin: nextStartMin,
|
|
381
|
+
endMin: nextEndMin
|
|
382
|
+
};
|
|
383
|
+
};
|
|
384
|
+
var buildNextResizeDrag = (drag, pointer, dayMinutes, pointerToMin) => {
|
|
385
|
+
const pointerMin = pointerToMin(drag.resourceId, pointer.clientY);
|
|
386
|
+
const nextEndMin = clamp(snap(pointerMin), drag.startMin + MIN_EVENT_MIN, dayMinutes);
|
|
387
|
+
if (nextEndMin === drag.endMin) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
...drag,
|
|
392
|
+
endMin: nextEndMin
|
|
393
|
+
};
|
|
394
|
+
};
|
|
395
|
+
var processPointerFrame = (args) => {
|
|
396
|
+
const {
|
|
397
|
+
dayMinutes,
|
|
398
|
+
dragRef,
|
|
399
|
+
latestPointerRef,
|
|
400
|
+
pendingDragRef,
|
|
401
|
+
pointerToMin,
|
|
402
|
+
pointerToResourceId,
|
|
403
|
+
setDrag,
|
|
404
|
+
setMovePreview,
|
|
405
|
+
suppressClickRef
|
|
406
|
+
} = args;
|
|
407
|
+
const pointer = latestPointerRef.current;
|
|
408
|
+
if (!pointer) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const currentDrag = dragRef.current;
|
|
412
|
+
if (currentDrag.kind === "none" && pendingDragRef.current) {
|
|
413
|
+
startPendingDragIfThresholdPassed({
|
|
414
|
+
pointer,
|
|
415
|
+
pendingDragRef,
|
|
416
|
+
setDrag,
|
|
417
|
+
setMovePreview,
|
|
418
|
+
dragRef,
|
|
419
|
+
suppressClickRef
|
|
420
|
+
});
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (currentDrag.kind === "none") {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (currentDrag.kind === "move") {
|
|
427
|
+
updateMovePreviewPosition(setMovePreview, pointer);
|
|
428
|
+
const nextDrag2 = buildNextMoveDrag(
|
|
429
|
+
currentDrag,
|
|
430
|
+
pointer,
|
|
431
|
+
dayMinutes,
|
|
432
|
+
pointerToResourceId,
|
|
433
|
+
pointerToMin
|
|
434
|
+
);
|
|
435
|
+
if (nextDrag2) {
|
|
436
|
+
suppressClickRef.current = true;
|
|
437
|
+
dragRef.current = nextDrag2;
|
|
438
|
+
setDrag(nextDrag2);
|
|
439
|
+
}
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const nextDrag = buildNextResizeDrag(currentDrag, pointer, dayMinutes, pointerToMin);
|
|
443
|
+
if (nextDrag) {
|
|
444
|
+
suppressClickRef.current = true;
|
|
445
|
+
dragRef.current = nextDrag;
|
|
446
|
+
setDrag(nextDrag);
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
var finishDrag = (args) => {
|
|
450
|
+
const {
|
|
451
|
+
dragRef,
|
|
452
|
+
frameRef,
|
|
453
|
+
latestPointerRef,
|
|
454
|
+
pendingDragRef,
|
|
455
|
+
persistDrag,
|
|
456
|
+
setDrag,
|
|
457
|
+
setMovePreview,
|
|
458
|
+
suppressClickRef
|
|
459
|
+
} = args;
|
|
460
|
+
if (frameRef.current !== null) {
|
|
461
|
+
window.cancelAnimationFrame(frameRef.current);
|
|
462
|
+
frameRef.current = null;
|
|
463
|
+
}
|
|
464
|
+
latestPointerRef.current = null;
|
|
465
|
+
pendingDragRef.current = null;
|
|
466
|
+
setMovePreview(null);
|
|
467
|
+
const current = dragRef.current;
|
|
468
|
+
if (current.kind === "none") {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
setDrag({ kind: "none" });
|
|
472
|
+
dragRef.current = { kind: "none" };
|
|
473
|
+
if (suppressClickRef.current) {
|
|
474
|
+
void persistDrag(current);
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
// src/hooks/useSchedulerInteractions.ts
|
|
479
|
+
var useSchedulerInteractions = ({
|
|
491
480
|
appointmentMap,
|
|
492
481
|
dayMinutes,
|
|
493
482
|
dayStartAbs,
|
|
@@ -496,11 +485,28 @@ function useSchedulerInteractions({
|
|
|
496
485
|
renderAppts,
|
|
497
486
|
resources,
|
|
498
487
|
selectedDate
|
|
499
|
-
}) {
|
|
500
|
-
const [drag, setDrag] =
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
488
|
+
}) => {
|
|
489
|
+
const [drag, setDrag] = React2.useState({ kind: "none" });
|
|
490
|
+
const [movePreview, setMovePreview] = React2.useState(
|
|
491
|
+
null
|
|
492
|
+
);
|
|
493
|
+
const colRefs = React2.useRef({});
|
|
494
|
+
const suppressClickRef = React2.useRef(false);
|
|
495
|
+
const dragRef = React2.useRef({ kind: "none" });
|
|
496
|
+
const pendingDragRef = React2.useRef(null);
|
|
497
|
+
const latestPointerRef = React2.useRef(null);
|
|
498
|
+
const frameRef = React2.useRef(null);
|
|
499
|
+
React2.useEffect(() => {
|
|
500
|
+
dragRef.current = drag;
|
|
501
|
+
}, [drag]);
|
|
502
|
+
React2.useEffect(() => {
|
|
503
|
+
return () => {
|
|
504
|
+
if (frameRef.current !== null) {
|
|
505
|
+
window.cancelAnimationFrame(frameRef.current);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
}, []);
|
|
509
|
+
const pointerToMin = React2.useCallback(
|
|
504
510
|
(resourceId, clientY) => {
|
|
505
511
|
const col = colRefs.current[String(resourceId)];
|
|
506
512
|
if (!col) {
|
|
@@ -511,7 +517,7 @@ function useSchedulerInteractions({
|
|
|
511
517
|
},
|
|
512
518
|
[dayMinutes]
|
|
513
519
|
);
|
|
514
|
-
const pointerToResourceId =
|
|
520
|
+
const pointerToResourceId = React2.useCallback(
|
|
515
521
|
(clientX, fallbackResourceId) => {
|
|
516
522
|
for (const resource of resources) {
|
|
517
523
|
const col = colRefs.current[String(resource.id)];
|
|
@@ -527,14 +533,24 @@ function useSchedulerInteractions({
|
|
|
527
533
|
},
|
|
528
534
|
[resources]
|
|
529
535
|
);
|
|
530
|
-
const hasOverlap =
|
|
536
|
+
const hasOverlap = React2.useCallback(
|
|
531
537
|
(appointmentId, resourceId, startMin, endMin) => {
|
|
532
|
-
const events = renderAppts.filter(
|
|
533
|
-
|
|
538
|
+
const events = renderAppts.filter(
|
|
539
|
+
(appointment) => appointment.resourceId === resourceId && appointment.id !== appointmentId
|
|
540
|
+
);
|
|
541
|
+
return events.some(
|
|
542
|
+
(appointment) => overlaps(appointment, { startMin, endMin })
|
|
543
|
+
);
|
|
534
544
|
},
|
|
535
545
|
[renderAppts]
|
|
536
546
|
);
|
|
537
|
-
const
|
|
547
|
+
const isDropInvalid = React2.useMemo(() => {
|
|
548
|
+
if (drag.kind === "none") {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
return hasOverlap(drag.appointmentId, drag.resourceId, drag.startMin, drag.endMin);
|
|
552
|
+
}, [drag, hasOverlap]);
|
|
553
|
+
const persistDrag = React2.useCallback(
|
|
538
554
|
async (state) => {
|
|
539
555
|
const target = appointmentMap.get(state.appointmentId);
|
|
540
556
|
if (!target) {
|
|
@@ -570,98 +586,124 @@ function useSchedulerInteractions({
|
|
|
570
586
|
});
|
|
571
587
|
}
|
|
572
588
|
},
|
|
573
|
-
[
|
|
589
|
+
[
|
|
590
|
+
appointmentMap,
|
|
591
|
+
dayStartAbs,
|
|
592
|
+
hasOverlap,
|
|
593
|
+
onAppointmentChange,
|
|
594
|
+
onPersistMoveResize,
|
|
595
|
+
selectedDate
|
|
596
|
+
]
|
|
574
597
|
);
|
|
575
|
-
const onApptPointerDown =
|
|
598
|
+
const onApptPointerDown = React2.useCallback(
|
|
576
599
|
(event, appointment) => {
|
|
577
600
|
if (event.button !== 0) {
|
|
578
601
|
return;
|
|
579
602
|
}
|
|
580
603
|
event.stopPropagation();
|
|
581
604
|
suppressClickRef.current = false;
|
|
605
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
582
606
|
const pointerMin = pointerToMin(appointment.resourceId, event.clientY);
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
607
|
+
pendingDragRef.current = {
|
|
608
|
+
drag: {
|
|
609
|
+
kind: "move",
|
|
610
|
+
appointmentId: appointment.id,
|
|
611
|
+
pointerId: event.pointerId,
|
|
612
|
+
resourceId: appointment.resourceId,
|
|
613
|
+
durationMin: Math.max(MIN_EVENT_MIN, appointment.endMin - appointment.startMin),
|
|
614
|
+
offsetMin: pointerMin - appointment.startMin,
|
|
615
|
+
startMin: appointment.startMin,
|
|
616
|
+
endMin: appointment.endMin
|
|
617
|
+
},
|
|
618
|
+
appointment,
|
|
619
|
+
startClientX: event.clientX,
|
|
620
|
+
startClientY: event.clientY,
|
|
621
|
+
width: rect.width,
|
|
622
|
+
height: rect.height,
|
|
623
|
+
offsetX: event.clientX - rect.left,
|
|
624
|
+
offsetY: event.clientY - rect.top
|
|
625
|
+
};
|
|
593
626
|
},
|
|
594
627
|
[pointerToMin]
|
|
595
628
|
);
|
|
596
|
-
const onResizePointerDown =
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
}
|
|
600
|
-
event.stopPropagation();
|
|
601
|
-
suppressClickRef.current = false;
|
|
602
|
-
setDrag({
|
|
603
|
-
kind: "resize",
|
|
604
|
-
appointmentId: appointment.id,
|
|
605
|
-
pointerId: event.pointerId,
|
|
606
|
-
resourceId: appointment.resourceId,
|
|
607
|
-
startMin: appointment.startMin,
|
|
608
|
-
endMin: appointment.endMin
|
|
609
|
-
});
|
|
610
|
-
}, []);
|
|
611
|
-
const onGlobalPointerMove = React5.useCallback(
|
|
612
|
-
(event) => {
|
|
613
|
-
if (drag.kind === "none") {
|
|
629
|
+
const onResizePointerDown = React2.useCallback(
|
|
630
|
+
(event, appointment) => {
|
|
631
|
+
if (event.button !== 0) {
|
|
614
632
|
return;
|
|
615
633
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
634
|
+
event.stopPropagation();
|
|
635
|
+
suppressClickRef.current = false;
|
|
636
|
+
pendingDragRef.current = {
|
|
637
|
+
drag: {
|
|
638
|
+
kind: "resize",
|
|
639
|
+
appointmentId: appointment.id,
|
|
640
|
+
pointerId: event.pointerId,
|
|
641
|
+
resourceId: appointment.resourceId,
|
|
642
|
+
startMin: appointment.startMin,
|
|
643
|
+
endMin: appointment.endMin
|
|
644
|
+
},
|
|
645
|
+
appointment,
|
|
646
|
+
startClientX: event.clientX,
|
|
647
|
+
startClientY: event.clientY,
|
|
648
|
+
width: 0,
|
|
649
|
+
height: 0,
|
|
650
|
+
offsetX: 0,
|
|
651
|
+
offsetY: 0
|
|
652
|
+
};
|
|
653
|
+
},
|
|
654
|
+
[]
|
|
655
|
+
);
|
|
656
|
+
const onGlobalPointerMove = React2.useCallback(
|
|
657
|
+
(event) => {
|
|
658
|
+
latestPointerRef.current = {
|
|
659
|
+
clientX: event.clientX,
|
|
660
|
+
clientY: event.clientY,
|
|
661
|
+
pointerId: event.pointerId
|
|
662
|
+
};
|
|
663
|
+
if (frameRef.current !== null) {
|
|
631
664
|
return;
|
|
632
665
|
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
666
|
+
frameRef.current = window.requestAnimationFrame(() => {
|
|
667
|
+
frameRef.current = null;
|
|
668
|
+
processPointerFrame({
|
|
669
|
+
dayMinutes,
|
|
670
|
+
dragRef,
|
|
671
|
+
latestPointerRef,
|
|
672
|
+
pendingDragRef,
|
|
673
|
+
pointerToMin,
|
|
674
|
+
pointerToResourceId,
|
|
675
|
+
setDrag,
|
|
676
|
+
setMovePreview,
|
|
677
|
+
suppressClickRef
|
|
640
678
|
});
|
|
641
|
-
}
|
|
679
|
+
});
|
|
642
680
|
},
|
|
643
|
-
[dayMinutes,
|
|
681
|
+
[dayMinutes, pointerToMin, pointerToResourceId]
|
|
644
682
|
);
|
|
645
|
-
const onGlobalPointerUp =
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
683
|
+
const onGlobalPointerUp = React2.useCallback(() => {
|
|
684
|
+
finishDrag({
|
|
685
|
+
dragRef,
|
|
686
|
+
frameRef,
|
|
687
|
+
latestPointerRef,
|
|
688
|
+
pendingDragRef,
|
|
689
|
+
persistDrag,
|
|
690
|
+
setDrag,
|
|
691
|
+
setMovePreview,
|
|
692
|
+
suppressClickRef
|
|
693
|
+
});
|
|
694
|
+
}, [persistDrag]);
|
|
655
695
|
return {
|
|
656
696
|
colRefs,
|
|
657
697
|
drag,
|
|
698
|
+
isDropInvalid,
|
|
699
|
+
movePreview,
|
|
658
700
|
onApptPointerDown,
|
|
659
701
|
onGlobalPointerMove,
|
|
660
702
|
onGlobalPointerUp,
|
|
661
703
|
onResizePointerDown,
|
|
662
704
|
suppressClickRef
|
|
663
705
|
};
|
|
664
|
-
}
|
|
706
|
+
};
|
|
665
707
|
function useSchedulerPresentationData({
|
|
666
708
|
appointmentColorTokenClassMap,
|
|
667
709
|
dayStartAbs,
|
|
@@ -674,13 +716,24 @@ function useSchedulerPresentationData({
|
|
|
674
716
|
resources,
|
|
675
717
|
selectedDate
|
|
676
718
|
}) {
|
|
677
|
-
const effectiveAppts =
|
|
678
|
-
return
|
|
719
|
+
const effectiveAppts = React2.useMemo(() => {
|
|
720
|
+
return buildPresentationAppointments(renderAppts, drag, selectedDate, dayStartAbs);
|
|
679
721
|
}, [dayStartAbs, drag, renderAppts, selectedDate]);
|
|
680
|
-
const
|
|
681
|
-
return buildLaidOutByResource(resources,
|
|
682
|
-
}, [
|
|
683
|
-
const
|
|
722
|
+
const baseLaidOutByResource = React2.useMemo(() => {
|
|
723
|
+
return buildLaidOutByResource(resources, renderAppts);
|
|
724
|
+
}, [renderAppts, resources]);
|
|
725
|
+
const laidOutByResource = React2.useMemo(() => {
|
|
726
|
+
if (drag.kind !== "resize") {
|
|
727
|
+
return baseLaidOutByResource;
|
|
728
|
+
}
|
|
729
|
+
const next = new Map(baseLaidOutByResource);
|
|
730
|
+
const nextAppointmentsForResource = effectiveAppts.filter(
|
|
731
|
+
(appointment) => appointment.resourceId === drag.resourceId
|
|
732
|
+
);
|
|
733
|
+
next.set(drag.resourceId, buildLaidOutAppointments(nextAppointmentsForResource));
|
|
734
|
+
return next;
|
|
735
|
+
}, [baseLaidOutByResource, drag, effectiveAppts]);
|
|
736
|
+
const appointmentBgByResource = React2.useMemo(() => {
|
|
684
737
|
if (!getResourceAppointmentBackground) {
|
|
685
738
|
return /* @__PURE__ */ new Map();
|
|
686
739
|
}
|
|
@@ -691,7 +744,7 @@ function useSchedulerPresentationData({
|
|
|
691
744
|
toKey: (resource) => resource.id
|
|
692
745
|
});
|
|
693
746
|
}, [getResourceAppointmentBackground, resources]);
|
|
694
|
-
const appointmentColorTokenClassByResource =
|
|
747
|
+
const appointmentColorTokenClassByResource = React2.useMemo(() => {
|
|
695
748
|
if (!getResourceAppointmentColorToken) {
|
|
696
749
|
return /* @__PURE__ */ new Map();
|
|
697
750
|
}
|
|
@@ -709,7 +762,7 @@ function useSchedulerPresentationData({
|
|
|
709
762
|
toKey: (resource) => resource.id
|
|
710
763
|
});
|
|
711
764
|
}, [appointmentColorTokenClassMap, getResourceAppointmentColorToken, resources]);
|
|
712
|
-
const appointmentAppearanceByResource =
|
|
765
|
+
const appointmentAppearanceByResource = React2.useMemo(() => {
|
|
713
766
|
return buildAppointmentAppearanceByResource(
|
|
714
767
|
resources,
|
|
715
768
|
resourceAppointmentClassMap,
|
|
@@ -731,6 +784,453 @@ function useSchedulerPresentationData({
|
|
|
731
784
|
laidOutByResource
|
|
732
785
|
};
|
|
733
786
|
}
|
|
787
|
+
function Gutter({ dayMinutes, dayStartAbs, gridHeight }) {
|
|
788
|
+
return /* @__PURE__ */ jsx(
|
|
789
|
+
"div",
|
|
790
|
+
{
|
|
791
|
+
className: "rfs-times border-border bg-card",
|
|
792
|
+
style: { width: GUTTER_W, height: gridHeight },
|
|
793
|
+
children: Array.from({ length: Math.floor(dayMinutes / 60) + 1 }).map((_, i) => /* @__PURE__ */ jsxs(
|
|
794
|
+
"div",
|
|
795
|
+
{
|
|
796
|
+
className: "rfs-time-tick text-muted-foreground",
|
|
797
|
+
style: { top: TOP_PAD + i * 60 * PX_PER_MIN },
|
|
798
|
+
children: [
|
|
799
|
+
String(Math.floor((dayStartAbs + i * 60) / 60)).padStart(2, "0"),
|
|
800
|
+
":",
|
|
801
|
+
String((dayStartAbs + i * 60) % 60).padStart(2, "0")
|
|
802
|
+
]
|
|
803
|
+
},
|
|
804
|
+
`tick-${i}`
|
|
805
|
+
))
|
|
806
|
+
}
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
function defaultRenderResourceHeader({ resource }) {
|
|
810
|
+
return /* @__PURE__ */ jsx("strong", { children: resource.label });
|
|
811
|
+
}
|
|
812
|
+
function Header({ resources, renderResourceHeader }) {
|
|
813
|
+
return /* @__PURE__ */ jsxs("div", { className: "rfs-header border-border bg-card", children: [
|
|
814
|
+
/* @__PURE__ */ jsx(
|
|
815
|
+
"div",
|
|
816
|
+
{
|
|
817
|
+
className: "rfs-gutter border-border",
|
|
818
|
+
style: { width: GUTTER_W }
|
|
819
|
+
}
|
|
820
|
+
),
|
|
821
|
+
resources.map((resource) => /* @__PURE__ */ jsxs(
|
|
822
|
+
"div",
|
|
823
|
+
{
|
|
824
|
+
className: "rfs-resource-header border-border bg-card text-foreground",
|
|
825
|
+
style: { minWidth: RESOURCE_MIN_W, flex: 1 },
|
|
826
|
+
children: [
|
|
827
|
+
(renderResourceHeader ?? ((r) => defaultRenderResourceHeader({ resource: r })))(
|
|
828
|
+
resource
|
|
829
|
+
),
|
|
830
|
+
isNameLike(resource) ? /* @__PURE__ */ jsx("div", { className: "rfs-resource-subtitle text-muted-foreground", children: fullName(resource) }) : null
|
|
831
|
+
]
|
|
832
|
+
},
|
|
833
|
+
String(resource.id)
|
|
834
|
+
))
|
|
835
|
+
] });
|
|
836
|
+
}
|
|
837
|
+
var ResourceColInner = ({
|
|
838
|
+
resource,
|
|
839
|
+
colRefs,
|
|
840
|
+
gridHeight,
|
|
841
|
+
dayMinutes,
|
|
842
|
+
appointments,
|
|
843
|
+
appointmentAppearance,
|
|
844
|
+
appointmentBg,
|
|
845
|
+
isDropInvalid,
|
|
846
|
+
renderAppointment,
|
|
847
|
+
onApptPointerDown,
|
|
848
|
+
onResizePointerDown,
|
|
849
|
+
drag,
|
|
850
|
+
suppressClickRef
|
|
851
|
+
}) => {
|
|
852
|
+
return /* @__PURE__ */ jsx(
|
|
853
|
+
"div",
|
|
854
|
+
{
|
|
855
|
+
className: "rfs-col border-border bg-card",
|
|
856
|
+
style: { minWidth: RESOURCE_MIN_W, flex: 1 },
|
|
857
|
+
children: /* @__PURE__ */ jsxs(
|
|
858
|
+
"div",
|
|
859
|
+
{
|
|
860
|
+
ref: (el) => {
|
|
861
|
+
colRefs.current[String(resource.id)] = el;
|
|
862
|
+
},
|
|
863
|
+
className: "rfs-col-inner bg-card",
|
|
864
|
+
style: { height: gridHeight },
|
|
865
|
+
children: [
|
|
866
|
+
Array.from({ length: Math.floor(dayMinutes / 60) + 1 }).map((_, i) => /* @__PURE__ */ jsx(
|
|
867
|
+
"div",
|
|
868
|
+
{
|
|
869
|
+
className: "rfs-hour-line border-border/70",
|
|
870
|
+
style: { top: TOP_PAD + i * 60 * PX_PER_MIN }
|
|
871
|
+
},
|
|
872
|
+
`major-${i}`
|
|
873
|
+
)),
|
|
874
|
+
Array.from({ length: Math.floor(dayMinutes / STEP_MIN) + 1 }).map((_, i) => /* @__PURE__ */ jsx(
|
|
875
|
+
"div",
|
|
876
|
+
{
|
|
877
|
+
className: "rfs-slot-line border-border/50",
|
|
878
|
+
style: { top: TOP_PAD + i * STEP_MIN * PX_PER_MIN }
|
|
879
|
+
},
|
|
880
|
+
`minor-${i}`
|
|
881
|
+
)),
|
|
882
|
+
appointments.map((appointment) => {
|
|
883
|
+
const laneWidthPct = 100 / appointment.lanes;
|
|
884
|
+
const top = TOP_PAD + appointment.startMin * PX_PER_MIN;
|
|
885
|
+
const height = Math.max(
|
|
886
|
+
MIN_EVENT_MIN * PX_PER_MIN,
|
|
887
|
+
(appointment.endMin - appointment.startMin) * PX_PER_MIN
|
|
888
|
+
);
|
|
889
|
+
return /* @__PURE__ */ jsx(
|
|
890
|
+
"div",
|
|
891
|
+
{
|
|
892
|
+
className: `rfs-card-slot ${appointment.visualState === "ghost" ? "rfs-card-slot--ghost" : ""} ${appointment.visualState === "dragging" ? "rfs-card-slot--dragging" : ""}`,
|
|
893
|
+
style: {
|
|
894
|
+
top,
|
|
895
|
+
left: `${laneWidthPct * appointment.lane}%`,
|
|
896
|
+
width: `${laneWidthPct}%`,
|
|
897
|
+
height
|
|
898
|
+
},
|
|
899
|
+
children: (renderAppointment ?? defaultRenderAppointment)({
|
|
900
|
+
appointment,
|
|
901
|
+
isDropInvalid: isDropInvalid && drag.kind === "resize" && appointment.id === drag.appointmentId,
|
|
902
|
+
onPointerDown: (event) => onApptPointerDown(event, appointment),
|
|
903
|
+
onResizePointerDown: (event) => onResizePointerDown(event, appointment),
|
|
904
|
+
appointmentAppearance,
|
|
905
|
+
appointmentBackgroundColor: appointmentBg,
|
|
906
|
+
drag,
|
|
907
|
+
suppressClickRef
|
|
908
|
+
})
|
|
909
|
+
},
|
|
910
|
+
appointment.renderKey
|
|
911
|
+
);
|
|
912
|
+
})
|
|
913
|
+
]
|
|
914
|
+
}
|
|
915
|
+
)
|
|
916
|
+
}
|
|
917
|
+
);
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
// src/components/scheduler/parts/ResourceCol.tsx
|
|
921
|
+
var areResourceColsEqual = (prev, next) => [
|
|
922
|
+
prev.resource === next.resource,
|
|
923
|
+
prev.colRefs === next.colRefs,
|
|
924
|
+
prev.gridHeight === next.gridHeight,
|
|
925
|
+
prev.dayMinutes === next.dayMinutes,
|
|
926
|
+
prev.appointments === next.appointments,
|
|
927
|
+
prev.appointmentAppearance === next.appointmentAppearance,
|
|
928
|
+
prev.appointmentBg === next.appointmentBg,
|
|
929
|
+
prev.isDropInvalid === next.isDropInvalid,
|
|
930
|
+
prev.renderAppointment === next.renderAppointment,
|
|
931
|
+
prev.onApptPointerDown === next.onApptPointerDown,
|
|
932
|
+
prev.onResizePointerDown === next.onResizePointerDown,
|
|
933
|
+
prev.drag === next.drag,
|
|
934
|
+
prev.suppressClickRef === next.suppressClickRef
|
|
935
|
+
].every(Boolean);
|
|
936
|
+
var ResourceCol = React2.memo(
|
|
937
|
+
ResourceColInner,
|
|
938
|
+
areResourceColsEqual
|
|
939
|
+
);
|
|
940
|
+
|
|
941
|
+
// src/lib/cn.ts
|
|
942
|
+
function cn(...inputs) {
|
|
943
|
+
return inputs.filter(Boolean).join(" ");
|
|
944
|
+
}
|
|
945
|
+
var variantClasses = {
|
|
946
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
947
|
+
outline: "border border-border bg-card text-foreground hover:bg-muted",
|
|
948
|
+
ghost: "text-foreground hover:bg-muted"
|
|
949
|
+
};
|
|
950
|
+
var sizeClasses = {
|
|
951
|
+
default: "h-9 px-4 py-2",
|
|
952
|
+
sm: "h-8 px-3 text-xs",
|
|
953
|
+
icon: "h-8 w-8"
|
|
954
|
+
};
|
|
955
|
+
var Button = React2.forwardRef(function Button2({ className, variant = "default", size = "default", type = "button", ...props }, ref) {
|
|
956
|
+
return /* @__PURE__ */ jsx(
|
|
957
|
+
"button",
|
|
958
|
+
{
|
|
959
|
+
ref,
|
|
960
|
+
type,
|
|
961
|
+
className: cn(
|
|
962
|
+
"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",
|
|
963
|
+
variantClasses[variant],
|
|
964
|
+
sizeClasses[size],
|
|
965
|
+
className
|
|
966
|
+
),
|
|
967
|
+
...props
|
|
968
|
+
}
|
|
969
|
+
);
|
|
970
|
+
});
|
|
971
|
+
function Calendar({
|
|
972
|
+
selected,
|
|
973
|
+
onSelect
|
|
974
|
+
}) {
|
|
975
|
+
const [month, setMonth] = React2.useState(selected ?? /* @__PURE__ */ new Date());
|
|
976
|
+
React2.useEffect(() => {
|
|
977
|
+
if (selected) {
|
|
978
|
+
setMonth(selected);
|
|
979
|
+
}
|
|
980
|
+
}, [selected]);
|
|
981
|
+
const monthStart = startOfMonth(month);
|
|
982
|
+
const calendarStart = startOfWeek(monthStart, { weekStartsOn: 1 });
|
|
983
|
+
const calendarEnd = endOfWeek(endOfMonth(month), { weekStartsOn: 1 });
|
|
984
|
+
const days = [];
|
|
985
|
+
for (let day = calendarStart; day <= calendarEnd; day = addDays(day, 1)) {
|
|
986
|
+
days.push(day);
|
|
987
|
+
}
|
|
988
|
+
return /* @__PURE__ */ jsxs("div", { className: "w-72 rounded-md bg-popover", children: [
|
|
989
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between", children: [
|
|
990
|
+
/* @__PURE__ */ jsx(
|
|
991
|
+
Button,
|
|
992
|
+
{
|
|
993
|
+
variant: "ghost",
|
|
994
|
+
size: "icon",
|
|
995
|
+
"aria-label": "Previous month",
|
|
996
|
+
onClick: () => setMonth(addMonths(month, -1)),
|
|
997
|
+
children: "<"
|
|
998
|
+
}
|
|
999
|
+
),
|
|
1000
|
+
/* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: format(month, "MMMM yyyy") }),
|
|
1001
|
+
/* @__PURE__ */ jsx(
|
|
1002
|
+
Button,
|
|
1003
|
+
{
|
|
1004
|
+
variant: "ghost",
|
|
1005
|
+
size: "icon",
|
|
1006
|
+
"aria-label": "Next month",
|
|
1007
|
+
onClick: () => setMonth(addMonths(month, 1)),
|
|
1008
|
+
children: ">"
|
|
1009
|
+
}
|
|
1010
|
+
)
|
|
1011
|
+
] }),
|
|
1012
|
+
/* @__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(
|
|
1013
|
+
"div",
|
|
1014
|
+
{
|
|
1015
|
+
className: "py-1",
|
|
1016
|
+
children: label
|
|
1017
|
+
},
|
|
1018
|
+
label
|
|
1019
|
+
)) }),
|
|
1020
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-7 gap-1", children: days.map((day) => {
|
|
1021
|
+
const isSelected = selected ? isSameDay(day, selected) : false;
|
|
1022
|
+
return /* @__PURE__ */ jsx(
|
|
1023
|
+
Button,
|
|
1024
|
+
{
|
|
1025
|
+
variant: isSelected ? "default" : "ghost",
|
|
1026
|
+
size: "icon",
|
|
1027
|
+
className: cn(
|
|
1028
|
+
"text-xs",
|
|
1029
|
+
!isSameMonth(day, month) && "text-muted-foreground opacity-60",
|
|
1030
|
+
isToday(day) && !isSelected && "border border-border"
|
|
1031
|
+
),
|
|
1032
|
+
onClick: () => onSelect?.(day),
|
|
1033
|
+
children: format(day, "d")
|
|
1034
|
+
},
|
|
1035
|
+
day.toISOString()
|
|
1036
|
+
);
|
|
1037
|
+
}) })
|
|
1038
|
+
] });
|
|
1039
|
+
}
|
|
1040
|
+
var PopoverContext = React2.createContext(null);
|
|
1041
|
+
function usePopoverContext() {
|
|
1042
|
+
const context = React2.useContext(PopoverContext);
|
|
1043
|
+
if (!context) {
|
|
1044
|
+
throw new Error("Popover components must be used within Popover.");
|
|
1045
|
+
}
|
|
1046
|
+
return context;
|
|
1047
|
+
}
|
|
1048
|
+
function Popover({
|
|
1049
|
+
children,
|
|
1050
|
+
open: controlledOpen,
|
|
1051
|
+
onOpenChange
|
|
1052
|
+
}) {
|
|
1053
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React2.useState(false);
|
|
1054
|
+
const triggerRef = React2.useRef(null);
|
|
1055
|
+
const contentRef = React2.useRef(null);
|
|
1056
|
+
const open = controlledOpen ?? uncontrolledOpen;
|
|
1057
|
+
const setOpen = React2.useCallback(
|
|
1058
|
+
(value) => {
|
|
1059
|
+
const nextValue = typeof value === "function" ? value(open) : value;
|
|
1060
|
+
if (controlledOpen === void 0) {
|
|
1061
|
+
setUncontrolledOpen(nextValue);
|
|
1062
|
+
}
|
|
1063
|
+
onOpenChange?.(nextValue);
|
|
1064
|
+
},
|
|
1065
|
+
[controlledOpen, onOpenChange, open]
|
|
1066
|
+
);
|
|
1067
|
+
React2.useEffect(() => {
|
|
1068
|
+
if (!open) {
|
|
1069
|
+
return void 0;
|
|
1070
|
+
}
|
|
1071
|
+
const handlePointerDown = (event) => {
|
|
1072
|
+
const target = event.target;
|
|
1073
|
+
if (triggerRef.current?.contains(target) || contentRef.current?.contains(target)) {
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
setOpen(false);
|
|
1077
|
+
};
|
|
1078
|
+
const handleKeyDown = (event) => {
|
|
1079
|
+
if (event.key === "Escape") {
|
|
1080
|
+
setOpen(false);
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
document.addEventListener("mousedown", handlePointerDown);
|
|
1084
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
1085
|
+
return () => {
|
|
1086
|
+
document.removeEventListener("mousedown", handlePointerDown);
|
|
1087
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
1088
|
+
};
|
|
1089
|
+
}, [open, setOpen]);
|
|
1090
|
+
return /* @__PURE__ */ jsx(PopoverContext.Provider, { value: { open, setOpen, triggerRef, contentRef }, children: /* @__PURE__ */ jsx("div", { className: "relative inline-flex", children }) });
|
|
1091
|
+
}
|
|
1092
|
+
function PopoverTrigger({
|
|
1093
|
+
children,
|
|
1094
|
+
asChild = false
|
|
1095
|
+
}) {
|
|
1096
|
+
const { open, setOpen, triggerRef } = usePopoverContext();
|
|
1097
|
+
if (asChild && React2.isValidElement(children)) {
|
|
1098
|
+
const child = children;
|
|
1099
|
+
return React2.cloneElement(child, {
|
|
1100
|
+
ref: triggerRef,
|
|
1101
|
+
"aria-expanded": open,
|
|
1102
|
+
onClick: (event) => {
|
|
1103
|
+
child.props.onClick?.(event);
|
|
1104
|
+
setOpen((current) => !current);
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
return /* @__PURE__ */ jsx(
|
|
1109
|
+
"button",
|
|
1110
|
+
{
|
|
1111
|
+
ref: triggerRef,
|
|
1112
|
+
type: "button",
|
|
1113
|
+
"aria-expanded": open,
|
|
1114
|
+
onClick: () => setOpen((current) => !current),
|
|
1115
|
+
children
|
|
1116
|
+
}
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
function PopoverContent({
|
|
1120
|
+
children,
|
|
1121
|
+
className,
|
|
1122
|
+
align = "start"
|
|
1123
|
+
}) {
|
|
1124
|
+
const { open, contentRef } = usePopoverContext();
|
|
1125
|
+
if (!open) {
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
1128
|
+
return /* @__PURE__ */ jsx(
|
|
1129
|
+
"div",
|
|
1130
|
+
{
|
|
1131
|
+
ref: contentRef,
|
|
1132
|
+
className: cn(
|
|
1133
|
+
"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",
|
|
1134
|
+
align === "start" && "left-0",
|
|
1135
|
+
align === "center" && "left-1/2 -translate-x-1/2",
|
|
1136
|
+
align === "end" && "right-0",
|
|
1137
|
+
className
|
|
1138
|
+
),
|
|
1139
|
+
children
|
|
1140
|
+
}
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
function defaultRenderDatePicker({
|
|
1144
|
+
selectedDate,
|
|
1145
|
+
onSelectedDateChange
|
|
1146
|
+
}) {
|
|
1147
|
+
return /* @__PURE__ */ jsxs(Popover, { children: [
|
|
1148
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
1149
|
+
Button,
|
|
1150
|
+
{
|
|
1151
|
+
variant: "outline",
|
|
1152
|
+
size: "sm",
|
|
1153
|
+
className: "min-w-40 justify-start text-left font-normal",
|
|
1154
|
+
children: format(selectedDate, "PPP")
|
|
1155
|
+
}
|
|
1156
|
+
) }),
|
|
1157
|
+
/* @__PURE__ */ jsx(
|
|
1158
|
+
PopoverContent,
|
|
1159
|
+
{
|
|
1160
|
+
className: "w-auto p-2",
|
|
1161
|
+
align: "center",
|
|
1162
|
+
children: /* @__PURE__ */ jsx(
|
|
1163
|
+
Calendar,
|
|
1164
|
+
{
|
|
1165
|
+
selected: selectedDate,
|
|
1166
|
+
onSelect: onSelectedDateChange
|
|
1167
|
+
}
|
|
1168
|
+
)
|
|
1169
|
+
}
|
|
1170
|
+
)
|
|
1171
|
+
] });
|
|
1172
|
+
}
|
|
1173
|
+
function defaultRenderToolbar({
|
|
1174
|
+
goToPreviousDay,
|
|
1175
|
+
goToNextDay,
|
|
1176
|
+
defaultDatePicker,
|
|
1177
|
+
prevButtonLabel = "Prev",
|
|
1178
|
+
nextButtonLabel = "Next"
|
|
1179
|
+
}) {
|
|
1180
|
+
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: [
|
|
1181
|
+
/* @__PURE__ */ jsx(
|
|
1182
|
+
Button,
|
|
1183
|
+
{
|
|
1184
|
+
variant: "outline",
|
|
1185
|
+
size: "sm",
|
|
1186
|
+
onClick: goToPreviousDay,
|
|
1187
|
+
children: prevButtonLabel
|
|
1188
|
+
}
|
|
1189
|
+
),
|
|
1190
|
+
defaultDatePicker,
|
|
1191
|
+
/* @__PURE__ */ jsx(
|
|
1192
|
+
Button,
|
|
1193
|
+
{
|
|
1194
|
+
variant: "outline",
|
|
1195
|
+
size: "sm",
|
|
1196
|
+
onClick: goToNextDay,
|
|
1197
|
+
children: nextButtonLabel
|
|
1198
|
+
}
|
|
1199
|
+
)
|
|
1200
|
+
] }) });
|
|
1201
|
+
}
|
|
1202
|
+
function Toolbar({
|
|
1203
|
+
selectedDate,
|
|
1204
|
+
onSelectedDateChange,
|
|
1205
|
+
prevButtonLabel,
|
|
1206
|
+
nextButtonLabel,
|
|
1207
|
+
renderToolbar,
|
|
1208
|
+
renderDatePicker
|
|
1209
|
+
}) {
|
|
1210
|
+
const goToPreviousDay = () => onSelectedDateChange(shiftDays(selectedDate, -1));
|
|
1211
|
+
const goToNextDay = () => onSelectedDateChange(shiftDays(selectedDate, 1));
|
|
1212
|
+
const defaultDatePicker = (renderDatePicker ?? defaultRenderDatePicker)({
|
|
1213
|
+
selectedDate,
|
|
1214
|
+
onSelectedDateChange
|
|
1215
|
+
});
|
|
1216
|
+
if (renderToolbar) {
|
|
1217
|
+
return /* @__PURE__ */ jsx(Fragment, { children: renderToolbar({
|
|
1218
|
+
selectedDate,
|
|
1219
|
+
onSelectedDateChange,
|
|
1220
|
+
goToPreviousDay,
|
|
1221
|
+
goToNextDay,
|
|
1222
|
+
defaultDatePicker
|
|
1223
|
+
}) });
|
|
1224
|
+
}
|
|
1225
|
+
return /* @__PURE__ */ jsx(Fragment, { children: defaultRenderToolbar({
|
|
1226
|
+
goToPreviousDay,
|
|
1227
|
+
goToNextDay,
|
|
1228
|
+
defaultDatePicker,
|
|
1229
|
+
prevButtonLabel,
|
|
1230
|
+
nextButtonLabel
|
|
1231
|
+
}) });
|
|
1232
|
+
}
|
|
1233
|
+
var IDLE_DRAG = { kind: "none" };
|
|
734
1234
|
function Scheduler({
|
|
735
1235
|
resources,
|
|
736
1236
|
appointments,
|
|
@@ -753,24 +1253,12 @@ function Scheduler({
|
|
|
753
1253
|
dayStart = "09:00",
|
|
754
1254
|
dayEnd = "18:00"
|
|
755
1255
|
}) {
|
|
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
|
-
});
|
|
1256
|
+
const { appointmentMap, dayMinutes, dayStartAbs, gridHeight, gridMinWidth, renderAppts } = useSchedulerBaseData({ adapter, appointments, dayEnd, dayStart, resources, selectedDate });
|
|
771
1257
|
const {
|
|
772
1258
|
colRefs,
|
|
773
1259
|
drag,
|
|
1260
|
+
isDropInvalid,
|
|
1261
|
+
movePreview,
|
|
774
1262
|
onApptPointerDown,
|
|
775
1263
|
onGlobalPointerMove,
|
|
776
1264
|
onGlobalPointerUp,
|
|
@@ -786,11 +1274,7 @@ function Scheduler({
|
|
|
786
1274
|
resources,
|
|
787
1275
|
selectedDate
|
|
788
1276
|
});
|
|
789
|
-
const {
|
|
790
|
-
appointmentAppearanceByResource,
|
|
791
|
-
appointmentBgByResource,
|
|
792
|
-
laidOutByResource
|
|
793
|
-
} = useSchedulerPresentationData({
|
|
1277
|
+
const { appointmentAppearanceByResource, appointmentBgByResource, laidOutByResource } = useSchedulerPresentationData({
|
|
794
1278
|
appointmentColorTokenClassMap,
|
|
795
1279
|
dayStartAbs,
|
|
796
1280
|
drag,
|
|
@@ -802,23 +1286,29 @@ function Scheduler({
|
|
|
802
1286
|
resources,
|
|
803
1287
|
selectedDate
|
|
804
1288
|
});
|
|
805
|
-
const
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1289
|
+
const moveOverlay = React2.useMemo(() => {
|
|
1290
|
+
if (drag.kind !== "move" || !movePreview) {
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
return {
|
|
1294
|
+
appointment: {
|
|
1295
|
+
...movePreview.appointment,
|
|
1296
|
+
resourceId: drag.resourceId,
|
|
1297
|
+
startMin: drag.startMin,
|
|
1298
|
+
endMin: drag.endMin,
|
|
1299
|
+
visualState: "ghost",
|
|
1300
|
+
renderKey: `${movePreview.appointment.renderKey}-overlay`,
|
|
1301
|
+
lane: 0,
|
|
1302
|
+
lanes: 1
|
|
1303
|
+
},
|
|
1304
|
+
appearance: appointmentAppearanceByResource.get(drag.resourceId),
|
|
1305
|
+
background: appointmentBgByResource.get(drag.resourceId),
|
|
1306
|
+
left: movePreview.clientX - movePreview.offsetX,
|
|
1307
|
+
top: movePreview.clientY - movePreview.offsetY,
|
|
1308
|
+
width: movePreview.width,
|
|
1309
|
+
height: movePreview.height
|
|
1310
|
+
};
|
|
1311
|
+
}, [appointmentAppearanceByResource, appointmentBgByResource, drag, movePreview]);
|
|
822
1312
|
return /* @__PURE__ */ jsxs(
|
|
823
1313
|
"section",
|
|
824
1314
|
{
|
|
@@ -827,104 +1317,77 @@ function Scheduler({
|
|
|
827
1317
|
onPointerUp: onGlobalPointerUp,
|
|
828
1318
|
onPointerCancel: onGlobalPointerUp,
|
|
829
1319
|
children: [
|
|
830
|
-
|
|
1320
|
+
/* @__PURE__ */ jsx(
|
|
1321
|
+
Toolbar,
|
|
1322
|
+
{
|
|
1323
|
+
selectedDate,
|
|
1324
|
+
onSelectedDateChange,
|
|
1325
|
+
prevButtonLabel,
|
|
1326
|
+
nextButtonLabel,
|
|
1327
|
+
renderToolbar,
|
|
1328
|
+
renderDatePicker
|
|
1329
|
+
}
|
|
1330
|
+
),
|
|
831
1331
|
/* @__PURE__ */ jsx("div", { className: "rfs-shell border-border bg-card", children: /* @__PURE__ */ jsxs("div", { style: { minWidth: gridMinWidth }, children: [
|
|
832
|
-
/* @__PURE__ */
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
children: [
|
|
840
|
-
(renderResourceHeader ?? defaultResourceHeaderRenderer)(resource),
|
|
841
|
-
isNameLike(resource) ? /* @__PURE__ */ jsx("div", { className: "rfs-resource-subtitle text-muted-foreground", children: fullName(resource) }) : null
|
|
842
|
-
]
|
|
843
|
-
},
|
|
844
|
-
String(resource.id)
|
|
845
|
-
))
|
|
846
|
-
] }),
|
|
1332
|
+
/* @__PURE__ */ jsx(
|
|
1333
|
+
Header,
|
|
1334
|
+
{
|
|
1335
|
+
resources,
|
|
1336
|
+
renderResourceHeader
|
|
1337
|
+
}
|
|
1338
|
+
),
|
|
847
1339
|
/* @__PURE__ */ jsxs("div", { className: "rfs-body", children: [
|
|
848
|
-
/* @__PURE__ */ jsx(
|
|
849
|
-
|
|
1340
|
+
/* @__PURE__ */ jsx(
|
|
1341
|
+
Gutter,
|
|
850
1342
|
{
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
String((dayStartAbs + i * 60) % 60).padStart(2, "0")
|
|
857
|
-
]
|
|
858
|
-
},
|
|
859
|
-
`tick-${i}`
|
|
860
|
-
)) }),
|
|
1343
|
+
dayMinutes,
|
|
1344
|
+
dayStartAbs,
|
|
1345
|
+
gridHeight
|
|
1346
|
+
}
|
|
1347
|
+
),
|
|
861
1348
|
/* @__PURE__ */ jsx("div", { className: "rfs-grid", children: resources.map((resource) => /* @__PURE__ */ jsx(
|
|
862
|
-
|
|
1349
|
+
ResourceCol,
|
|
863
1350
|
{
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
{
|
|
878
|
-
className: "rfs-hour-line border-border/70",
|
|
879
|
-
style: { top: TOP_PAD + i * 60 * PX_PER_MIN }
|
|
880
|
-
},
|
|
881
|
-
`major-${String(resource.id)}-${i}`
|
|
882
|
-
)),
|
|
883
|
-
Array.from({ length: Math.floor(dayMinutes / STEP_MIN) + 1 }).map((_, i) => /* @__PURE__ */ jsx(
|
|
884
|
-
"div",
|
|
885
|
-
{
|
|
886
|
-
className: "rfs-slot-line border-border/50",
|
|
887
|
-
style: { top: TOP_PAD + i * STEP_MIN * PX_PER_MIN }
|
|
888
|
-
},
|
|
889
|
-
`minor-${String(resource.id)}-${i}`
|
|
890
|
-
)),
|
|
891
|
-
(laidOutByResource.get(resource.id) ?? []).map((appointment) => {
|
|
892
|
-
const laneWidthPct = 100 / appointment.lanes;
|
|
893
|
-
const top = TOP_PAD + appointment.startMin * PX_PER_MIN;
|
|
894
|
-
const height = Math.max(MIN_EVENT_MIN * PX_PER_MIN, (appointment.endMin - appointment.startMin) * PX_PER_MIN);
|
|
895
|
-
const appointmentAppearance = appointmentAppearanceByResource.get(resource.id);
|
|
896
|
-
const appointmentBackgroundColor = appointmentBgByResource.get(resource.id);
|
|
897
|
-
return /* @__PURE__ */ jsx(
|
|
898
|
-
"div",
|
|
899
|
-
{
|
|
900
|
-
className: "rfs-card-slot",
|
|
901
|
-
style: {
|
|
902
|
-
top,
|
|
903
|
-
left: `${laneWidthPct * appointment.lane}%`,
|
|
904
|
-
width: `${laneWidthPct}%`,
|
|
905
|
-
height
|
|
906
|
-
},
|
|
907
|
-
children: (renderAppointment ?? defaultRenderAppointment)({
|
|
908
|
-
appointment,
|
|
909
|
-
onPointerDown: (event) => onApptPointerDown(event, appointment),
|
|
910
|
-
onResizePointerDown: (event) => onResizePointerDown(event, appointment),
|
|
911
|
-
appointmentAppearance,
|
|
912
|
-
appointmentBackgroundColor,
|
|
913
|
-
drag,
|
|
914
|
-
suppressClickRef
|
|
915
|
-
})
|
|
916
|
-
},
|
|
917
|
-
String(appointment.id)
|
|
918
|
-
);
|
|
919
|
-
})
|
|
920
|
-
]
|
|
921
|
-
}
|
|
922
|
-
)
|
|
1351
|
+
resource,
|
|
1352
|
+
colRefs,
|
|
1353
|
+
gridHeight,
|
|
1354
|
+
dayMinutes,
|
|
1355
|
+
appointments: laidOutByResource.get(resource.id) ?? [],
|
|
1356
|
+
appointmentAppearance: appointmentAppearanceByResource.get(resource.id),
|
|
1357
|
+
appointmentBg: appointmentBgByResource.get(resource.id),
|
|
1358
|
+
isDropInvalid,
|
|
1359
|
+
renderAppointment,
|
|
1360
|
+
onApptPointerDown,
|
|
1361
|
+
onResizePointerDown,
|
|
1362
|
+
drag: drag.kind === "resize" && drag.resourceId === resource.id ? drag : IDLE_DRAG,
|
|
1363
|
+
suppressClickRef
|
|
923
1364
|
},
|
|
924
1365
|
String(resource.id)
|
|
925
1366
|
)) })
|
|
926
1367
|
] })
|
|
927
|
-
] }) })
|
|
1368
|
+
] }) }),
|
|
1369
|
+
moveOverlay ? /* @__PURE__ */ jsx(
|
|
1370
|
+
"div",
|
|
1371
|
+
{
|
|
1372
|
+
className: "rfs-drag-overlay",
|
|
1373
|
+
style: {
|
|
1374
|
+
left: moveOverlay.left,
|
|
1375
|
+
top: moveOverlay.top,
|
|
1376
|
+
width: moveOverlay.width,
|
|
1377
|
+
height: moveOverlay.height
|
|
1378
|
+
},
|
|
1379
|
+
children: (renderAppointment ?? defaultRenderAppointment)({
|
|
1380
|
+
appointment: moveOverlay.appointment,
|
|
1381
|
+
isDropInvalid,
|
|
1382
|
+
onPointerDown: () => void 0,
|
|
1383
|
+
onResizePointerDown: () => void 0,
|
|
1384
|
+
appointmentAppearance: moveOverlay.appearance,
|
|
1385
|
+
appointmentBackgroundColor: moveOverlay.background,
|
|
1386
|
+
drag,
|
|
1387
|
+
suppressClickRef
|
|
1388
|
+
})
|
|
1389
|
+
}
|
|
1390
|
+
) : null
|
|
928
1391
|
]
|
|
929
1392
|
}
|
|
930
1393
|
);
|