@marco.colia/react-fast-scheduler 2.1.0 → 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 +144 -67
- package/dist/index.cjs +883 -261
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -3
- package/dist/index.d.ts +17 -3
- package/dist/index.js +885 -263
- 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 { jsxs,
|
|
2
|
-
import { format } from 'date-fns';
|
|
1
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import { format, startOfMonth, startOfWeek, endOfWeek, endOfMonth, addDays, addMonths, isSameDay, isToday, isSameMonth } from 'date-fns';
|
|
3
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,23 +15,36 @@ 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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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(
|
|
22
|
+
"div",
|
|
23
|
+
{
|
|
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
|
+
]
|
|
46
|
+
}
|
|
47
|
+
);
|
|
34
48
|
}
|
|
35
49
|
|
|
36
50
|
// src/components/constants.ts
|
|
@@ -41,8 +55,6 @@ var GUTTER_W = 72;
|
|
|
41
55
|
var TOP_PAD = 12;
|
|
42
56
|
var BOTTOM_PAD = 24;
|
|
43
57
|
var RESOURCE_MIN_W = 420;
|
|
44
|
-
|
|
45
|
-
// src/utils/scheduler-core.utils.ts
|
|
46
58
|
function isNameLike(resource) {
|
|
47
59
|
if (!resource || typeof resource !== "object") {
|
|
48
60
|
return false;
|
|
@@ -50,22 +62,6 @@ function isNameLike(resource) {
|
|
|
50
62
|
const maybe = resource;
|
|
51
63
|
return typeof maybe.firstName === "string" || typeof maybe.lastName === "string";
|
|
52
64
|
}
|
|
53
|
-
function toInputDateValue(date) {
|
|
54
|
-
const year = date.getFullYear();
|
|
55
|
-
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
56
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
57
|
-
return `${year}-${month}-${day}`;
|
|
58
|
-
}
|
|
59
|
-
function fromInputDateValue(value) {
|
|
60
|
-
const [yearRaw, monthRaw, dayRaw] = value.split("-");
|
|
61
|
-
const year = Number(yearRaw);
|
|
62
|
-
const month = Number(monthRaw);
|
|
63
|
-
const day = Number(dayRaw);
|
|
64
|
-
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
return new Date(year, month - 1, day);
|
|
68
|
-
}
|
|
69
65
|
var DEFAULT_APPOINTMENT_COLOR_TOKEN_CLASS_MAP = {
|
|
70
66
|
slate: "bg-slate-100 dark:bg-slate-900/40",
|
|
71
67
|
gray: "bg-gray-100 dark:bg-gray-900/40",
|
|
@@ -176,26 +172,6 @@ function layoutOverlaps(blocks) {
|
|
|
176
172
|
}
|
|
177
173
|
return output;
|
|
178
174
|
}
|
|
179
|
-
function defaultRenderDatePicker({ selectedDate, onSelectedDateChange }) {
|
|
180
|
-
return /* @__PURE__ */ jsx(
|
|
181
|
-
"input",
|
|
182
|
-
{
|
|
183
|
-
type: "date",
|
|
184
|
-
value: toInputDateValue(selectedDate),
|
|
185
|
-
onChange: (event) => {
|
|
186
|
-
const nextDate = fromInputDateValue(event.target.value);
|
|
187
|
-
if (nextDate) {
|
|
188
|
-
onSelectedDateChange(nextDate);
|
|
189
|
-
}
|
|
190
|
-
},
|
|
191
|
-
className: "h-7 rounded-md border border-border bg-card px-2 text-xs text-foreground",
|
|
192
|
-
"aria-label": "Select date"
|
|
193
|
-
}
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
function defaultRenderResourceHeader({ resource }) {
|
|
197
|
-
return /* @__PURE__ */ jsx("strong", { children: resource.label });
|
|
198
|
-
}
|
|
199
175
|
|
|
200
176
|
// src/utils/scheduler-data.utils.ts
|
|
201
177
|
function buildResourceMap({
|
|
@@ -228,17 +204,22 @@ function normalizeAppointments({
|
|
|
228
204
|
end,
|
|
229
205
|
startMin: clamp(minutesFromDayStart(dayStartAbs, start), 0, dayMinutes),
|
|
230
206
|
endMin: clamp(minutesFromDayStart(dayStartAbs, end), 0, dayMinutes),
|
|
231
|
-
title: adapter.getTitle(appointment)
|
|
207
|
+
title: adapter.getTitle(appointment),
|
|
208
|
+
renderKey: String(adapter.getId(appointment)),
|
|
209
|
+
visualState: "normal"
|
|
232
210
|
};
|
|
233
211
|
}).filter((appointment) => dayISO(appointment.start) === selectedISO);
|
|
234
212
|
}
|
|
235
213
|
function buildAppointmentMap(appointments) {
|
|
236
214
|
return new Map(appointments.map((appointment) => [appointment.id, appointment]));
|
|
237
215
|
}
|
|
238
|
-
function
|
|
216
|
+
function buildPresentationAppointments(appointments, drag, selectedDate, dayStartAbs) {
|
|
239
217
|
if (drag.kind === "none") {
|
|
240
218
|
return appointments;
|
|
241
219
|
}
|
|
220
|
+
if (drag.kind === "move") {
|
|
221
|
+
return appointments;
|
|
222
|
+
}
|
|
242
223
|
return appointments.map((appointment) => {
|
|
243
224
|
if (appointment.id !== drag.appointmentId) {
|
|
244
225
|
return appointment;
|
|
@@ -249,7 +230,8 @@ function applyDragToAppointments(appointments, drag, selectedDate, dayStartAbs)
|
|
|
249
230
|
startMin: drag.startMin,
|
|
250
231
|
endMin: drag.endMin,
|
|
251
232
|
start: dateAtMinute(selectedDate, dayStartAbs, drag.startMin),
|
|
252
|
-
end: dateAtMinute(selectedDate, dayStartAbs, drag.endMin)
|
|
233
|
+
end: dateAtMinute(selectedDate, dayStartAbs, drag.endMin),
|
|
234
|
+
visualState: "dragging"
|
|
253
235
|
};
|
|
254
236
|
});
|
|
255
237
|
}
|
|
@@ -262,10 +244,22 @@ function buildLaidOutByResource(resources, appointments) {
|
|
|
262
244
|
}
|
|
263
245
|
const result = /* @__PURE__ */ new Map();
|
|
264
246
|
for (const resource of resources) {
|
|
265
|
-
result.set(resource.id,
|
|
247
|
+
result.set(resource.id, buildLaidOutAppointments(appointmentsByResource.get(resource.id) ?? []));
|
|
266
248
|
}
|
|
267
249
|
return result;
|
|
268
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
|
+
}
|
|
269
263
|
function buildAppointmentAppearanceByResource(resources, resourceAppointmentClassMap, getResourceAppointmentAppearance, appointmentColorTokenClassByResource, appointmentBgByResource) {
|
|
270
264
|
const map = /* @__PURE__ */ new Map();
|
|
271
265
|
for (const resource of resources) {
|
|
@@ -289,8 +283,6 @@ function buildAppointmentAppearanceByResource(resources, resourceAppointmentClas
|
|
|
289
283
|
}
|
|
290
284
|
return map;
|
|
291
285
|
}
|
|
292
|
-
|
|
293
|
-
// src/hooks/useSchedulerBaseData.ts
|
|
294
286
|
function useSchedulerBaseData({
|
|
295
287
|
adapter,
|
|
296
288
|
appointments,
|
|
@@ -301,9 +293,18 @@ function useSchedulerBaseData({
|
|
|
301
293
|
}) {
|
|
302
294
|
const dayStartAbs = React2.useMemo(() => parseHHMM(dayStart), [dayStart]);
|
|
303
295
|
const dayEndAbs = React2.useMemo(() => parseHHMM(dayEnd), [dayEnd]);
|
|
304
|
-
const dayMinutes = React2.useMemo(
|
|
305
|
-
|
|
306
|
-
|
|
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
|
+
);
|
|
307
308
|
const selectedISO = React2.useMemo(() => dayISO(selectedDate), [selectedDate]);
|
|
308
309
|
const renderAppts = React2.useMemo(() => {
|
|
309
310
|
return normalizeAppointments({
|
|
@@ -325,7 +326,157 @@ function useSchedulerBaseData({
|
|
|
325
326
|
renderAppts
|
|
326
327
|
};
|
|
327
328
|
}
|
|
328
|
-
|
|
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 = ({
|
|
329
480
|
appointmentMap,
|
|
330
481
|
dayMinutes,
|
|
331
482
|
dayStartAbs,
|
|
@@ -334,10 +485,27 @@ function useSchedulerInteractions({
|
|
|
334
485
|
renderAppts,
|
|
335
486
|
resources,
|
|
336
487
|
selectedDate
|
|
337
|
-
}) {
|
|
488
|
+
}) => {
|
|
338
489
|
const [drag, setDrag] = React2.useState({ kind: "none" });
|
|
490
|
+
const [movePreview, setMovePreview] = React2.useState(
|
|
491
|
+
null
|
|
492
|
+
);
|
|
339
493
|
const colRefs = React2.useRef({});
|
|
340
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
|
+
}, []);
|
|
341
509
|
const pointerToMin = React2.useCallback(
|
|
342
510
|
(resourceId, clientY) => {
|
|
343
511
|
const col = colRefs.current[String(resourceId)];
|
|
@@ -367,11 +535,21 @@ function useSchedulerInteractions({
|
|
|
367
535
|
);
|
|
368
536
|
const hasOverlap = React2.useCallback(
|
|
369
537
|
(appointmentId, resourceId, startMin, endMin) => {
|
|
370
|
-
const events = renderAppts.filter(
|
|
371
|
-
|
|
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
|
+
);
|
|
372
544
|
},
|
|
373
545
|
[renderAppts]
|
|
374
546
|
);
|
|
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]);
|
|
375
553
|
const persistDrag = React2.useCallback(
|
|
376
554
|
async (state) => {
|
|
377
555
|
const target = appointmentMap.get(state.appointmentId);
|
|
@@ -408,7 +586,14 @@ function useSchedulerInteractions({
|
|
|
408
586
|
});
|
|
409
587
|
}
|
|
410
588
|
},
|
|
411
|
-
[
|
|
589
|
+
[
|
|
590
|
+
appointmentMap,
|
|
591
|
+
dayStartAbs,
|
|
592
|
+
hasOverlap,
|
|
593
|
+
onAppointmentChange,
|
|
594
|
+
onPersistMoveResize,
|
|
595
|
+
selectedDate
|
|
596
|
+
]
|
|
412
597
|
);
|
|
413
598
|
const onApptPointerDown = React2.useCallback(
|
|
414
599
|
(event, appointment) => {
|
|
@@ -417,89 +602,108 @@ function useSchedulerInteractions({
|
|
|
417
602
|
}
|
|
418
603
|
event.stopPropagation();
|
|
419
604
|
suppressClickRef.current = false;
|
|
605
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
420
606
|
const pointerMin = pointerToMin(appointment.resourceId, event.clientY);
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
+
};
|
|
431
626
|
},
|
|
432
627
|
[pointerToMin]
|
|
433
628
|
);
|
|
434
|
-
const onResizePointerDown = React2.useCallback(
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
}
|
|
438
|
-
event.stopPropagation();
|
|
439
|
-
suppressClickRef.current = false;
|
|
440
|
-
setDrag({
|
|
441
|
-
kind: "resize",
|
|
442
|
-
appointmentId: appointment.id,
|
|
443
|
-
pointerId: event.pointerId,
|
|
444
|
-
resourceId: appointment.resourceId,
|
|
445
|
-
startMin: appointment.startMin,
|
|
446
|
-
endMin: appointment.endMin
|
|
447
|
-
});
|
|
448
|
-
}, []);
|
|
449
|
-
const onGlobalPointerMove = React2.useCallback(
|
|
450
|
-
(event) => {
|
|
451
|
-
if (drag.kind === "none") {
|
|
629
|
+
const onResizePointerDown = React2.useCallback(
|
|
630
|
+
(event, appointment) => {
|
|
631
|
+
if (event.button !== 0) {
|
|
452
632
|
return;
|
|
453
633
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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) {
|
|
469
664
|
return;
|
|
470
665
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
|
478
678
|
});
|
|
479
|
-
}
|
|
679
|
+
});
|
|
480
680
|
},
|
|
481
|
-
[dayMinutes,
|
|
681
|
+
[dayMinutes, pointerToMin, pointerToResourceId]
|
|
482
682
|
);
|
|
483
683
|
const onGlobalPointerUp = React2.useCallback(() => {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
684
|
+
finishDrag({
|
|
685
|
+
dragRef,
|
|
686
|
+
frameRef,
|
|
687
|
+
latestPointerRef,
|
|
688
|
+
pendingDragRef,
|
|
689
|
+
persistDrag,
|
|
690
|
+
setDrag,
|
|
691
|
+
setMovePreview,
|
|
692
|
+
suppressClickRef
|
|
693
|
+
});
|
|
694
|
+
}, [persistDrag]);
|
|
493
695
|
return {
|
|
494
696
|
colRefs,
|
|
495
697
|
drag,
|
|
698
|
+
isDropInvalid,
|
|
699
|
+
movePreview,
|
|
496
700
|
onApptPointerDown,
|
|
497
701
|
onGlobalPointerMove,
|
|
498
702
|
onGlobalPointerUp,
|
|
499
703
|
onResizePointerDown,
|
|
500
704
|
suppressClickRef
|
|
501
705
|
};
|
|
502
|
-
}
|
|
706
|
+
};
|
|
503
707
|
function useSchedulerPresentationData({
|
|
504
708
|
appointmentColorTokenClassMap,
|
|
505
709
|
dayStartAbs,
|
|
@@ -513,11 +717,22 @@ function useSchedulerPresentationData({
|
|
|
513
717
|
selectedDate
|
|
514
718
|
}) {
|
|
515
719
|
const effectiveAppts = React2.useMemo(() => {
|
|
516
|
-
return
|
|
720
|
+
return buildPresentationAppointments(renderAppts, drag, selectedDate, dayStartAbs);
|
|
517
721
|
}, [dayStartAbs, drag, renderAppts, selectedDate]);
|
|
722
|
+
const baseLaidOutByResource = React2.useMemo(() => {
|
|
723
|
+
return buildLaidOutByResource(resources, renderAppts);
|
|
724
|
+
}, [renderAppts, resources]);
|
|
518
725
|
const laidOutByResource = React2.useMemo(() => {
|
|
519
|
-
|
|
520
|
-
|
|
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]);
|
|
521
736
|
const appointmentBgByResource = React2.useMemo(() => {
|
|
522
737
|
if (!getResourceAppointmentBackground) {
|
|
523
738
|
return /* @__PURE__ */ new Map();
|
|
@@ -569,6 +784,453 @@ function useSchedulerPresentationData({
|
|
|
569
784
|
laidOutByResource
|
|
570
785
|
};
|
|
571
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" };
|
|
572
1234
|
function Scheduler({
|
|
573
1235
|
resources,
|
|
574
1236
|
appointments,
|
|
@@ -584,30 +1246,19 @@ function Scheduler({
|
|
|
584
1246
|
getResourceAppointmentColorToken,
|
|
585
1247
|
appointmentColorTokenClassMap,
|
|
586
1248
|
getResourceAppointmentBackground,
|
|
1249
|
+
renderToolbar,
|
|
587
1250
|
renderDatePicker,
|
|
588
1251
|
prevButtonLabel = "Prev",
|
|
589
1252
|
nextButtonLabel = "Next",
|
|
590
1253
|
dayStart = "09:00",
|
|
591
1254
|
dayEnd = "18:00"
|
|
592
1255
|
}) {
|
|
593
|
-
const {
|
|
594
|
-
appointmentMap,
|
|
595
|
-
dayMinutes,
|
|
596
|
-
dayStartAbs,
|
|
597
|
-
gridHeight,
|
|
598
|
-
gridMinWidth,
|
|
599
|
-
renderAppts
|
|
600
|
-
} = useSchedulerBaseData({
|
|
601
|
-
adapter,
|
|
602
|
-
appointments,
|
|
603
|
-
dayEnd,
|
|
604
|
-
dayStart,
|
|
605
|
-
resources,
|
|
606
|
-
selectedDate
|
|
607
|
-
});
|
|
1256
|
+
const { appointmentMap, dayMinutes, dayStartAbs, gridHeight, gridMinWidth, renderAppts } = useSchedulerBaseData({ adapter, appointments, dayEnd, dayStart, resources, selectedDate });
|
|
608
1257
|
const {
|
|
609
1258
|
colRefs,
|
|
610
1259
|
drag,
|
|
1260
|
+
isDropInvalid,
|
|
1261
|
+
movePreview,
|
|
611
1262
|
onApptPointerDown,
|
|
612
1263
|
onGlobalPointerMove,
|
|
613
1264
|
onGlobalPointerUp,
|
|
@@ -623,11 +1274,7 @@ function Scheduler({
|
|
|
623
1274
|
resources,
|
|
624
1275
|
selectedDate
|
|
625
1276
|
});
|
|
626
|
-
const {
|
|
627
|
-
appointmentAppearanceByResource,
|
|
628
|
-
appointmentBgByResource,
|
|
629
|
-
laidOutByResource
|
|
630
|
-
} = useSchedulerPresentationData({
|
|
1277
|
+
const { appointmentAppearanceByResource, appointmentBgByResource, laidOutByResource } = useSchedulerPresentationData({
|
|
631
1278
|
appointmentColorTokenClassMap,
|
|
632
1279
|
dayStartAbs,
|
|
633
1280
|
drag,
|
|
@@ -639,7 +1286,29 @@ function Scheduler({
|
|
|
639
1286
|
resources,
|
|
640
1287
|
selectedDate
|
|
641
1288
|
});
|
|
642
|
-
const
|
|
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]);
|
|
643
1312
|
return /* @__PURE__ */ jsxs(
|
|
644
1313
|
"section",
|
|
645
1314
|
{
|
|
@@ -648,124 +1317,77 @@ function Scheduler({
|
|
|
648
1317
|
onPointerUp: onGlobalPointerUp,
|
|
649
1318
|
onPointerCancel: onGlobalPointerUp,
|
|
650
1319
|
children: [
|
|
651
|
-
/* @__PURE__ */
|
|
1320
|
+
/* @__PURE__ */ jsx(
|
|
1321
|
+
Toolbar,
|
|
1322
|
+
{
|
|
1323
|
+
selectedDate,
|
|
1324
|
+
onSelectedDateChange,
|
|
1325
|
+
prevButtonLabel,
|
|
1326
|
+
nextButtonLabel,
|
|
1327
|
+
renderToolbar,
|
|
1328
|
+
renderDatePicker
|
|
1329
|
+
}
|
|
1330
|
+
),
|
|
1331
|
+
/* @__PURE__ */ jsx("div", { className: "rfs-shell border-border bg-card", children: /* @__PURE__ */ jsxs("div", { style: { minWidth: gridMinWidth }, children: [
|
|
652
1332
|
/* @__PURE__ */ jsx(
|
|
653
|
-
|
|
1333
|
+
Header,
|
|
654
1334
|
{
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
onClick: () => onSelectedDateChange(shiftDays(selectedDate, -1)),
|
|
658
|
-
children: prevButtonLabel
|
|
1335
|
+
resources,
|
|
1336
|
+
renderResourceHeader
|
|
659
1337
|
}
|
|
660
1338
|
),
|
|
661
|
-
(renderDatePicker ?? defaultRenderDatePicker)({ selectedDate, onSelectedDateChange }),
|
|
662
|
-
/* @__PURE__ */ jsx(
|
|
663
|
-
"button",
|
|
664
|
-
{
|
|
665
|
-
className: "rfs-btn border-border bg-card text-foreground hover:bg-muted",
|
|
666
|
-
type: "button",
|
|
667
|
-
onClick: () => onSelectedDateChange(shiftDays(selectedDate, 1)),
|
|
668
|
-
children: nextButtonLabel
|
|
669
|
-
}
|
|
670
|
-
)
|
|
671
|
-
] }),
|
|
672
|
-
/* @__PURE__ */ jsx("div", { className: "rfs-shell border-border bg-card", children: /* @__PURE__ */ jsxs("div", { style: { minWidth: gridMinWidth }, children: [
|
|
673
|
-
/* @__PURE__ */ jsxs("div", { className: "rfs-header border-border bg-card", children: [
|
|
674
|
-
/* @__PURE__ */ jsx("div", { className: "rfs-gutter border-border", style: { width: GUTTER_W } }),
|
|
675
|
-
resources.map((resource) => /* @__PURE__ */ jsxs(
|
|
676
|
-
"div",
|
|
677
|
-
{
|
|
678
|
-
className: "rfs-resource-header border-border bg-card text-foreground",
|
|
679
|
-
style: { minWidth: RESOURCE_MIN_W, flex: 1 },
|
|
680
|
-
children: [
|
|
681
|
-
(renderResourceHeader ?? defaultResourceHeaderRenderer)(resource),
|
|
682
|
-
isNameLike(resource) ? /* @__PURE__ */ jsx("div", { className: "rfs-resource-subtitle text-muted-foreground", children: fullName(resource) }) : null
|
|
683
|
-
]
|
|
684
|
-
},
|
|
685
|
-
String(resource.id)
|
|
686
|
-
))
|
|
687
|
-
] }),
|
|
688
1339
|
/* @__PURE__ */ jsxs("div", { className: "rfs-body", children: [
|
|
689
|
-
/* @__PURE__ */ jsx(
|
|
690
|
-
|
|
1340
|
+
/* @__PURE__ */ jsx(
|
|
1341
|
+
Gutter,
|
|
691
1342
|
{
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
String((dayStartAbs + i * 60) % 60).padStart(2, "0")
|
|
698
|
-
]
|
|
699
|
-
},
|
|
700
|
-
`tick-${i}`
|
|
701
|
-
)) }),
|
|
1343
|
+
dayMinutes,
|
|
1344
|
+
dayStartAbs,
|
|
1345
|
+
gridHeight
|
|
1346
|
+
}
|
|
1347
|
+
),
|
|
702
1348
|
/* @__PURE__ */ jsx("div", { className: "rfs-grid", children: resources.map((resource) => /* @__PURE__ */ jsx(
|
|
703
|
-
|
|
1349
|
+
ResourceCol,
|
|
704
1350
|
{
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
{
|
|
719
|
-
className: "rfs-hour-line border-border/70",
|
|
720
|
-
style: { top: TOP_PAD + i * 60 * PX_PER_MIN }
|
|
721
|
-
},
|
|
722
|
-
`major-${String(resource.id)}-${i}`
|
|
723
|
-
)),
|
|
724
|
-
Array.from({ length: Math.floor(dayMinutes / STEP_MIN) + 1 }).map((_, i) => /* @__PURE__ */ jsx(
|
|
725
|
-
"div",
|
|
726
|
-
{
|
|
727
|
-
className: "rfs-slot-line border-border/50",
|
|
728
|
-
style: { top: TOP_PAD + i * STEP_MIN * PX_PER_MIN }
|
|
729
|
-
},
|
|
730
|
-
`minor-${String(resource.id)}-${i}`
|
|
731
|
-
)),
|
|
732
|
-
(laidOutByResource.get(resource.id) ?? []).map((appointment) => {
|
|
733
|
-
const laneWidthPct = 100 / appointment.lanes;
|
|
734
|
-
const top = TOP_PAD + appointment.startMin * PX_PER_MIN;
|
|
735
|
-
const height = Math.max(MIN_EVENT_MIN * PX_PER_MIN, (appointment.endMin - appointment.startMin) * PX_PER_MIN);
|
|
736
|
-
const appointmentAppearance = appointmentAppearanceByResource.get(resource.id);
|
|
737
|
-
const appointmentBackgroundColor = appointmentBgByResource.get(resource.id);
|
|
738
|
-
return /* @__PURE__ */ jsx(
|
|
739
|
-
"div",
|
|
740
|
-
{
|
|
741
|
-
className: "rfs-card-slot",
|
|
742
|
-
style: {
|
|
743
|
-
top,
|
|
744
|
-
left: `${laneWidthPct * appointment.lane}%`,
|
|
745
|
-
width: `${laneWidthPct}%`,
|
|
746
|
-
height
|
|
747
|
-
},
|
|
748
|
-
children: (renderAppointment ?? defaultRenderAppointment)({
|
|
749
|
-
appointment,
|
|
750
|
-
onPointerDown: (event) => onApptPointerDown(event, appointment),
|
|
751
|
-
onResizePointerDown: (event) => onResizePointerDown(event, appointment),
|
|
752
|
-
appointmentAppearance,
|
|
753
|
-
appointmentBackgroundColor,
|
|
754
|
-
drag,
|
|
755
|
-
suppressClickRef
|
|
756
|
-
})
|
|
757
|
-
},
|
|
758
|
-
String(appointment.id)
|
|
759
|
-
);
|
|
760
|
-
})
|
|
761
|
-
]
|
|
762
|
-
}
|
|
763
|
-
)
|
|
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
|
|
764
1364
|
},
|
|
765
1365
|
String(resource.id)
|
|
766
1366
|
)) })
|
|
767
1367
|
] })
|
|
768
|
-
] }) })
|
|
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
|
|
769
1391
|
]
|
|
770
1392
|
}
|
|
771
1393
|
);
|