@omidrahmati/react-slot-scheduler 1.0.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/dist/index.js ADDED
@@ -0,0 +1,1154 @@
1
+ // src/components/BookingCalendar.tsx
2
+ import React2, { useEffect, useMemo as useMemo2, useState as useState2 } from "react";
3
+
4
+ // src/utils/date.ts
5
+ function toIsoDate(date) {
6
+ return date.toISOString().slice(0, 10);
7
+ }
8
+ function getWeekDays(anchor, weekStartsOn = 6) {
9
+ const current = new Date(anchor);
10
+ current.setHours(12, 0, 0, 0);
11
+ const day = current.getDay();
12
+ const offset = (day - weekStartsOn + 7) % 7;
13
+ current.setDate(current.getDate() - offset);
14
+ return Array.from({ length: 7 }, (_, i) => {
15
+ const d = new Date(current);
16
+ d.setDate(current.getDate() + i);
17
+ return d;
18
+ });
19
+ }
20
+ function addDays(date, amount) {
21
+ const d = new Date(date);
22
+ d.setDate(d.getDate() + amount);
23
+ return d;
24
+ }
25
+ function generateTimeSlots(startHour, endHour, granularity) {
26
+ const items = [];
27
+ const startMinutes = startHour * 60;
28
+ const endMinutes = endHour * 60;
29
+ for (let m = startMinutes; m < endMinutes; m += granularity) {
30
+ const h = Math.floor(m / 60);
31
+ const mm = m % 60;
32
+ items.push(`${h.toString().padStart(2, "0")}:${mm.toString().padStart(2, "0")}`);
33
+ }
34
+ return items;
35
+ }
36
+ function getHourBounds(work) {
37
+ let start = 8;
38
+ let end = 20;
39
+ const starts = [];
40
+ const ends = [];
41
+ for (const d of work) {
42
+ if (d.workStartTime && d.workEndTime) {
43
+ starts.push(Number(d.workStartTime.split(":")[0]));
44
+ const [eh, em] = d.workEndTime.split(":").map(Number);
45
+ ends.push(eh + (em > 0 ? 1 : 0));
46
+ }
47
+ }
48
+ if (starts.length && ends.length) {
49
+ start = Math.min(...starts);
50
+ end = Math.max(...ends);
51
+ }
52
+ return { start, end };
53
+ }
54
+ function rangesOverlap(startA, endA, startB, endB) {
55
+ return startA < endB && endA > startB;
56
+ }
57
+
58
+ // src/styles/defaultTheme.ts
59
+ var defaultTheme = {
60
+ primary: "#0f766e",
61
+ bg: "#f4f7f7",
62
+ panel: "#ffffff",
63
+ border: "#d6e0df",
64
+ text: "#102725",
65
+ mutedText: "#5c7270",
66
+ availableBg: "#dcfce7",
67
+ bookedBg: "#fee2e2",
68
+ blockedBg: "#e5e7eb",
69
+ customBg: "#e0f2fe"
70
+ };
71
+
72
+ // src/components/GanttScheduler.tsx
73
+ import { useCallback, useMemo, useRef, useState } from "react";
74
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
75
+ function toMin(t) {
76
+ const [h, m] = t.split(":").map(Number);
77
+ return h * 60 + m;
78
+ }
79
+ function toTime(mins) {
80
+ const clamped = Math.max(0, Math.min(23 * 60 + 59, mins));
81
+ return `${String(Math.floor(clamped / 60)).padStart(2, "0")}:${String(clamped % 60).padStart(2, "0")}`;
82
+ }
83
+ function snapToGrid(mins, gran) {
84
+ return Math.round(mins / gran) * gran;
85
+ }
86
+ function isoStr(d) {
87
+ return d.toISOString().slice(0, 10);
88
+ }
89
+ function addDays2(d, n) {
90
+ const dd = new Date(d);
91
+ dd.setDate(dd.getDate() + n);
92
+ return dd;
93
+ }
94
+ function assignLanesSequential(items) {
95
+ const n = items.length || 1;
96
+ return items.map((item, i) => ({ ...item, lane: i, totalLanes: n }));
97
+ }
98
+ function daysBetween(a, b) {
99
+ return Math.round((new Date(b).getTime() - new Date(a).getTime()) / 864e5);
100
+ }
101
+ function addDaysToIso(iso, n) {
102
+ const d = new Date(iso);
103
+ d.setDate(d.getDate() + n);
104
+ return isoStr(d);
105
+ }
106
+ function assignLanes(items) {
107
+ const sorted = [...items].sort((a, b) => toMin(a.startTime) - toMin(b.startTime));
108
+ const laneEnds = [];
109
+ const result = [];
110
+ for (const item of sorted) {
111
+ const s = toMin(item.startTime);
112
+ const e = toMin(item.endTime);
113
+ let assigned = laneEnds.findIndex((end) => end <= s);
114
+ if (assigned === -1) {
115
+ assigned = laneEnds.length;
116
+ laneEnds.push(0);
117
+ }
118
+ laneEnds[assigned] = e;
119
+ result.push({ ...item, lane: assigned, totalLanes: 0 });
120
+ }
121
+ const n = laneEnds.length || 1;
122
+ return result.map((r) => ({ ...r, totalLanes: n }));
123
+ }
124
+ var ROW_MIN_H = 60;
125
+ var LANE_H = 44;
126
+ var LANE_GAP = 4;
127
+ var LABEL_W = 144;
128
+ function GanttScheduler({
129
+ schedulerMode,
130
+ rows,
131
+ items,
132
+ date,
133
+ timeUnit = "hour",
134
+ scale = "week",
135
+ weekStartsOn = 1,
136
+ timeStart = 8,
137
+ timeEnd = 20,
138
+ granularity = 30,
139
+ locale = "en-US",
140
+ direction = "ltr",
141
+ theme,
142
+ onItemClick,
143
+ onItemMove,
144
+ onItemResize,
145
+ onItemCreate,
146
+ translations,
147
+ onDateChange
148
+ }) {
149
+ const merged = { ...defaultTheme, ...theme };
150
+ const isRtl = direction === "rtl";
151
+ const todayStr = (() => {
152
+ const d = /* @__PURE__ */ new Date();
153
+ d.setHours(12, 0, 0, 0);
154
+ return isoStr(d);
155
+ })();
156
+ const totalMins = (timeEnd - timeStart) * 60;
157
+ const t = { previous: "Previous", today: "Today", next: "Next", week: "Week", month: "Month", ...translations };
158
+ const cssVars = {
159
+ "--gantt-primary": merged.primary,
160
+ "--gantt-bg": merged.bg,
161
+ "--gantt-panel": merged.panel,
162
+ "--gantt-border": merged.border,
163
+ "--gantt-text": merged.text,
164
+ "--gantt-muted": merged.mutedText,
165
+ "--gantt-booked": merged.bookedBg,
166
+ "--gantt-blocked": merged.blockedBg,
167
+ "--gantt-custom": merged.customBg
168
+ };
169
+ const hourCols = useMemo(() => {
170
+ if (timeUnit !== "hour") return [];
171
+ return Array.from(
172
+ { length: timeEnd - timeStart },
173
+ (_, i) => `${String(timeStart + i).padStart(2, "0")}:00`
174
+ );
175
+ }, [timeUnit, timeStart, timeEnd]);
176
+ const dayColumns = useMemo(() => {
177
+ if (timeUnit !== "day") return [];
178
+ const anchor = new Date(date);
179
+ anchor.setHours(12, 0, 0, 0);
180
+ if (scale === "week") {
181
+ const dow = anchor.getDay();
182
+ const offset = (dow - weekStartsOn + 7) % 7;
183
+ anchor.setDate(anchor.getDate() - offset);
184
+ return Array.from({ length: 7 }, (_, i) => addDays2(anchor, i));
185
+ }
186
+ if (scale === "month") {
187
+ const first = new Date(anchor.getFullYear(), anchor.getMonth(), 1, 12);
188
+ const daysInMonth = new Date(anchor.getFullYear(), anchor.getMonth() + 1, 0).getDate();
189
+ return Array.from({ length: daysInMonth }, (_, i) => addDays2(first, i));
190
+ }
191
+ return [new Date(anchor)];
192
+ }, [timeUnit, date, scale, weekStartsOn]);
193
+ const dayColStrs = useMemo(() => dayColumns.map(isoStr), [dayColumns]);
194
+ const itemsByRowHour = useMemo(() => {
195
+ if (timeUnit !== "hour") return /* @__PURE__ */ new Map();
196
+ const dateStr = isoStr(date);
197
+ const map = /* @__PURE__ */ new Map();
198
+ for (const item of items) {
199
+ if (item.date !== dateStr) continue;
200
+ const list = map.get(item.rowId) ?? [];
201
+ list.push(item);
202
+ map.set(item.rowId, list);
203
+ }
204
+ return map;
205
+ }, [items, date, timeUnit]);
206
+ const itemsByRowDate = useMemo(() => {
207
+ if (timeUnit !== "day") return /* @__PURE__ */ new Map();
208
+ const dateSet = new Set(dayColStrs);
209
+ const map = /* @__PURE__ */ new Map();
210
+ for (const item of items) {
211
+ if (!dateSet.has(item.date)) continue;
212
+ const key = `${item.rowId}__${item.date}`;
213
+ const list = map.get(key) ?? [];
214
+ list.push(item);
215
+ map.set(key, list);
216
+ }
217
+ return map;
218
+ }, [items, timeUnit, dayColStrs]);
219
+ const gridRef = useRef(null);
220
+ const [dragging, setDragging] = useState(null);
221
+ const [creating, setCreating] = useState(null);
222
+ const getTimeFromX = useCallback((clientX, grid) => {
223
+ const rect = grid.getBoundingClientRect();
224
+ const contentW = rect.width - LABEL_W;
225
+ const contentLeft = isRtl ? rect.left : rect.left + LABEL_W;
226
+ const contentRight = isRtl ? rect.right - LABEL_W : rect.right;
227
+ let frac = isRtl ? (contentRight - clientX) / contentW : (clientX - contentLeft) / contentW;
228
+ frac = Math.max(0, Math.min(1, frac));
229
+ return timeStart * 60 + frac * totalMins;
230
+ }, [isRtl, timeStart, totalMins]);
231
+ const getDateFromX = useCallback((clientX, grid) => {
232
+ if (!dayColumns.length) return null;
233
+ const rect = grid.getBoundingClientRect();
234
+ const contentW = rect.width - LABEL_W;
235
+ const contentLeft = isRtl ? rect.left : rect.left + LABEL_W;
236
+ const contentRight = isRtl ? rect.right - LABEL_W : rect.right;
237
+ let frac = isRtl ? (contentRight - clientX) / contentW : (clientX - contentLeft) / contentW;
238
+ frac = Math.max(0, Math.min(0.9999, frac));
239
+ const colIdx = Math.floor(frac * dayColumns.length);
240
+ return isoStr(dayColumns[Math.min(colIdx, dayColumns.length - 1)]);
241
+ }, [dayColumns, isRtl]);
242
+ const getRowFromY = useCallback((clientY, grid) => {
243
+ const scrollTop = grid.scrollTop ?? 0;
244
+ const rect = grid.getBoundingClientRect();
245
+ const relY = clientY - rect.top + scrollTop - 40;
246
+ let acc = 0;
247
+ for (const row of rows) {
248
+ const h = ROW_MIN_H;
249
+ if (relY >= acc && relY < acc + h) return row.id;
250
+ acc += h;
251
+ }
252
+ return null;
253
+ }, [rows]);
254
+ const onMouseMove = useCallback((e) => {
255
+ if (creating && gridRef.current && timeUnit === "hour") {
256
+ const rawMin = getTimeFromX(e.clientX, gridRef.current);
257
+ const snapped = snapToGrid(rawMin, granularity);
258
+ const endMin = Math.max(snapped, creating.startMin + granularity);
259
+ setCreating((c) => c ? { ...c, endMin } : null);
260
+ return;
261
+ }
262
+ if (creating && gridRef.current && timeUnit === "day") {
263
+ const hovered = getDateFromX(e.clientX, gridRef.current);
264
+ if (hovered) setCreating((c) => c ? { ...c, endDate: hovered } : null);
265
+ return;
266
+ }
267
+ if (!dragging || !gridRef.current) return;
268
+ if (timeUnit === "hour") {
269
+ const rawMin = getTimeFromX(e.clientX, gridRef.current);
270
+ const duration = dragging.origEnd - dragging.origStart;
271
+ if (dragging.type === "move") {
272
+ const snapped = snapToGrid(rawMin - duration / 2, granularity);
273
+ const newStart = Math.max(timeStart * 60, snapped);
274
+ const newRowId = getRowFromY(e.clientY, gridRef.current) ?? dragging.curRowId;
275
+ setDragging((d) => d ? { ...d, curStart: newStart, curEnd: newStart + duration, curRowId: newRowId } : null);
276
+ } else if (dragging.type === "resize-left") {
277
+ const snapped = snapToGrid(rawMin, granularity);
278
+ setDragging((d) => d ? { ...d, curStart: Math.min(snapped, dragging.curEnd - granularity) } : null);
279
+ } else {
280
+ const snapped = snapToGrid(rawMin, granularity);
281
+ setDragging((d) => d ? { ...d, curEnd: Math.max(snapped, dragging.curStart + granularity) } : null);
282
+ }
283
+ } else if (timeUnit === "day" && dragging.type === "move") {
284
+ const newDate = getDateFromX(e.clientX, gridRef.current) ?? dragging.curDate;
285
+ const newRowId = getRowFromY(e.clientY, gridRef.current) ?? dragging.curRowId;
286
+ setDragging((d) => d ? { ...d, curDate: newDate, curRowId: newRowId } : null);
287
+ }
288
+ }, [creating, dragging, getTimeFromX, getDateFromX, getRowFromY, granularity, timeStart, timeUnit]);
289
+ const onMouseUp = useCallback(() => {
290
+ if (creating) {
291
+ if (timeUnit === "hour") {
292
+ const dur = creating.endMin - creating.startMin;
293
+ if (dur >= granularity) {
294
+ onItemCreate?.({
295
+ rowId: creating.rowId,
296
+ date: creating.date,
297
+ startTime: toTime(creating.startMin),
298
+ endTime: toTime(creating.endMin)
299
+ });
300
+ }
301
+ } else {
302
+ const endDate = creating.endDate ?? creating.date;
303
+ const [startDate, finalEnd] = creating.date <= endDate ? [creating.date, endDate] : [endDate, creating.date];
304
+ onItemCreate?.({
305
+ rowId: creating.rowId,
306
+ date: startDate,
307
+ endDate: finalEnd !== startDate ? finalEnd : void 0,
308
+ startTime: "09:00",
309
+ endTime: "18:00"
310
+ });
311
+ }
312
+ setCreating(null);
313
+ return;
314
+ }
315
+ if (!dragging) return;
316
+ const { item, type, curStart, curEnd, curRowId, curDate } = dragging;
317
+ if (type === "move") {
318
+ let newEndDate;
319
+ if (item.endDate) {
320
+ const span = daysBetween(item.date, item.endDate);
321
+ newEndDate = addDaysToIso(curDate, span);
322
+ }
323
+ onItemMove?.({
324
+ item,
325
+ newRowId: curRowId,
326
+ newDate: curDate,
327
+ newEndDate,
328
+ newStartTime: toTime(curStart),
329
+ newEndTime: toTime(curEnd)
330
+ });
331
+ } else {
332
+ onItemResize?.({ item, newStartTime: toTime(curStart), newEndTime: toTime(curEnd) });
333
+ }
334
+ setDragging(null);
335
+ }, [creating, dragging, granularity, onItemCreate, onItemMove, onItemResize, timeUnit]);
336
+ const navDelta = timeUnit === "day" ? scale === "month" ? 30 : scale === "week" ? 7 : 1 : 1;
337
+ const navigate = (delta) => {
338
+ if (!onDateChange) return;
339
+ onDateChange(addDays2(date, delta * navDelta));
340
+ };
341
+ const titleLabel = useMemo(() => {
342
+ if (timeUnit === "day" && dayColumns.length > 1) {
343
+ const first = dayColumns[0].toLocaleDateString(locale, { month: "short", day: "numeric" });
344
+ const last = dayColumns[dayColumns.length - 1].toLocaleDateString(locale, { month: "short", day: "numeric", year: "numeric" });
345
+ return `${first} \u2013 ${last}`;
346
+ }
347
+ return date.toLocaleDateString(locale, { weekday: "long", year: "numeric", month: "short", day: "numeric" });
348
+ }, [timeUnit, dayColumns, date, locale]);
349
+ return /* @__PURE__ */ jsxs(
350
+ "div",
351
+ {
352
+ className: `gantt-root ${isRtl ? "gantt-rtl" : "gantt-ltr"}`,
353
+ "data-scheduler-mode": schedulerMode,
354
+ style: { ...cssVars, direction: isRtl ? "rtl" : "ltr" },
355
+ onMouseMove: dragging || creating ? onMouseMove : void 0,
356
+ onMouseUp: dragging || creating ? onMouseUp : void 0,
357
+ onMouseLeave: dragging || creating ? onMouseUp : void 0,
358
+ children: [
359
+ /* @__PURE__ */ jsxs("div", { className: "gantt-toolbar", children: [
360
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6 }, children: [
361
+ /* @__PURE__ */ jsx("button", { className: "gantt-btn", onClick: () => navigate(-1), children: t.previous }),
362
+ /* @__PURE__ */ jsx("button", { className: "gantt-btn", onClick: () => onDateChange?.(/* @__PURE__ */ new Date()), children: t.today }),
363
+ /* @__PURE__ */ jsx("button", { className: "gantt-btn", onClick: () => navigate(1), children: t.next })
364
+ ] }),
365
+ /* @__PURE__ */ jsx("strong", { className: "gantt-title", children: titleLabel }),
366
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 4 }, children: /* @__PURE__ */ jsx("span", { className: "gantt-badge", children: timeUnit === "hour" ? "\u23F1 Hour" : scale === "month" ? "\u{1F4C5} Month" : scale === "week" ? "\u{1F4C5} Week" : "\u{1F4C5} Day" }) })
367
+ ] }),
368
+ /* @__PURE__ */ jsxs("div", { className: "gantt-scroll-wrap", ref: gridRef, children: [
369
+ timeUnit === "hour" && /* @__PURE__ */ jsxs(Fragment, { children: [
370
+ /* @__PURE__ */ jsxs("div", { className: "gantt-header", children: [
371
+ /* @__PURE__ */ jsx("div", { className: "gantt-label-col gantt-header-label" }),
372
+ /* @__PURE__ */ jsx("div", { className: "gantt-time-header", children: hourCols.map((slot) => /* @__PURE__ */ jsx("div", { className: "gantt-time-cell", children: slot }, slot)) })
373
+ ] }),
374
+ rows.map((row) => {
375
+ const baseItems = itemsByRowHour.get(row.id) ?? [];
376
+ const dragItem = dragging?.item;
377
+ const isDragTargetRow = dragging?.type === "move" && dragging.curRowId === row.id && dragItem?.rowId !== row.id;
378
+ const isDragSourceRow = dragging?.type === "move" && dragItem?.rowId === row.id && dragging.curRowId !== row.id;
379
+ const visibleItems = isDragSourceRow ? baseItems.filter((i) => i.id !== dragItem.id) : baseItems;
380
+ const ghostItems = isDragTargetRow && dragItem ? [{ ...dragItem, rowId: row.id, startTime: toTime(dragging.curStart), endTime: toTime(dragging.curEnd) }] : [];
381
+ const allVisible = [...visibleItems, ...ghostItems];
382
+ const laned = assignLanes(allVisible);
383
+ const maxLanes = Math.max(1, laned.length > 0 ? Math.max(...laned.map((l) => l.totalLanes)) : 1);
384
+ const rowH = Math.max(ROW_MIN_H, maxLanes * (LANE_H + LANE_GAP) + LANE_GAP * 2);
385
+ return /* @__PURE__ */ jsxs("div", { className: "gantt-row", style: { minHeight: rowH }, children: [
386
+ /* @__PURE__ */ jsxs("div", { className: "gantt-label-col", children: [
387
+ /* @__PURE__ */ jsx("div", { className: "gantt-row-label-text", children: row.label }),
388
+ row.subLabel && /* @__PURE__ */ jsx("div", { className: "gantt-row-sublabel", children: row.subLabel })
389
+ ] }),
390
+ /* @__PURE__ */ jsxs(
391
+ "div",
392
+ {
393
+ className: "gantt-row-content",
394
+ style: { "--gantt-cols": hourCols.length, cursor: onItemCreate ? "cell" : "default" },
395
+ onMouseDown: (ev) => {
396
+ if (ev.button !== 0 || !onItemCreate || !gridRef.current) return;
397
+ if (ev.target.closest(".gantt-item")) return;
398
+ ev.preventDefault();
399
+ const rawMin = getTimeFromX(ev.clientX, gridRef.current);
400
+ const snapped = snapToGrid(rawMin, granularity);
401
+ setCreating({ rowId: row.id, date: isoStr(date), startMin: snapped, endMin: snapped });
402
+ },
403
+ children: [
404
+ hourCols.map((_, i) => /* @__PURE__ */ jsx(
405
+ "div",
406
+ {
407
+ className: "gantt-grid-line",
408
+ style: isRtl ? { right: `${i / hourCols.length * 100}%` } : { left: `${i / hourCols.length * 100}%` }
409
+ },
410
+ i
411
+ )),
412
+ creating?.rowId === row.id && /* @__PURE__ */ jsx(
413
+ "div",
414
+ {
415
+ className: "gantt-selection-ghost",
416
+ style: {
417
+ ...isRtl ? { right: `${Math.max(0, (creating.startMin - timeStart * 60) / totalMins * 100)}%` } : { left: `${Math.max(0, (creating.startMin - timeStart * 60) / totalMins * 100)}%` },
418
+ width: `${Math.max(0.5, (creating.endMin - creating.startMin) / totalMins * 100)}%`,
419
+ top: LANE_GAP,
420
+ height: LANE_H
421
+ },
422
+ children: /* @__PURE__ */ jsx("div", { className: "gantt-item-inner", children: /* @__PURE__ */ jsxs("div", { className: "gantt-item-title", children: [
423
+ toTime(creating.startMin),
424
+ " \u2013 ",
425
+ toTime(creating.endMin)
426
+ ] }) })
427
+ }
428
+ ),
429
+ laned.map((item) => {
430
+ const isOrigDrg = dragging?.item.id === item.id && dragging.curRowId === row.id;
431
+ const isGhost = ghostItems.some((g) => g.id === item.id);
432
+ const s = isOrigDrg || isGhost ? dragging.curStart : toMin(item.startTime);
433
+ const e = isOrigDrg || isGhost ? dragging.curEnd : toMin(item.endTime);
434
+ const leftPct = (s - timeStart * 60) / totalMins * 100;
435
+ const widthPct = (e - s) / totalMins * 100;
436
+ const topPx = LANE_GAP + item.lane * (LANE_H + LANE_GAP);
437
+ const cls = `gantt-item gantt-item-${item.status} ${isOrigDrg || isGhost ? "gantt-item-dragging" : ""}`;
438
+ return /* @__PURE__ */ jsxs(
439
+ "div",
440
+ {
441
+ className: cls,
442
+ style: {
443
+ ...isRtl ? { right: `${Math.max(0, leftPct)}%` } : { left: `${Math.max(0, leftPct)}%` },
444
+ width: `${Math.max(0.5, widthPct)}%`,
445
+ top: topPx,
446
+ height: LANE_H,
447
+ cursor: isGhost ? "grabbing" : "grab"
448
+ },
449
+ onClick: (ev) => {
450
+ ev.stopPropagation();
451
+ if (!dragging) onItemClick?.(item.id);
452
+ },
453
+ onKeyDown: (ev) => {
454
+ if (ev.key !== "Enter" && ev.key !== " ") return;
455
+ ev.preventDefault();
456
+ ev.stopPropagation();
457
+ if (!dragging) onItemClick?.(item.id);
458
+ },
459
+ role: "button",
460
+ tabIndex: 0,
461
+ onMouseDown: (ev) => {
462
+ if (ev.button !== 0 || isGhost) return;
463
+ ev.preventDefault();
464
+ setDragging({ type: "move", item, origStart: toMin(item.startTime), origEnd: toMin(item.endTime), curStart: toMin(item.startTime), curEnd: toMin(item.endTime), curRowId: row.id, curDate: item.date });
465
+ },
466
+ children: [
467
+ /* @__PURE__ */ jsx("div", { className: "gantt-handle gantt-handle-left", onMouseDown: (ev) => {
468
+ if (isGhost) return;
469
+ ev.preventDefault();
470
+ ev.stopPropagation();
471
+ setDragging({ type: isRtl ? "resize-right" : "resize-left", item, origStart: toMin(item.startTime), origEnd: toMin(item.endTime), curStart: toMin(item.startTime), curEnd: toMin(item.endTime), curRowId: row.id, curDate: item.date });
472
+ } }),
473
+ /* @__PURE__ */ jsxs("div", { className: "gantt-item-inner", children: [
474
+ /* @__PURE__ */ jsx("div", { className: "gantt-item-title", children: item.title }),
475
+ item.subTitle && /* @__PURE__ */ jsx("div", { className: "gantt-item-sub", children: item.subTitle })
476
+ ] }),
477
+ /* @__PURE__ */ jsx("div", { className: "gantt-handle gantt-handle-right", onMouseDown: (ev) => {
478
+ if (isGhost) return;
479
+ ev.preventDefault();
480
+ ev.stopPropagation();
481
+ setDragging({ type: isRtl ? "resize-left" : "resize-right", item, origStart: toMin(item.startTime), origEnd: toMin(item.endTime), curStart: toMin(item.startTime), curEnd: toMin(item.endTime), curRowId: row.id, curDate: item.date });
482
+ } })
483
+ ]
484
+ },
485
+ `${item.id}-${row.id}`
486
+ );
487
+ })
488
+ ]
489
+ }
490
+ )
491
+ ] }, row.id);
492
+ })
493
+ ] }),
494
+ timeUnit === "day" && /* @__PURE__ */ jsxs(Fragment, { children: [
495
+ /* @__PURE__ */ jsxs("div", { className: "gantt-header", children: [
496
+ /* @__PURE__ */ jsx("div", { className: "gantt-label-col gantt-header-label" }),
497
+ /* @__PURE__ */ jsx("div", { className: "gantt-time-header", children: dayColumns.map((d) => {
498
+ const ds = isoStr(d);
499
+ const isToday = ds === todayStr;
500
+ const label = scale === "month" ? d.toLocaleDateString(locale, { day: "numeric" }) : d.toLocaleDateString(locale, { weekday: "short", month: "short", day: "numeric" });
501
+ return /* @__PURE__ */ jsx("div", { className: `gantt-time-cell ${isToday ? "gantt-today-col" : ""}`, children: label }, ds);
502
+ }) })
503
+ ] }),
504
+ rows.map((row) => {
505
+ const rowItems = dayColStrs.flatMap(
506
+ (ds) => assignLanesSequential(itemsByRowDate.get(`${row.id}__${ds}`) ?? [])
507
+ );
508
+ const maxPerCol = Math.max(1, ...dayColStrs.map(
509
+ (ds) => (itemsByRowDate.get(`${row.id}__${ds}`) ?? []).length
510
+ ));
511
+ const rowH = Math.max(ROW_MIN_H, maxPerCol * (LANE_H + LANE_GAP) + LANE_GAP * 2);
512
+ const creatingThisRow = creating?.rowId === row.id && creating.endDate !== void 0;
513
+ let ghostColStart = -1, ghostColEnd = -1;
514
+ if (creatingThisRow && creating) {
515
+ const a = dayColStrs.indexOf(creating.date);
516
+ const b = dayColStrs.indexOf(creating.endDate);
517
+ const ai = a < 0 ? 0 : a;
518
+ const bi = b < 0 ? 0 : b;
519
+ ghostColStart = Math.min(ai, bi);
520
+ ghostColEnd = Math.max(ai, bi);
521
+ }
522
+ return /* @__PURE__ */ jsxs("div", { className: "gantt-row", style: { minHeight: rowH }, children: [
523
+ /* @__PURE__ */ jsxs("div", { className: "gantt-label-col", children: [
524
+ /* @__PURE__ */ jsx("div", { className: "gantt-row-label-text", children: row.label }),
525
+ row.subLabel && /* @__PURE__ */ jsx("div", { className: "gantt-row-sublabel", children: row.subLabel })
526
+ ] }),
527
+ /* @__PURE__ */ jsxs("div", { className: "gantt-row-day-content", style: { position: "relative" }, children: [
528
+ dayColumns.map((d) => {
529
+ const ds = isoStr(d);
530
+ const isToday = ds === todayStr;
531
+ const isDragTarget = dragging?.type === "move" && dragging.curRowId === row.id && dragging.curDate === ds && (dragging.curDate !== dragging.item.date || dragging.curRowId !== dragging.item.rowId);
532
+ return /* @__PURE__ */ jsx(
533
+ "div",
534
+ {
535
+ className: `gantt-day-cell ${isToday ? "gantt-today-col" : ""} ${isDragTarget ? "gantt-day-cell-target" : ""}`,
536
+ style: { minHeight: rowH, cursor: onItemCreate ? "cell" : "default" },
537
+ onMouseDown: (ev) => {
538
+ if (ev.button !== 0 || !onItemCreate || !gridRef.current) return;
539
+ if (ev.target.closest(".gantt-item")) return;
540
+ ev.preventDefault();
541
+ setCreating({ rowId: row.id, date: ds, startMin: 0, endMin: 0, endDate: ds });
542
+ }
543
+ },
544
+ ds
545
+ );
546
+ }),
547
+ creatingThisRow && ghostColStart >= 0 && /* @__PURE__ */ jsx(
548
+ "div",
549
+ {
550
+ className: "gantt-selection-ghost",
551
+ style: {
552
+ position: "absolute",
553
+ top: LANE_GAP,
554
+ height: LANE_H,
555
+ ...isRtl ? { right: `${ghostColStart / dayColumns.length * 100}%` } : { left: `${ghostColStart / dayColumns.length * 100}%` },
556
+ width: `${(ghostColEnd - ghostColStart + 1) / dayColumns.length * 100}%`
557
+ },
558
+ children: /* @__PURE__ */ jsx("div", { className: "gantt-item-inner", children: /* @__PURE__ */ jsxs("div", { className: "gantt-item-title", children: [
559
+ dayColumns[ghostColStart]?.toLocaleDateString(locale, { month: "short", day: "numeric" }),
560
+ ghostColEnd !== ghostColStart && ` \u2013 ${dayColumns[ghostColEnd]?.toLocaleDateString(locale, { month: "short", day: "numeric" })}`
561
+ ] }) })
562
+ }
563
+ ),
564
+ rowItems.map((item) => {
565
+ const colStart = dayColStrs.indexOf(item.date);
566
+ if (colStart < 0) return null;
567
+ const rawColEnd = item.endDate ? dayColStrs.indexOf(item.endDate) : colStart;
568
+ const colEnd = rawColEnd < 0 ? dayColStrs.length - 1 : Math.max(colStart, rawColEnd);
569
+ const spanCols = colEnd - colStart + 1;
570
+ const isDraggingThis = dragging?.item.id === item.id && dragging.type === "move";
571
+ const rawEffectiveColStart = isDraggingThis ? dayColStrs.indexOf(dragging.curDate) : colStart;
572
+ if (rawEffectiveColStart < 0) return null;
573
+ const effectiveColStart = rawEffectiveColStart;
574
+ const effectiveSpan = spanCols;
575
+ const leftPct = effectiveColStart / dayColumns.length * 100;
576
+ const widthPct = effectiveSpan / dayColumns.length * 100;
577
+ const topPx = LANE_GAP + item.lane * (LANE_H + LANE_GAP);
578
+ const cls = `gantt-item gantt-item-${item.status} gantt-day-item-overlay${isDraggingThis ? " gantt-item-dragging" : ""}`;
579
+ return /* @__PURE__ */ jsx(
580
+ "div",
581
+ {
582
+ className: cls,
583
+ style: {
584
+ position: "absolute",
585
+ top: topPx,
586
+ height: LANE_H,
587
+ ...isRtl ? { right: `${Math.max(0, leftPct)}%` } : { left: `${Math.max(0, leftPct)}%` },
588
+ width: `${Math.max(0.5, widthPct)}%`,
589
+ cursor: isDraggingThis ? "grabbing" : "grab",
590
+ boxSizing: "border-box",
591
+ paddingLeft: 4,
592
+ paddingRight: 4
593
+ },
594
+ onClick: (ev) => {
595
+ ev.stopPropagation();
596
+ if (!dragging) onItemClick?.(item.id);
597
+ },
598
+ onKeyDown: (ev) => {
599
+ if (ev.key !== "Enter" && ev.key !== " ") return;
600
+ ev.preventDefault();
601
+ ev.stopPropagation();
602
+ if (!dragging) onItemClick?.(item.id);
603
+ },
604
+ role: "button",
605
+ tabIndex: 0,
606
+ onMouseDown: (ev) => {
607
+ if (ev.button !== 0) return;
608
+ ev.preventDefault();
609
+ ev.stopPropagation();
610
+ setDragging({
611
+ type: "move",
612
+ item,
613
+ origStart: toMin(item.startTime),
614
+ origEnd: toMin(item.endTime),
615
+ curStart: toMin(item.startTime),
616
+ curEnd: toMin(item.endTime),
617
+ curRowId: row.id,
618
+ curDate: item.date
619
+ });
620
+ },
621
+ children: /* @__PURE__ */ jsxs("div", { className: "gantt-item-inner", children: [
622
+ /* @__PURE__ */ jsx("div", { className: "gantt-item-title", children: item.title }),
623
+ /* @__PURE__ */ jsxs("div", { className: "gantt-item-sub", children: [
624
+ item.endDate ? `${item.date} \u2013 ${item.endDate}` : `${item.startTime}\u2013${item.endTime}`,
625
+ item.subTitle ? ` \xB7 ${item.subTitle}` : ""
626
+ ] })
627
+ ] })
628
+ },
629
+ `${item.id}-${row.id}`
630
+ );
631
+ })
632
+ ] })
633
+ ] }, row.id);
634
+ })
635
+ ] })
636
+ ] })
637
+ ]
638
+ }
639
+ );
640
+ }
641
+
642
+ // src/components/BookingCalendar.tsx
643
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
644
+ function toMinutes(t) {
645
+ const [h, m] = t.split(":").map(Number);
646
+ return h * 60 + m;
647
+ }
648
+ function slotClass(status) {
649
+ return `rbc-slot ${status}`;
650
+ }
651
+ function minutesToTime(total) {
652
+ const h = Math.floor(total / 60);
653
+ const m = total % 60;
654
+ return `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}`;
655
+ }
656
+ function compressSelectionKeys(keys, slotGranularity) {
657
+ const byDate = /* @__PURE__ */ new Map();
658
+ for (const k of keys) {
659
+ const [date, startTime] = k.split("__");
660
+ const minute = toMinutes(startTime);
661
+ const arr = byDate.get(date) ?? [];
662
+ arr.push(minute);
663
+ byDate.set(date, arr);
664
+ }
665
+ const merged = [];
666
+ for (const [date, mins] of byDate.entries()) {
667
+ mins.sort((a, b) => a - b);
668
+ let rangeStart = mins[0];
669
+ let prev = mins[0];
670
+ for (let i = 1; i < mins.length; i += 1) {
671
+ const current = mins[i];
672
+ if (current === prev + slotGranularity) {
673
+ prev = current;
674
+ continue;
675
+ }
676
+ merged.push({ date, startTime: minutesToTime(rangeStart), endTime: minutesToTime(prev + slotGranularity) });
677
+ rangeStart = current;
678
+ prev = current;
679
+ }
680
+ merged.push({ date, startTime: minutesToTime(rangeStart), endTime: minutesToTime(prev + slotGranularity) });
681
+ }
682
+ return merged.sort((a, b) => a.date === b.date ? toMinutes(a.startTime) - toMinutes(b.startTime) : a.date.localeCompare(b.date));
683
+ }
684
+ function BookingCalendar({
685
+ value,
686
+ onChange,
687
+ schedules,
688
+ mode: schedulerMode = "time-grid",
689
+ dataAdapter,
690
+ dataSource,
691
+ ganttTimeUnit,
692
+ ganttScale,
693
+ viewMode,
694
+ onViewModeChange,
695
+ onSlotClick,
696
+ onItemClick,
697
+ onBookingClick,
698
+ onSlotMove,
699
+ onGanttItemMove,
700
+ onGanttItemResize,
701
+ onGanttItemCreate,
702
+ onBeforeSlotMove,
703
+ onSlotConflict,
704
+ draggableSlots = false,
705
+ selectionMode = false,
706
+ selectedSlots,
707
+ onSelectionChange,
708
+ onSlotDragSelectStart,
709
+ onSlotDragSelectMove,
710
+ onSlotDragSelectEnd,
711
+ isSlotSelected,
712
+ slotGranularity = 30,
713
+ locale = "fa-IR",
714
+ weekStartsOn = 6,
715
+ direction = "auto",
716
+ translations,
717
+ theme,
718
+ hideTimeColumn = false,
719
+ className
720
+ }) {
721
+ const [internalMode, setInternalMode] = useState2("week");
722
+ const [dragOverKey, setDragOverKey] = useState2(null);
723
+ const [internalSelectedKeys, setInternalSelectedKeys] = useState2(/* @__PURE__ */ new Set());
724
+ const liveSelectionRef = React2.useRef(/* @__PURE__ */ new Set());
725
+ const isSelectingRef = React2.useRef(false);
726
+ const [isSelecting, setIsSelecting] = useState2(false);
727
+ useEffect(() => {
728
+ if (typeof window === "undefined") return;
729
+ setInternalMode(window.innerWidth < 768 ? "day" : "week");
730
+ }, []);
731
+ useEffect(() => {
732
+ if (typeof window === "undefined") return;
733
+ const onUp = () => {
734
+ isSelectingRef.current = false;
735
+ setIsSelecting(false);
736
+ };
737
+ window.addEventListener("mouseup", onUp);
738
+ return () => window.removeEventListener("mouseup", onUp);
739
+ }, []);
740
+ const calendarMode = viewMode ?? internalMode;
741
+ const setCalendarMode = (m) => {
742
+ setInternalMode(m);
743
+ onViewModeChange?.(m);
744
+ };
745
+ const isRtlLocale = /^fa|^ar|^he/.test(locale.toLowerCase());
746
+ const resolvedDirection = direction === "auto" ? isRtlLocale ? "rtl" : "ltr" : direction;
747
+ const t = {
748
+ previous: isRtlLocale ? "\u0642\u0628\u0644\u06CC" : "Previous",
749
+ today: isRtlLocale ? "\u0627\u0645\u0631\u0648\u0632" : "Today",
750
+ next: isRtlLocale ? "\u0628\u0639\u062F\u06CC" : "Next",
751
+ day: isRtlLocale ? "\u0631\u0648\u0632" : "Day",
752
+ week: isRtlLocale ? "\u0647\u0641\u062A\u0647" : "Week",
753
+ ...translations
754
+ };
755
+ const ganttTranslations = useMemo2(() => {
756
+ const map = {};
757
+ if (t.previous) map.previous = t.previous;
758
+ if (t.today) map.today = t.today;
759
+ if (t.next) map.next = t.next;
760
+ return map;
761
+ }, [t.previous, t.today, t.next]);
762
+ const mergedTheme = { ...defaultTheme, ...theme };
763
+ const cssVars = {
764
+ ["--rbc-primary"]: mergedTheme.primary,
765
+ ["--rbc-bg"]: mergedTheme.bg,
766
+ ["--rbc-panel"]: mergedTheme.panel,
767
+ ["--rbc-border"]: mergedTheme.border,
768
+ ["--rbc-text"]: mergedTheme.text,
769
+ ["--rbc-muted"]: mergedTheme.mutedText,
770
+ ["--rbc-available"]: mergedTheme.availableBg,
771
+ ["--rbc-booked"]: mergedTheme.bookedBg,
772
+ ["--rbc-blocked"]: mergedTheme.blockedBg,
773
+ ["--rbc-custom"]: mergedTheme.customBg
774
+ };
775
+ const isGanttMode = schedulerMode === "task-timeline" || schedulerMode === "resource-planner";
776
+ const ganttData = useMemo2(() => {
777
+ if (!isGanttMode || !dataAdapter || dataSource === void 0) return null;
778
+ const adapter = dataAdapter;
779
+ if (typeof adapter.toGantt !== "function") return null;
780
+ return adapter.toGantt(dataSource, { date: value, locale });
781
+ }, [isGanttMode, dataAdapter, dataSource, value, locale]);
782
+ if (isGanttMode && ganttData) {
783
+ return /* @__PURE__ */ jsx2(
784
+ GanttScheduler,
785
+ {
786
+ schedulerMode,
787
+ rows: ganttData.rows,
788
+ items: ganttData.items,
789
+ date: value,
790
+ timeStart: ganttData.timeStart,
791
+ timeEnd: ganttData.timeEnd,
792
+ granularity: ganttData.granularity,
793
+ timeUnit: ganttTimeUnit ?? ganttData.timeUnit,
794
+ scale: ganttScale ?? ganttData.scale ?? (ganttData.timeUnit === "day" ? "week" : void 0),
795
+ weekStartsOn,
796
+ locale,
797
+ direction: direction === "auto" ? /^fa|^ar|^he/.test(locale.toLowerCase()) ? "rtl" : "ltr" : direction,
798
+ theme,
799
+ onItemClick,
800
+ onItemMove: onGanttItemMove,
801
+ onItemResize: onGanttItemResize,
802
+ onItemCreate: onGanttItemCreate,
803
+ translations: ganttTranslations,
804
+ onDateChange: onChange
805
+ }
806
+ );
807
+ }
808
+ const normalizedSchedules = useMemo2(() => {
809
+ if (dataAdapter && dataSource !== void 0 && !isGanttMode) {
810
+ const adapter = dataAdapter;
811
+ if (typeof adapter.toSchedules === "function") return adapter.toSchedules(dataSource);
812
+ }
813
+ return schedules;
814
+ }, [dataAdapter, dataSource, schedules, isGanttMode]);
815
+ const days = useMemo2(() => calendarMode === "week" ? getWeekDays(value, weekStartsOn) : [value], [calendarMode, value, weekStartsOn]);
816
+ const byDate = useMemo2(() => new Map(normalizedSchedules.map((d) => [d.date, d])), [normalizedSchedules]);
817
+ const bounds = useMemo2(() => getHourBounds(normalizedSchedules), [normalizedSchedules]);
818
+ const times = useMemo2(() => generateTimeSlots(bounds.start, bounds.end, slotGranularity), [bounds, slotGranularity]);
819
+ const title = calendarMode === "week" ? `${days[0].toLocaleDateString(locale)} - ${days[days.length - 1].toLocaleDateString(locale)}` : value.toLocaleDateString(locale, { weekday: "long", year: "numeric", month: "long", day: "numeric" });
820
+ const rowsHeight = 30;
821
+ const totalMinutes = (bounds.end - bounds.start) * 60;
822
+ const currentSelectionKeys = useMemo2(() => {
823
+ if (selectedSlots) {
824
+ const keys = /* @__PURE__ */ new Set();
825
+ for (const s of selectedSlots) {
826
+ const start = toMinutes(s.startTime);
827
+ const end = toMinutes(s.endTime);
828
+ for (let m = start; m < end; m += slotGranularity) {
829
+ keys.add(`${s.date}__${minutesToTime(m)}`);
830
+ }
831
+ }
832
+ return keys;
833
+ }
834
+ return internalSelectedKeys;
835
+ }, [selectedSlots, internalSelectedKeys, slotGranularity]);
836
+ const emitSelection = (keys) => {
837
+ const values = compressSelectionKeys(keys, slotGranularity);
838
+ onSelectionChange?.(values);
839
+ return values;
840
+ };
841
+ const addSelection = (date, startTime) => {
842
+ const key = `${date}__${startTime}`;
843
+ if (liveSelectionRef.current.has(key)) return;
844
+ liveSelectionRef.current = new Set(liveSelectionRef.current);
845
+ liveSelectionRef.current.add(key);
846
+ const endTime = minutesToTime(toMinutes(startTime) + slotGranularity);
847
+ onSlotDragSelectMove?.({ date, startTime, endTime });
848
+ if (selectedSlots) {
849
+ emitSelection(liveSelectionRef.current);
850
+ return;
851
+ }
852
+ const nextKeys = new Set(liveSelectionRef.current);
853
+ setInternalSelectedKeys(nextKeys);
854
+ setTimeout(() => emitSelection(nextKeys), 0);
855
+ };
856
+ const findConflict = (payload) => {
857
+ const targetStart = toMinutes(payload.to.startTime);
858
+ const targetEnd = toMinutes(payload.to.endTime);
859
+ const sameSlotMove = payload.from.date === payload.to.date && payload.from.startTime === payload.to.startTime && payload.from.endTime === payload.to.endTime;
860
+ if (sameSlotMove) return void 0;
861
+ const targetDay = byDate.get(payload.to.date);
862
+ if (!targetDay) return void 0;
863
+ return targetDay.slots.find((s) => {
864
+ if (payload.from.date === payload.to.date && s.startTime === payload.from.startTime && s.endTime === payload.from.endTime && (s.itemId ?? s.bookingId) === (payload.slot.itemId ?? payload.slot.bookingId)) {
865
+ return false;
866
+ }
867
+ const sStart = toMinutes(s.startTime);
868
+ const sEnd = toMinutes(s.endTime);
869
+ return rangesOverlap(targetStart, targetEnd, sStart, sEnd);
870
+ });
871
+ };
872
+ return /* @__PURE__ */ jsxs2(
873
+ "div",
874
+ {
875
+ className: `rbc-root rbc-model-${schedulerMode} ${className ?? ""} ${resolvedDirection === "rtl" ? "rbc-rtl" : "rbc-ltr"}`,
876
+ style: cssVars,
877
+ dir: resolvedDirection,
878
+ "data-scheduler-mode": schedulerMode,
879
+ children: [
880
+ /* @__PURE__ */ jsxs2("div", { className: "rbc-toolbar", children: [
881
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 8 }, children: [
882
+ /* @__PURE__ */ jsx2("button", { className: "rbc-btn", onClick: () => onChange(addDays(value, calendarMode === "week" ? -7 : -1)), children: t.previous }),
883
+ /* @__PURE__ */ jsx2("button", { className: "rbc-btn", onClick: () => onChange(/* @__PURE__ */ new Date()), children: t.today }),
884
+ /* @__PURE__ */ jsx2("button", { className: "rbc-btn", onClick: () => onChange(addDays(value, calendarMode === "week" ? 7 : 1)), children: t.next })
885
+ ] }),
886
+ /* @__PURE__ */ jsx2("strong", { children: title }),
887
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 6 }, children: [
888
+ /* @__PURE__ */ jsx2("button", { className: `rbc-btn ${calendarMode === "day" ? "active" : ""}`, onClick: () => setCalendarMode("day"), children: t.day }),
889
+ /* @__PURE__ */ jsx2("button", { className: `rbc-btn ${calendarMode === "week" ? "active" : ""}`, onClick: () => setCalendarMode("week"), children: t.week })
890
+ ] })
891
+ ] }),
892
+ /* @__PURE__ */ jsxs2("div", { className: "rbc-grid", style: {
893
+ ["--rbc-days"]: String(days.length),
894
+ ["--rbc-rows"]: String(times.length),
895
+ gridTemplateRows: `auto repeat(${times.length}, ${rowsHeight}px)`
896
+ }, children: [
897
+ !hideTimeColumn && /* @__PURE__ */ jsx2("div", { className: "rbc-day-head" }),
898
+ days.map((d) => {
899
+ const isToday = toIsoDate(d) === toIsoDate(/* @__PURE__ */ new Date());
900
+ return /* @__PURE__ */ jsx2("div", { className: `rbc-day-head ${isToday ? "today" : ""}`, children: d.toLocaleDateString(locale, { weekday: "short", month: "short", day: "numeric" }) }, d.toISOString());
901
+ }),
902
+ times.map((t2, rowIdx) => /* @__PURE__ */ jsxs2(React2.Fragment, { children: [
903
+ !hideTimeColumn && /* @__PURE__ */ jsx2(
904
+ "div",
905
+ {
906
+ className: "rbc-time",
907
+ style: { gridRow: rowIdx + 2, gridColumn: 1 },
908
+ children: t2
909
+ }
910
+ ),
911
+ days.map((d, colIdx) => {
912
+ const dayIso = toIsoDate(d);
913
+ const cellKey = `${dayIso}-${t2}`;
914
+ const tMin = toMinutes(t2);
915
+ const cellHasSlot = byDate.get(dayIso)?.slots.some(
916
+ (s) => toMinutes(s.startTime) <= tMin && tMin < toMinutes(s.endTime)
917
+ ) ?? false;
918
+ return /* @__PURE__ */ jsx2(
919
+ "div",
920
+ {
921
+ "data-slot-cell": "true",
922
+ "data-date": dayIso,
923
+ "data-start-time": t2,
924
+ className: `rbc-cell ${dragOverKey === cellKey ? "rbc-drop-target" : ""} ${selectionMode && !cellHasSlot && (isSlotSelected?.({ date: dayIso, startTime: t2, endTime: minutesToTime(tMin + slotGranularity) }) || currentSelectionKeys.has(`${dayIso}__${t2}`)) ? "rbc-cell-selected" : ""}`,
925
+ style: { height: rowsHeight, gridRow: rowIdx + 2, gridColumn: (hideTimeColumn ? 1 : 2) + colIdx },
926
+ onMouseDown: (e) => {
927
+ if (!selectionMode || e.button !== 0 || cellHasSlot) return;
928
+ liveSelectionRef.current = /* @__PURE__ */ new Set();
929
+ setInternalSelectedKeys(/* @__PURE__ */ new Set());
930
+ isSelectingRef.current = true;
931
+ setIsSelecting(true);
932
+ const endTime = minutesToTime(tMin + slotGranularity);
933
+ onSlotDragSelectStart?.({ date: dayIso, startTime: t2, endTime });
934
+ addSelection(dayIso, t2);
935
+ },
936
+ onMouseEnter: () => {
937
+ if (!selectionMode || !isSelectingRef.current || cellHasSlot) return;
938
+ addSelection(dayIso, t2);
939
+ },
940
+ onMouseUp: () => {
941
+ if (!selectionMode || !isSelectingRef.current) return;
942
+ isSelectingRef.current = false;
943
+ setIsSelecting(false);
944
+ const keys = liveSelectionRef.current;
945
+ const final = compressSelectionKeys(keys, slotGranularity);
946
+ liveSelectionRef.current = /* @__PURE__ */ new Set();
947
+ onSlotDragSelectEnd?.(final);
948
+ }
949
+ },
950
+ `${t2}-${d.toISOString()}`
951
+ );
952
+ })
953
+ ] }, t2)),
954
+ !hideTimeColumn && /* @__PURE__ */ jsx2(
955
+ "div",
956
+ {
957
+ className: "rbc-empty-col",
958
+ style: { gridRow: `2 / span ${times.length}`, gridColumn: 1 }
959
+ }
960
+ ),
961
+ days.map((d, i) => {
962
+ const dayIso = toIsoDate(d);
963
+ const day = byDate.get(dayIso);
964
+ const calcTimeFromY = (e) => {
965
+ const rect = e.currentTarget.getBoundingClientRect();
966
+ const fraction = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height));
967
+ const mins = Math.floor(fraction * totalMinutes / slotGranularity) * slotGranularity;
968
+ return minutesToTime(bounds.start * 60 + mins);
969
+ };
970
+ return /* @__PURE__ */ jsx2(
971
+ "div",
972
+ {
973
+ className: "rbc-empty-col",
974
+ style: {
975
+ gridRow: `2 / span ${times.length}`,
976
+ gridColumn: (hideTimeColumn ? 1 : 2) + i,
977
+ pointerEvents: draggableSlots ? "auto" : "none"
978
+ },
979
+ onDragOver: (e) => {
980
+ if (!draggableSlots) return;
981
+ e.preventDefault();
982
+ const t2 = calcTimeFromY(e);
983
+ setDragOverKey(`${dayIso}-${t2}`);
984
+ },
985
+ onDragLeave: () => setDragOverKey(null),
986
+ onDrop: (e) => {
987
+ if (!draggableSlots || !onSlotMove) return;
988
+ e.preventDefault();
989
+ setDragOverKey(null);
990
+ const raw = e.dataTransfer.getData("application/x-rbc-slot");
991
+ if (!raw) return;
992
+ const payload = JSON.parse(raw);
993
+ const targetStart = calcTimeFromY(e);
994
+ const duration = toMinutes(payload.slot.endTime) - toMinutes(payload.slot.startTime);
995
+ if (duration <= 0) return;
996
+ const movePayload = {
997
+ slot: payload.slot,
998
+ from: { date: payload.date, startTime: payload.slot.startTime, endTime: payload.slot.endTime },
999
+ to: { date: dayIso, startTime: targetStart, endTime: minutesToTime(toMinutes(targetStart) + duration) }
1000
+ };
1001
+ const conflict = findConflict(movePayload);
1002
+ if (conflict) {
1003
+ onSlotConflict?.({ ...movePayload, reason: "overlap", conflictingSlot: conflict });
1004
+ return;
1005
+ }
1006
+ Promise.resolve(onBeforeSlotMove?.(movePayload) ?? true).then((allowed) => {
1007
+ if (!allowed) {
1008
+ onSlotConflict?.({ ...movePayload, reason: "blocked-by-policy" });
1009
+ return;
1010
+ }
1011
+ onSlotMove(movePayload);
1012
+ });
1013
+ },
1014
+ children: (day?.slots ?? []).map((slot, i2) => {
1015
+ const start = toMinutes(slot.startTime);
1016
+ const end = toMinutes(slot.endTime);
1017
+ const top = (start - bounds.start * 60) / totalMinutes * 100;
1018
+ const height = (end - start) / totalMinutes * 100;
1019
+ return /* @__PURE__ */ jsxs2(
1020
+ "div",
1021
+ {
1022
+ "data-slot-item": "true",
1023
+ "data-date": dayIso,
1024
+ "data-start-time": slot.startTime,
1025
+ "data-end-time": slot.endTime,
1026
+ className: slotClass(slot.status),
1027
+ style: { top: `${top}%`, height: `${height}%` },
1028
+ draggable: draggableSlots,
1029
+ onDragStart: (e) => {
1030
+ if (!draggableSlots) return;
1031
+ e.stopPropagation();
1032
+ e.dataTransfer.effectAllowed = "move";
1033
+ e.dataTransfer.setData("application/x-rbc-slot", JSON.stringify({ slot, date: dayIso }));
1034
+ },
1035
+ onClick: () => {
1036
+ const id = slot.itemId ?? slot.bookingId;
1037
+ if (id) {
1038
+ onItemClick?.(id);
1039
+ onBookingClick?.(id);
1040
+ return;
1041
+ }
1042
+ onSlotClick?.(dayIso, slot);
1043
+ },
1044
+ role: "button",
1045
+ tabIndex: 0,
1046
+ onKeyDown: (e) => {
1047
+ if (e.key !== "Enter" && e.key !== " ") return;
1048
+ e.preventDefault();
1049
+ const id = slot.itemId ?? slot.bookingId;
1050
+ if (id) {
1051
+ onItemClick?.(id);
1052
+ onBookingClick?.(id);
1053
+ return;
1054
+ }
1055
+ onSlotClick?.(dayIso, slot);
1056
+ },
1057
+ children: [
1058
+ /* @__PURE__ */ jsx2("div", { className: "rbc-label", children: slot.title ?? `${slot.startTime} - ${slot.endTime}` }),
1059
+ /* @__PURE__ */ jsx2("div", { className: "rbc-sub", children: slot.description ?? slot.status })
1060
+ ]
1061
+ },
1062
+ `${slot.startTime}-${slot.endTime}-${i2}`
1063
+ );
1064
+ })
1065
+ },
1066
+ `slots-${dayIso}`
1067
+ );
1068
+ })
1069
+ ] })
1070
+ ]
1071
+ }
1072
+ );
1073
+ }
1074
+ var BookingCalendar_default = BookingCalendar;
1075
+
1076
+ // src/adapters.ts
1077
+ function defStatus(s) {
1078
+ return s ?? "custom";
1079
+ }
1080
+ function createTaskTimelineAdapter() {
1081
+ return {
1082
+ toGantt(items, { date }) {
1083
+ const assigneeOrder = [];
1084
+ const seen = /* @__PURE__ */ new Set();
1085
+ for (const item of items) {
1086
+ const key = item.assignee ?? item.title;
1087
+ if (!seen.has(key)) {
1088
+ seen.add(key);
1089
+ assigneeOrder.push(key);
1090
+ }
1091
+ }
1092
+ const rows = assigneeOrder.map((a) => ({ id: a, label: a }));
1093
+ const ganttItems = items.map((item) => ({
1094
+ id: item.id,
1095
+ rowId: item.assignee ?? item.title,
1096
+ date: item.date,
1097
+ endDate: item.endDate,
1098
+ startTime: item.startTime,
1099
+ endTime: item.endTime,
1100
+ title: item.title,
1101
+ subTitle: item.progress != null ? `${item.progress}%` : item.assignee,
1102
+ status: defStatus(item.status)
1103
+ }));
1104
+ const todayStr = date.toISOString().slice(0, 10);
1105
+ const todayItems = ganttItems.filter((i) => i.date === todayStr);
1106
+ const allItems = ganttItems;
1107
+ const starts = allItems.map((i) => toMin2(i.startTime));
1108
+ const ends = allItems.map((i) => toMin2(i.endTime));
1109
+ const timeStart = starts.length ? Math.max(0, Math.floor(Math.min(...starts) / 60) - 1) : 8;
1110
+ const timeEnd = ends.length ? Math.min(24, Math.ceil(Math.max(...ends) / 60) + 1) : 20;
1111
+ return { rows, items: ganttItems, timeStart, timeEnd, granularity: 30, timeUnit: "hour" };
1112
+ }
1113
+ };
1114
+ }
1115
+ function createResourcePlannerAdapter(resources) {
1116
+ return {
1117
+ toGantt(items) {
1118
+ const rows = resources.map((r) => ({
1119
+ id: r.id,
1120
+ label: r.title,
1121
+ subLabel: r.meta?.type
1122
+ }));
1123
+ const ganttItems = items.map((item) => ({
1124
+ id: item.id,
1125
+ rowId: item.resourceId,
1126
+ date: item.date,
1127
+ startTime: item.startTime,
1128
+ endTime: item.endTime,
1129
+ title: item.title,
1130
+ subTitle: item.description,
1131
+ status: defStatus(item.status)
1132
+ }));
1133
+ const starts = ganttItems.map((i) => toMin2(i.startTime));
1134
+ const ends = ganttItems.map((i) => toMin2(i.endTime));
1135
+ const timeStart = starts.length ? Math.max(0, Math.floor(Math.min(...starts) / 60) - 1) : 8;
1136
+ const timeEnd = ends.length ? Math.min(24, Math.ceil(Math.max(...ends) / 60) + 1) : 20;
1137
+ return { rows, items: ganttItems, timeStart, timeEnd, granularity: 30, timeUnit: "day" };
1138
+ }
1139
+ };
1140
+ }
1141
+ function toMin2(t) {
1142
+ const [h, m] = t.split(":").map(Number);
1143
+ return h * 60 + m;
1144
+ }
1145
+ export {
1146
+ BookingCalendar,
1147
+ BookingCalendar_default as BookingCalendarDefault,
1148
+ BookingCalendar as SlotCalendar,
1149
+ BookingCalendar as SlotScheduler,
1150
+ createResourcePlannerAdapter,
1151
+ createTaskTimelineAdapter,
1152
+ defaultTheme
1153
+ };
1154
+ //# sourceMappingURL=index.js.map