akfatimeline 1.0.6 → 1.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.
Files changed (128) hide show
  1. package/CHANGELOG.md +98 -35
  2. package/dist/Timeline.js +4309 -1677
  3. package/dist/components/Timeline/AutocompleteSelect.js +150 -0
  4. package/dist/components/Timeline/ContextMenu.js +149 -0
  5. package/dist/components/Timeline/DailyView.js +255 -0
  6. package/dist/components/Timeline/DatePickerComponent.js +13 -0
  7. package/{public/dist/dist → dist}/components/Timeline/DragAndDropHandler.js +34 -34
  8. package/dist/components/Timeline/EventBadge.js +26 -0
  9. package/dist/components/Timeline/EventDetailModal.js +138 -0
  10. package/dist/components/Timeline/EventIcon.js +47 -0
  11. package/dist/{dist/components → components}/Timeline/EventTooltip.js +206 -206
  12. package/dist/components/Timeline/FilterPanel.js +179 -0
  13. package/dist/{dist/components → components}/Timeline/Indicator.js +26 -26
  14. package/dist/components/Timeline/LoadingSpinner.js +48 -0
  15. package/dist/{dist/components → components}/Timeline/MasterHeader.js +104 -68
  16. package/{public/dist/dist → dist}/components/Timeline/Resources.js +53 -53
  17. package/dist/{dist/components → components}/Timeline/ResourcesHeader.js +14 -14
  18. package/dist/components/Timeline/Timeline.css +2491 -0
  19. package/dist/components/Timeline/Timeline.js +607 -0
  20. package/dist/{dist/components → components}/Timeline/TimelineCell.js +8 -8
  21. package/dist/components/Timeline/TimelineContent.js +838 -0
  22. package/{public/dist/dist → dist}/components/Timeline/TimelineEvents.js +114 -114
  23. package/dist/components/Timeline/TimelineHeader.js +54 -0
  24. package/{public/dist/dist → dist}/components/Timeline/TimelineMonthContainer.js +29 -29
  25. package/{public/dist/dist → dist}/components/Timeline/TimelineResources.js +16 -16
  26. package/{public/dist/dist → dist}/hooks/useDragAndDrop.js +80 -80
  27. package/dist/{dist/hooks → hooks}/useEventDragDrop.js +126 -126
  28. package/dist/hooks/useEventManagement.js +173 -0
  29. package/dist/hooks/useEventSelection.js +82 -0
  30. package/{public/dist/dist → dist}/hooks/useExtendEvent.js +28 -28
  31. package/dist/hooks/useKeyboardShortcuts.js +158 -0
  32. package/dist/hooks/useTouchGestures.js +90 -0
  33. package/dist/utils/conflictUtils.js +105 -0
  34. package/dist/{dist/utils → utils}/dateUtils.js +36 -36
  35. package/dist/{dist/utils → utils}/filterTimelineData.js +20 -20
  36. package/dist/utils/filterUtils.js +106 -0
  37. package/dist/utils/timeUtils.js +179 -0
  38. package/dist/{dist/utils → utils}/timelineUtils.js +39 -39
  39. package/dist/utils/viewModeUtils.js +54 -0
  40. package/package.json +89 -19
  41. package/src/App.js +300 -19
  42. package/src/components/Timeline/AutocompleteSelect.js +150 -0
  43. package/src/components/Timeline/ContextMenu.js +149 -0
  44. package/src/components/Timeline/DailyView.js +255 -0
  45. package/src/components/Timeline/DatePickerComponent.js +13 -17
  46. package/src/components/Timeline/DragAndDropHandler.js +34 -34
  47. package/src/components/Timeline/EventBadge.js +26 -0
  48. package/src/components/Timeline/EventDetailModal.js +138 -0
  49. package/src/components/Timeline/EventIcon.js +47 -0
  50. package/src/components/Timeline/EventTooltip.js +206 -206
  51. package/src/components/Timeline/FilterPanel.js +179 -0
  52. package/src/components/Timeline/Indicator.js +26 -26
  53. package/src/components/Timeline/LoadingSpinner.js +48 -0
  54. package/src/components/Timeline/MasterHeader.js +104 -68
  55. package/src/components/Timeline/Resources.js +53 -53
  56. package/src/components/Timeline/ResourcesHeader.js +14 -14
  57. package/src/components/Timeline/Timeline.css +2491 -616
  58. package/src/components/Timeline/Timeline.js +607 -309
  59. package/src/components/Timeline/TimelineCell.js +8 -8
  60. package/src/components/Timeline/TimelineContent.js +838 -446
  61. package/src/components/Timeline/TimelineEvents.js +114 -114
  62. package/src/components/Timeline/TimelineHeader.js +54 -43
  63. package/src/components/Timeline/TimelineMonthContainer.js +29 -29
  64. package/src/components/Timeline/TimelineResources.js +16 -16
  65. package/src/demo.css +4 -0
  66. package/src/hooks/useDragAndDrop.js +80 -80
  67. package/src/hooks/useEventDragDrop.js +126 -126
  68. package/src/hooks/useEventManagement.js +173 -0
  69. package/src/hooks/useEventSelection.js +82 -0
  70. package/src/hooks/useExtendEvent.js +28 -28
  71. package/src/hooks/useKeyboardShortcuts.js +158 -0
  72. package/src/hooks/useTouchGestures.js +90 -0
  73. package/src/index.js +1 -7
  74. package/src/library.js +26 -0
  75. package/src/utils/conflictUtils.js +105 -0
  76. package/src/utils/dateUtils.js +36 -36
  77. package/src/utils/filterTimelineData.js +20 -20
  78. package/src/utils/filterUtils.js +106 -0
  79. package/src/utils/timeUtils.js +179 -0
  80. package/src/utils/timelineUtils.js +39 -39
  81. package/src/utils/viewModeUtils.js +54 -0
  82. package/.babelrc +0 -6
  83. package/babel.config.json +0 -4
  84. package/dist/dist/components/Timeline/DatePickerComponent.js +0 -17
  85. package/dist/dist/components/Timeline/DragAndDropHandler.js +0 -35
  86. package/dist/dist/components/Timeline/Resources.js +0 -53
  87. package/dist/dist/components/Timeline/Timeline.css +0 -616
  88. package/dist/dist/components/Timeline/Timeline.js +0 -309
  89. package/dist/dist/components/Timeline/TimelineContent.js +0 -446
  90. package/dist/dist/components/Timeline/TimelineEvents.js +0 -114
  91. package/dist/dist/components/Timeline/TimelineHeader.js +0 -43
  92. package/dist/dist/components/Timeline/TimelineMonthContainer.js +0 -29
  93. package/dist/dist/components/Timeline/TimelineResources.js +0 -16
  94. package/dist/dist/hooks/useDragAndDrop.js +0 -80
  95. package/dist/dist/hooks/useExtendEvent.js +0 -28
  96. package/public/dist/Timeline.js +0 -3277
  97. package/public/dist/dist/components/Timeline/DatePickerComponent.js +0 -17
  98. package/public/dist/dist/components/Timeline/EventTooltip.js +0 -206
  99. package/public/dist/dist/components/Timeline/Indicator.js +0 -29
  100. package/public/dist/dist/components/Timeline/MasterHeader.js +0 -68
  101. package/public/dist/dist/components/Timeline/ResourcesHeader.js +0 -14
  102. package/public/dist/dist/components/Timeline/Timeline.css +0 -616
  103. package/public/dist/dist/components/Timeline/Timeline.js +0 -304
  104. package/public/dist/dist/components/Timeline/TimelineCell.js +0 -8
  105. package/public/dist/dist/components/Timeline/TimelineContent.js +0 -447
  106. package/public/dist/dist/components/Timeline/TimelineHeader.js +0 -43
  107. package/public/dist/dist/hooks/useEventDragDrop.js +0 -126
  108. package/public/dist/dist/utils/HorizontalVirtualScroll.js +0 -0
  109. package/public/dist/dist/utils/dateUtils.js +0 -36
  110. package/public/dist/dist/utils/filterTimelineData.js +0 -21
  111. package/public/dist/dist/utils/timelineUtils.js +0 -40
  112. package/public/favicon.ico +0 -0
  113. package/public/index kutuphane /304/261c/304/261n.html" +0 -43
  114. package/public/index tasarim icin.html +0 -20
  115. package/public/index.html +0 -43
  116. package/public/logo192.png +0 -0
  117. package/public/logo512.png +0 -0
  118. package/public/manifest.json +0 -25
  119. package/public/robots.txt +0 -3
  120. package/src/App.css +0 -38
  121. package/src/App.test.js +0 -8
  122. package/src/dist/Timeline.js +0 -277
  123. package/src/index.css +0 -13
  124. package/src/logo.svg +0 -1
  125. package/src/reportWebVitals.js +0 -13
  126. package/src/setupTests.js +0 -5
  127. package/webpack.config.js +0 -49
  128. /package/dist/{dist/utils → utils}/HorizontalVirtualScroll.js +0 -0
@@ -1,446 +1,838 @@
1
- import React, { useState, useRef, useEffect } from "react";
2
- import { parseDate } from "../../utils/dateUtils";
3
- import useDragAndDrop from "../../hooks/useDragAndDrop";
4
- import useEventDragDrop from "../../hooks/useEventDragDrop";
5
- import Indicator from "./Indicator";
6
- import useExtendEvent from "../../hooks/useExtendEvent";
7
- // import "./Timeline.css"; // varsayalım "Timeline.css" globalde import ediliyor
8
-
9
- const TimelineContent = ({
10
- groupedResources,
11
- dates,
12
- collapsedGroups,
13
- events,
14
- setEvents,
15
- onEventClick,
16
- todayIndex,
17
- indicatorOn,
18
- resourceSettings,
19
- setDropInfo,
20
-
21
-
22
- eventsDragOn = true,
23
- eventsExtendOn = true,
24
- createNewEventOn = true,
25
-
26
- onExtendInfo,
27
- onCreateEventInfo,
28
- onEventRightClick,
29
-
30
- eventTooltipOn = true,
31
- tooltipComponent: TooltipComponent,
32
- tempEventStyle = {},
33
-
34
- eventStyleResolver = () => ({}),
35
- }) => {
36
- // ------------------- HOOKS & STATE -------------------
37
- const containerRef = useRef(null);
38
-
39
- // Drag
40
- const { isDragging, dragStart, dragEnd } = useDragAndDrop(events, setEvents);
41
- const { handleDragStart, handleDragOver, handleDrop, handleDragEnd } = useEventDragDrop(
42
- events,
43
- setEvents,
44
- setDropInfo // Doğrudan setDropInfo'yu geçiriyoruz
45
- );
46
-
47
-
48
-
49
-
50
-
51
- // Extend
52
- const { extendEvent } = useExtendEvent(events, setEvents);
53
- const [mode, setMode] = useState(null); // null | "extend"
54
- const [extendingEvent, setExtendingEvent] = useState(null);
55
- const [originalEndDate, setOriginalEndDate] = useState(null);
56
- const [startMouseX, setStartMouseX] = useState(null);
57
-
58
- // Create new event
59
- const [isCreating, setIsCreating] = useState(false);
60
- const [tempEvent, setTempEvent] = useState(null);
61
-
62
- // Tooltip
63
- const [selectedEvent, setSelectedEvent] = useState(null);
64
- const [tooltipPosition, setTooltipPosition] = useState({ top: 0, left: 0 });
65
-
66
- const totalDays = dates.length;
67
-
68
- // ------------------- Tooltip Logic -------------------
69
- const handleEventClickInternal = (event, e) => {
70
- e.stopPropagation();
71
- // Eğer mod "extend" ise tooltip'i açma
72
- if (mode === "extend") {
73
- return;
74
- }
75
-
76
- // Harici callback
77
- if (onEventClick) onEventClick(event, e);
78
-
79
- // Tooltip göstermek
80
- const eventElement = e.currentTarget;
81
- if (eventElement) {
82
- const rect = eventElement.getBoundingClientRect();
83
- setTooltipPosition({
84
- top: rect.top + window.scrollY,
85
- left: rect.left + rect.width / 2 + window.scrollX,
86
- });
87
- setSelectedEvent(event);
88
- }
89
- };
90
-
91
-
92
- const handleCloseTooltip = () => {
93
- setSelectedEvent(null);
94
- };
95
-
96
- // ------------------- Create New Event -------------------
97
- const handleCellClick = (resourceId, date) => {
98
- if (!createNewEventOn) return; // create devrede değilse
99
-
100
- const startDate = parseDate(date.fullDate);
101
- const newEvent = {
102
- id: Date.now(),
103
- title: "1 Gece",
104
- startDate,
105
- endDate: new Date(startDate.getTime() + 24 * 60 * 60 * 1000),
106
- resourceId,
107
- // color => var(--timeline-new-event-background-color) => => Sonra inline style yerine className
108
- color: "", // Bunu .css’te "var(--timeline-new-event-background-color)" atayabilirsin
109
- };
110
- setTempEvent(newEvent);
111
- setIsCreating(true);
112
- };
113
-
114
- useEffect(() => {
115
- if (!createNewEventOn) return;
116
- if (!isCreating) return;
117
- if (mode === "extend") {
118
- console.log(">>> 'extend' mode, skip new event creation");
119
- return;
120
- }
121
-
122
- const handleMouseMove = (e) => {
123
- if (!isCreating || !tempEvent) return;
124
- const cell = document.elementFromPoint(e.clientX, e.clientY);
125
- const cellW = cell?.offsetWidth || 30;
126
-
127
- const startX = tempEvent.startX || e.clientX;
128
- const deltaX = e.clientX - startX;
129
- const daysToAdd = Math.max(1, Math.floor(deltaX / cellW));
130
-
131
- const newEndDate = new Date(tempEvent.startDate.getTime());
132
- newEndDate.setDate(newEndDate.getDate() + daysToAdd);
133
-
134
- setTempEvent({
135
- ...tempEvent,
136
- endDate: newEndDate,
137
- startX: startX,
138
- title: `${daysToAdd} Gece`,
139
- });
140
- };
141
-
142
- const handleMouseUp = () => {
143
- if (isCreating && tempEvent) {
144
- setEvents([...events, tempEvent]);
145
- if (onCreateEventInfo) {
146
- onCreateEventInfo(tempEvent);
147
- }
148
- }
149
- setTempEvent(null);
150
- setIsCreating(false);
151
- };
152
-
153
- window.addEventListener("mousemove", handleMouseMove);
154
- window.addEventListener("mouseup", handleMouseUp);
155
-
156
- return () => {
157
- window.removeEventListener("mousemove", handleMouseMove);
158
- window.removeEventListener("mouseup", handleMouseUp);
159
- };
160
- }, [createNewEventOn, isCreating, mode, tempEvent, events, onCreateEventInfo, setEvents]);
161
-
162
- // ------------------- Drag Logic -------------------
163
- const handleDragStartSafe = (e, eventId) => {
164
- if (!eventsDragOn) {
165
- e.preventDefault();
166
- return;
167
- }
168
- handleDragStart(e, eventId);
169
- };
170
- const handleDragEndSafe = (e) => {
171
- if (!eventsDragOn) {
172
- e.preventDefault();
173
- return;
174
- }
175
- handleDragEnd();
176
-
177
-
178
- };
179
-
180
-
181
-
182
- // ------------------- Extend Logic -------------------
183
- const handleMouseDownExtend = (mouseEvent, event) => {
184
- if (!eventsExtendOn) return;
185
- mouseEvent.stopPropagation();
186
- console.log(">>> Extend start ID:", event.id);
187
- setMode("extend");
188
- setExtendingEvent(event);
189
- setOriginalEndDate(event.endDate);
190
- setStartMouseX(mouseEvent.clientX);
191
- };
192
-
193
- const handleMouseMoveExtend = (e) => {
194
- if (mode !== "extend" || !extendingEvent) return;
195
- if (!eventsExtendOn) return;
196
-
197
- const currentMouseX = e.clientX;
198
- const deltaX = currentMouseX - (startMouseX ?? 0);
199
- const cellW = 30;
200
- const daysToAdd = Math.floor(deltaX / cellW);
201
-
202
- const newEndDate = new Date((originalEndDate ?? new Date()).getTime());
203
- newEndDate.setDate(newEndDate.getDate() + daysToAdd);
204
-
205
-
206
- setEvents((prev) =>
207
- prev.map((evt) => (evt.id === extendingEvent.id ? { ...evt, endDate: newEndDate } : evt))
208
- );
209
- };
210
-
211
- const handleMouseUpExtend = () => {
212
- console.log(">>> Extend finished ID:", extendingEvent?.id);
213
- if (onExtendInfo && extendingEvent) {
214
- // callback
215
- const updatedEvent = events.find((ev) => ev.id === extendingEvent.id);
216
- if (updatedEvent) {
217
- onExtendInfo({
218
- eventId: extendingEvent.id,
219
- newEndDate: updatedEvent.endDate,
220
- });
221
- }
222
- }
223
-
224
- // Tooltip açılmasını engellemek için modun null olmasını geciktiriyoruz
225
- setTimeout(() => {
226
- setMode(null);
227
- }, 100); // 100ms gecikme
228
- setExtendingEvent(null);
229
- setOriginalEndDate(null);
230
- setStartMouseX(null);
231
- };
232
-
233
-
234
- useEffect(() => {
235
- if (mode === "extend") {
236
- const onMove = (e) => handleMouseMoveExtend(e);
237
- const onUp = () => handleMouseUpExtend();
238
- document.addEventListener("mousemove", onMove);
239
- document.addEventListener("mouseup", onUp);
240
- return () => {
241
- document.removeEventListener("mousemove", onMove);
242
- document.removeEventListener("mouseup", onUp);
243
- };
244
- }
245
- }, [mode, extendingEvent, eventsExtendOn, originalEndDate, startMouseX]);
246
-
247
- // ------------------- Right Click (context) -------------------
248
- const handleRightClickEvent = (evt, reactEvent) => {
249
- reactEvent.preventDefault();
250
- if (onEventRightClick) onEventRightClick(evt, reactEvent);
251
- };
252
-
253
- // ------------------- Helper isCellSelected -------------------
254
- const isCellSelected = (resourceId, date) => {
255
- if (!dragStart || !dragEnd) return false;
256
- if (resourceId !== dragStart.resourceId) return false;
257
-
258
- const startIndex = dates.findIndex((d) => parseDate(d.fullDate).getTime() === parseDate(dragStart.date).getTime());
259
- const endIndex = dates.findIndex((d) => parseDate(d.fullDate).getTime() === parseDate(dragEnd.date).getTime());
260
- const currentIndex = dates.findIndex((d) => parseDate(d.fullDate).getTime() === parseDate(date.fullDate).getTime());
261
-
262
- if (startIndex === -1 || endIndex === -1 || currentIndex === -1) return false;
263
-
264
- return currentIndex >= Math.min(startIndex, endIndex) && currentIndex <= Math.max(startIndex, endIndex);
265
- };
266
-
267
- // ------------------- calculatePosition -------------------
268
- const calculatePosition = (ev, dateArr) => {
269
- const startDate = parseDate(ev.startDate);
270
- const endDate = parseDate(ev.endDate);
271
-
272
- const startIndex = dateArr.findIndex((d) => parseDate(d.fullDate).toDateString() === startDate.toDateString());
273
- const endIndex = dateArr.findIndex((d) => parseDate(d.fullDate).toDateString() === endDate.toDateString());
274
-
275
- const totalDays = dateArr.length;
276
- if (startIndex < 0 && endIndex < 0) {
277
- return { isVisible: false, left: 0, width: 0, isPartialStart: false, isPartialEnd: false };
278
- }
279
- if (startIndex >= totalDays && endIndex >= totalDays) {
280
- return { isVisible: false, left: 0, width: 0, isPartialStart: false, isPartialEnd: false };
281
- }
282
-
283
- const effectiveStartIndex = Math.max(startIndex, 0);
284
- const effectiveEndIndex = Math.min(endIndex, totalDays - 1);
285
-
286
- const isPartialStart = startIndex < 0;
287
- const isPartialEnd = endIndex >= totalDays;
288
-
289
- const leftPercentage = ((effectiveStartIndex + (isPartialStart ? 0 : 0.5)) / totalDays) * 100;
290
- const rightPercentage = ((effectiveEndIndex + (isPartialEnd ? 1 : 0.5)) / totalDays) * 100;
291
- const widthPercentage = rightPercentage - leftPercentage;
292
-
293
- return {
294
- isVisible: true,
295
- left: `${leftPercentage}%`,
296
- width: `${widthPercentage}%`,
297
- isPartialStart,
298
- isPartialEnd,
299
- };
300
- };
301
-
302
-
303
-
304
-
305
-
306
-
307
- // ------------------- RENDER -------------------
308
- return (
309
- <div
310
- ref={containerRef}
311
- className="timeline-content-container" // Yeni class, stilini timeline.css'e ekleyebilirsin
312
- >
313
- {indicatorOn && (
314
- <Indicator todayIndex={todayIndex} totalDays={totalDays} />
315
- )}
316
-
317
- {groupedResources.map((group, groupIndex) => (
318
- <div key={groupIndex} className="timeline-group-container">
319
- {/* Grup Başlığı */}
320
- {resourceSettings.isGrouped && (
321
- <div className="timeline-group-header-row">
322
- {dates.map((dateObj, colIndex) => (
323
- <div
324
- key={`group-header-${groupIndex}-${colIndex}`}
325
- className="timeline-group-header-cell"
326
- ></div>
327
- ))}
328
- </div>
329
- )}
330
-
331
- {/* Kaynaklar */}
332
- {!collapsedGroups[group.groupName] &&
333
- group.resources.map((resource, rowIndex) => {
334
- const resourceEvents = events.filter((ev) => ev.resourceId === resource.id);
335
-
336
- return (
337
- <div key={resource.id} className="timeline-resource-row">
338
- {/* Her resource row'u */}
339
- {resourceEvents.map((event) => {
340
- const { isVisible, left, width, isPartialStart, isPartialEnd } =
341
- calculatePosition(event, dates);
342
- if (!isVisible) return null;
343
-
344
- // Kullanıcıdan gelen stil
345
- const eventStyle = eventStyleResolver ? eventStyleResolver(event) : {};
346
-
347
- return (
348
- <div
349
- key={event.id}
350
- className="timeline-event"
351
- draggable={mode !== "extend" && eventsDragOn}
352
- onDragStart={(e) => {
353
- if (mode === "extend") {
354
- e.preventDefault();
355
- return;
356
- }
357
- handleDragStartSafe(e, event.id);
358
- }}
359
- onDragEnd={(e) => {
360
- if (mode === "extend") {
361
- e.preventDefault();
362
- return;
363
- }
364
- handleDragEndSafe(e);
365
- }}
366
- onContextMenu={(reactEvent) => handleRightClickEvent(event, reactEvent)}
367
- onClick={(ev) => handleEventClickInternal(event, ev)}
368
- style={{
369
- left,
370
- width,
371
- top: "5px",
372
- borderTopLeftRadius: isPartialStart ? "0px" : "20px",
373
- borderBottomLeftRadius: isPartialStart ? "0px" : "20px",
374
- borderTopRightRadius: isPartialEnd ? "0px" : "20px",
375
- borderBottomRightRadius: isPartialEnd ? "0px" : "20px",
376
- cursor: mode === "extend" ? "col-resize" : "grab",
377
- ...eventStyle, // Kullanıcı tarafından tanımlanan stiller
378
- }}
379
- >
380
- {event.title}
381
- {eventsExtendOn && (
382
- <div
383
- className="timeline-event-extend-handle"
384
- onMouseDown={(mouseEvent) => {
385
- mouseEvent.stopPropagation();
386
- handleMouseDownExtend(mouseEvent, event);
387
- }}
388
- ></div>
389
- )}
390
- </div>
391
- );
392
- })}
393
-
394
- {/* Geçici (yeni) event */}
395
- {tempEvent && tempEvent.resourceId === resource.id && (
396
- <div
397
- className="timeline-temp-event"
398
- style={{
399
- ...calculatePosition(tempEvent, dates),
400
- ...tempEventStyle, // Kullanıcının geçtiği stiller
401
- }}
402
- >
403
- {tempEvent.title}
404
- </div>
405
- )}
406
-
407
- {/* Tarih Hücreleri */}
408
- {dates.map((dateObj, colIndex) => (
409
- <div
410
- key={`cell-${groupIndex}-${rowIndex}-${colIndex}`}
411
- className={`timeline-cell ${
412
- isCellSelected(resource.id, dateObj) ? "selected" : ""
413
- }`}
414
- data-date={JSON.stringify(dateObj)}
415
- data-resource-id={resource.id}
416
- onMouseDown={() => handleCellClick(resource.id, dateObj)}
417
- onDragOver={(e) => handleDragOver(e)}
418
- onDrop={(e) =>
419
- handleDrop(e, resource.id, parseDate(dateObj.fullDate))
420
- }
421
- ></div>
422
- ))}
423
- </div>
424
- );
425
- })}
426
- </div>
427
- ))}
428
-
429
-
430
- {/* Tooltip vb. */}
431
- {eventTooltipOn && selectedEvent && TooltipComponent && mode !== "extend" && (
432
- <TooltipComponent
433
- event={selectedEvent}
434
- position={tooltipPosition}
435
- onClose={handleCloseTooltip}
436
- />
437
- )}
438
-
439
-
440
-
441
-
442
- </div>
443
- );
444
- };
445
-
446
- export default TimelineContent;
1
+ import React, { useState, useRef, useEffect, useCallback } from "react";
2
+ import { parseDate } from "../../utils/dateUtils";
3
+ import useDragAndDrop from "../../hooks/useDragAndDrop";
4
+ import useEventDragDrop from "../../hooks/useEventDragDrop";
5
+ import Indicator from "./Indicator";
6
+ import EventIcon from "./EventIcon";
7
+ import EventBadge from "./EventBadge";
8
+ import ContextMenu from "./ContextMenu";
9
+ // import "./Timeline.css"; // varsayalım "Timeline.css" globalde import ediliyor
10
+
11
+ const TimelineContent = ({
12
+ groupedResources,
13
+ dates,
14
+ collapsedGroups,
15
+ events,
16
+ setEvents,
17
+ onEventClick,
18
+ todayIndex,
19
+ indicatorOn,
20
+ resourceSettings,
21
+ setDropInfo,
22
+
23
+
24
+ eventsDragOn = true,
25
+ eventsExtendOn = true,
26
+ createNewEventOn = true,
27
+
28
+ onExtendInfo,
29
+ onCreateEventInfo,
30
+ onEventRightClick,
31
+ onEventDoubleClick = null,
32
+ selectedEvents = [],
33
+ onEventSelect = null,
34
+
35
+ eventTooltipOn = true,
36
+ tooltipComponent: TooltipComponent,
37
+ tempEventStyle = {},
38
+
39
+ eventStyleResolver = () => ({}),
40
+
41
+ // Event Alignment Mode
42
+ eventAlignmentMode = "center", // "center" | "full"
43
+
44
+ // Past Date Protection
45
+ preventPastEvents = false,
46
+ minDate = null,
47
+
48
+ // Weekend Highlighting
49
+ highlightWeekends = false,
50
+
51
+ // Cell Tooltip
52
+ cellTooltipOn = false,
53
+ cellTooltipResolver = null,
54
+
55
+ // Event Icons & Badges
56
+ eventIconsOn = false, // İkonları göster/gizle
57
+ eventIconResolver = null, // (event) => icon type döndüren fonksiyon
58
+ eventBadgesOn = false, // Badge'leri göster/gizle
59
+ eventBadgeResolver = null, // (event) => { text, type, position } döndüren fonksiyon
60
+
61
+ // Loading State
62
+ isLoading = false,
63
+ loadingType = 'spinner', // 'spinner', 'dots', 'pulse'
64
+
65
+ // Context Menu
66
+ cellContextMenuOn = false, // Cell context menu'yu aç/kapa
67
+ cellContextMenuItems = [], // Context menu öğeleri
68
+ onCellContextMenu = null, // Context menu açıldığında çağrılacak callback
69
+ }) => {
70
+ // ------------------- HOOKS & STATE -------------------
71
+ const containerRef = useRef(null);
72
+
73
+ // Drag
74
+ const { dragStart, dragEnd } = useDragAndDrop(events, setEvents);
75
+ const { handleDragStart, handleDragOver, handleDrop, handleDragEnd } = useEventDragDrop(
76
+ events,
77
+ setEvents,
78
+ setDropInfo // Doğrudan setDropInfo'yu geçiriyoruz
79
+ );
80
+
81
+
82
+
83
+
84
+
85
+ // Extend
86
+ // extendEvent removed - not used (extend logic handled manually)
87
+ const [mode, setMode] = useState(null); // null | "extend"
88
+ const [extendingEvent, setExtendingEvent] = useState(null);
89
+ const [originalEndDate, setOriginalEndDate] = useState(null);
90
+ const [startMouseX, setStartMouseX] = useState(null);
91
+
92
+ // Create new event
93
+ const [isCreating, setIsCreating] = useState(false);
94
+ const [tempEvent, setTempEvent] = useState(null);
95
+
96
+ // Cell Tooltip State
97
+ const [cellTooltip, setCellTooltip] = useState(null);
98
+ const [cellTooltipPosition, setCellTooltipPosition] = useState({ top: 0, left: 0 });
99
+
100
+ // Context Menu State
101
+ const [contextMenu, setContextMenu] = useState({
102
+ isOpen: false,
103
+ position: null,
104
+ resource: null,
105
+ date: null,
106
+ });
107
+
108
+ // Tooltip
109
+ const [selectedEvent, setSelectedEvent] = useState(null);
110
+ const [tooltipPosition, setTooltipPosition] = useState({ top: 0, left: 0 });
111
+
112
+ const totalDays = dates.length;
113
+
114
+ // ------------------- Tooltip Logic -------------------
115
+ const handleEventClickInternal = (event, e) => {
116
+ e.stopPropagation();
117
+ // Eğer mod "extend" ise tooltip'i açma
118
+ if (mode === "extend") {
119
+ return;
120
+ }
121
+
122
+ // Multi-select için Ctrl+Click kontrolü
123
+ if (onEventSelect && e.ctrlKey) {
124
+ onEventSelect(event.id, true); // multiSelect = true
125
+ return;
126
+ }
127
+
128
+ // Harici callback
129
+ if (onEventClick) onEventClick(event, e);
130
+
131
+ // Tooltip göstermek
132
+ const eventElement = e.currentTarget;
133
+ if (eventElement) {
134
+ const rect = eventElement.getBoundingClientRect();
135
+ setTooltipPosition({
136
+ top: rect.top + window.scrollY,
137
+ left: rect.left + rect.width / 2 + window.scrollX,
138
+ });
139
+ setSelectedEvent(event);
140
+ }
141
+ };
142
+
143
+ const handleEventDoubleClickInternal = (event, e) => {
144
+ e.stopPropagation();
145
+ if (onEventDoubleClick) {
146
+ onEventDoubleClick(event);
147
+ }
148
+ };
149
+
150
+
151
+ const handleCloseTooltip = () => {
152
+ setSelectedEvent(null);
153
+ };
154
+
155
+ // ------------------- Context Menu -------------------
156
+ const handleCellContextMenu = useCallback((e, resource, dateObj) => {
157
+ if (!cellContextMenuOn) return;
158
+
159
+ e.preventDefault();
160
+ e.stopPropagation();
161
+
162
+ // Resource'u bul
163
+ const resourceObj = groupedResources
164
+ .flatMap(group => group.resources || [])
165
+ .find(r => r.id === resource.id || resource === r.id);
166
+
167
+ // Mouse pozisyonunu doğrudan kullan (scroll offset'i dahil etme)
168
+ setContextMenu({
169
+ isOpen: true,
170
+ position: {
171
+ x: e.clientX,
172
+ y: e.clientY
173
+ },
174
+ resource: resourceObj || resource,
175
+ date: dateObj,
176
+ });
177
+
178
+ if (onCellContextMenu) {
179
+ onCellContextMenu(resourceObj || resource, dateObj, e);
180
+ }
181
+ }, [cellContextMenuOn, groupedResources, onCellContextMenu]);
182
+
183
+ const handleCloseContextMenu = useCallback(() => {
184
+ setContextMenu({
185
+ isOpen: false,
186
+ position: null,
187
+ resource: null,
188
+ date: null,
189
+ });
190
+ }, []);
191
+
192
+ // ------------------- Create New Event -------------------
193
+ const handleCellClick = (resourceId, date, e) => {
194
+ if (!createNewEventOn) return; // create devrede değilse
195
+
196
+ // Sağ tıklamayı engelle (button 2 = sağ tık, button 0 = sol tık)
197
+ if (e.button === 2 || e.which === 3) {
198
+ return;
199
+ }
200
+
201
+ const startDate = parseDate(date.fullDate);
202
+
203
+ // Geçmiş tarih kontrolü
204
+ if (preventPastEvents && minDate) {
205
+ const minDateObj = parseDate(minDate);
206
+ // Sadece tarih karşılaştırması (saat bilgisi olmadan)
207
+ const startDateOnly = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
208
+ const minDateOnly = new Date(minDateObj.getFullYear(), minDateObj.getMonth(), minDateObj.getDate());
209
+
210
+ if (startDateOnly < minDateOnly) {
211
+ // Geçmiş tarihe tıklama engellendi
212
+ return;
213
+ }
214
+ }
215
+
216
+ const newEvent = {
217
+ id: Date.now(),
218
+ title: "1 Gece",
219
+ startDate,
220
+ endDate: new Date(startDate.getTime() + 24 * 60 * 60 * 1000),
221
+ resourceId,
222
+ // Mouse başlangıç pozisyonunu kaydet
223
+ startX: e?.clientX || 0,
224
+ startCellIndex: dates.findIndex((d) => parseDate(d.fullDate).toDateString() === startDate.toDateString()),
225
+ // color => var(--timeline-new-event-background-color) => => Sonra inline style yerine className
226
+ color: "", // Bunu .css'te "var(--timeline-new-event-background-color)" atayabilirsin
227
+ };
228
+ setTempEvent(newEvent);
229
+ setIsCreating(true);
230
+ };
231
+
232
+ useEffect(() => {
233
+ if (!createNewEventOn) return;
234
+ if (!isCreating) return;
235
+ if (mode === "extend") {
236
+ console.log(">>> 'extend' mode, skip new event creation");
237
+ return;
238
+ }
239
+
240
+ const handleMouseMove = (e) => {
241
+ if (!isCreating || !tempEvent) return;
242
+
243
+ // Timeline container'ı bul
244
+ const timelineContainer = containerRef.current?.closest('.timeline-scrollable-container');
245
+ if (!timelineContainer) return;
246
+
247
+ // Container'ın sol pozisyonunu al
248
+ const containerRect = timelineContainer.getBoundingClientRect();
249
+ const scrollLeft = timelineContainer.scrollLeft;
250
+
251
+ // Mouse'un container içindeki pozisyonunu hesapla
252
+ const mouseX = e.clientX - containerRect.left + scrollLeft;
253
+
254
+ // Gerçek cell genişliğini hesapla (container genişliği / toplam gün sayısı)
255
+ const containerWidth = timelineContainer.scrollWidth;
256
+ const cellWidth = containerWidth / totalDays;
257
+
258
+ // Hangi cell'in üzerinde olduğumuzu hesapla
259
+ let currentCellIndex = Math.floor(mouseX / cellWidth);
260
+ currentCellIndex = Math.max(0, Math.min(currentCellIndex, totalDays - 1)); // Sınırları kontrol et
261
+
262
+ // Başlangıç cell index'ini al
263
+ const startCellIndex = tempEvent.startCellIndex ?? 0;
264
+
265
+ // Geçmiş tarih kontrolü - eğer aktifse, minimum tarihten önceki cell'lere gitmeyi engelle
266
+ if (preventPastEvents && minDate && dates[currentCellIndex]) {
267
+ const currentDate = parseDate(dates[currentCellIndex].fullDate);
268
+ const minDateObj = parseDate(minDate);
269
+ const currentDateOnly = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate());
270
+ const minDateOnly = new Date(minDateObj.getFullYear(), minDateObj.getMonth(), minDateObj.getDate());
271
+
272
+ // Eğer geçmiş tarihe gidiyorsak, minimum tarihe sabitle
273
+ if (currentDateOnly < minDateOnly) {
274
+ // Minimum tarihin cell index'ini bul
275
+ const minDateIndex = dates.findIndex((d) => {
276
+ const dDate = parseDate(d.fullDate);
277
+ const dDateOnly = new Date(dDate.getFullYear(), dDate.getMonth(), dDate.getDate());
278
+ return dDateOnly.getTime() === minDateOnly.getTime();
279
+ });
280
+ if (minDateIndex !== -1) {
281
+ currentCellIndex = Math.max(startCellIndex, minDateIndex);
282
+ } else {
283
+ currentCellIndex = startCellIndex; // Minimum tarih bulunamazsa başlangıç pozisyonuna dön
284
+ }
285
+ }
286
+ }
287
+
288
+ // Kaç gün ekleneceğini hesapla (daha hassas)
289
+ const daysToAdd = Math.max(1, Math.abs(currentCellIndex - startCellIndex) + 1);
290
+
291
+ // Yeni bitiş tarihini hesapla
292
+ const newEndDate = new Date(tempEvent.startDate.getTime());
293
+ newEndDate.setDate(newEndDate.getDate() + daysToAdd - 1); // -1 çünkü başlangıç günü dahil
294
+
295
+ setTempEvent({
296
+ ...tempEvent,
297
+ endDate: newEndDate,
298
+ title: `${daysToAdd} Gece`,
299
+ });
300
+ };
301
+
302
+ const handleMouseUp = () => {
303
+ if (isCreating && tempEvent) {
304
+ setEvents([...events, tempEvent]);
305
+ if (onCreateEventInfo) {
306
+ onCreateEventInfo(tempEvent);
307
+ }
308
+ }
309
+ setTempEvent(null);
310
+ setIsCreating(false);
311
+ };
312
+
313
+ window.addEventListener("mousemove", handleMouseMove);
314
+ window.addEventListener("mouseup", handleMouseUp);
315
+
316
+ return () => {
317
+ window.removeEventListener("mousemove", handleMouseMove);
318
+ window.removeEventListener("mouseup", handleMouseUp);
319
+ };
320
+ }, [createNewEventOn, isCreating, mode, tempEvent, events, onCreateEventInfo, setEvents]);
321
+
322
+ // ------------------- Drag Logic -------------------
323
+ const handleDragStartSafe = (e, eventId) => {
324
+ if (!eventsDragOn) {
325
+ e.preventDefault();
326
+ return;
327
+ }
328
+ handleDragStart(e, eventId);
329
+ };
330
+ const handleDragEndSafe = (e) => {
331
+ if (!eventsDragOn) {
332
+ e.preventDefault();
333
+ return;
334
+ }
335
+ handleDragEnd();
336
+
337
+
338
+ };
339
+
340
+
341
+
342
+ // ------------------- Extend Logic -------------------
343
+ const handleMouseDownExtend = (mouseEvent, event) => {
344
+ if (!eventsExtendOn) return;
345
+ mouseEvent.stopPropagation();
346
+ console.log(">>> Extend start ID:", event.id);
347
+ setMode("extend");
348
+ setExtendingEvent(event);
349
+ setOriginalEndDate(event.endDate);
350
+ setStartMouseX(mouseEvent.clientX);
351
+ };
352
+
353
+ const handleMouseMoveExtend = useCallback((e) => {
354
+ if (mode !== "extend" || !extendingEvent) return;
355
+ if (!eventsExtendOn) return;
356
+
357
+ const currentMouseX = e.clientX;
358
+ const deltaX = currentMouseX - (startMouseX ?? 0);
359
+ const cellW = 30;
360
+ const daysToAdd = Math.floor(deltaX / cellW);
361
+
362
+ const newEndDate = new Date((originalEndDate ?? new Date()).getTime());
363
+ newEndDate.setDate(newEndDate.getDate() + daysToAdd);
364
+
365
+
366
+ setEvents((prev) =>
367
+ prev.map((evt) => (evt.id === extendingEvent.id ? { ...evt, endDate: newEndDate } : evt))
368
+ );
369
+ }, [mode, extendingEvent, eventsExtendOn, originalEndDate, startMouseX, setEvents]);
370
+
371
+ const handleMouseUpExtend = useCallback(() => {
372
+ console.log(">>> Extend finished ID:", extendingEvent?.id);
373
+ if (onExtendInfo && extendingEvent) {
374
+ // callback
375
+ const updatedEvent = events.find((ev) => ev.id === extendingEvent.id);
376
+ if (updatedEvent) {
377
+ onExtendInfo({
378
+ eventId: extendingEvent.id,
379
+ newEndDate: updatedEvent.endDate,
380
+ });
381
+ }
382
+ }
383
+
384
+ // Tooltip açılmasını engellemek için modun null olmasını geciktiriyoruz
385
+ setTimeout(() => {
386
+ setMode(null);
387
+ }, 100); // 100ms gecikme
388
+ setExtendingEvent(null);
389
+ setOriginalEndDate(null);
390
+ setStartMouseX(null);
391
+ }, [extendingEvent, onExtendInfo, events]);
392
+
393
+
394
+ useEffect(() => {
395
+ if (mode === "extend") {
396
+ const onMove = (e) => handleMouseMoveExtend(e);
397
+ const onUp = () => handleMouseUpExtend();
398
+ document.addEventListener("mousemove", onMove);
399
+ document.addEventListener("mouseup", onUp);
400
+ return () => {
401
+ document.removeEventListener("mousemove", onMove);
402
+ document.removeEventListener("mouseup", onUp);
403
+ };
404
+ }
405
+ }, [mode, handleMouseMoveExtend, handleMouseUpExtend]);
406
+
407
+ // ------------------- Right Click (context) -------------------
408
+ const handleRightClickEvent = (evt, reactEvent) => {
409
+ reactEvent.preventDefault();
410
+ if (onEventRightClick) onEventRightClick(evt, reactEvent);
411
+ };
412
+
413
+ // ------------------- Helper isCellSelected -------------------
414
+ const isCellSelected = (resourceId, date) => {
415
+ if (!dragStart || !dragEnd) return false;
416
+ if (resourceId !== dragStart.resourceId) return false;
417
+
418
+ const startIndex = dates.findIndex((d) => parseDate(d.fullDate).getTime() === parseDate(dragStart.date).getTime());
419
+ const endIndex = dates.findIndex((d) => parseDate(d.fullDate).getTime() === parseDate(dragEnd.date).getTime());
420
+ const currentIndex = dates.findIndex((d) => parseDate(d.fullDate).getTime() === parseDate(date.fullDate).getTime());
421
+
422
+ if (startIndex === -1 || endIndex === -1 || currentIndex === -1) return false;
423
+
424
+ return currentIndex >= Math.min(startIndex, endIndex) && currentIndex <= Math.max(startIndex, endIndex);
425
+ };
426
+
427
+ // ------------------- calculatePosition -------------------
428
+ const calculatePosition = (ev, dateArr) => {
429
+ const startDate = parseDate(ev.startDate);
430
+ const endDate = parseDate(ev.endDate);
431
+
432
+ const startIndex = dateArr.findIndex((d) => parseDate(d.fullDate).toDateString() === startDate.toDateString());
433
+ const endIndex = dateArr.findIndex((d) => parseDate(d.fullDate).toDateString() === endDate.toDateString());
434
+
435
+ const totalDays = dateArr.length;
436
+ if (startIndex < 0 && endIndex < 0) {
437
+ return { isVisible: false, left: 0, width: 0, isPartialStart: false, isPartialEnd: false };
438
+ }
439
+ if (startIndex >= totalDays && endIndex >= totalDays) {
440
+ return { isVisible: false, left: 0, width: 0, isPartialStart: false, isPartialEnd: false };
441
+ }
442
+
443
+ const effectiveStartIndex = Math.max(startIndex, 0);
444
+ const effectiveEndIndex = Math.min(endIndex, totalDays - 1);
445
+
446
+ const isPartialStart = startIndex < 0;
447
+ const isPartialEnd = endIndex >= totalDays;
448
+
449
+ // Event alignment mode'a göre pozisyon hesaplama
450
+ let leftPercentage, rightPercentage;
451
+
452
+ if (eventAlignmentMode === "full") {
453
+ // Full mode: Gün başından başlayıp gün sonunda bitiyor
454
+ // Bitiş tarihi hariç (exclusive) - örn: 3 Ocak bitiş tarihi ise 2 Ocak'ın sonunda biter
455
+ leftPercentage = (effectiveStartIndex / totalDays) * 100;
456
+ // endIndex zaten bitiş tarihini gösteriyor, bu yüzden endIndex'in başlangıcı = bir önceki günün sonu
457
+ rightPercentage = (effectiveEndIndex / totalDays) * 100;
458
+ } else {
459
+ // Center mode (varsayılan): Gün ortasından başlayıp gün ortasında bitiyor
460
+ leftPercentage = ((effectiveStartIndex + (isPartialStart ? 0 : 0.5)) / totalDays) * 100;
461
+ rightPercentage = ((effectiveEndIndex + (isPartialEnd ? 1 : 0.5)) / totalDays) * 100;
462
+ }
463
+
464
+ const widthPercentage = rightPercentage - leftPercentage;
465
+
466
+ return {
467
+ isVisible: true,
468
+ left: `${leftPercentage}%`,
469
+ width: `${widthPercentage}%`,
470
+ isPartialStart,
471
+ isPartialEnd,
472
+ };
473
+ };
474
+
475
+
476
+
477
+
478
+
479
+
480
+ // ------------------- RENDER -------------------
481
+ return (
482
+ <>
483
+ {/* Cell Tooltip */}
484
+ {cellTooltip && cellTooltipOn && (
485
+ <div
486
+ className="cell-tooltip"
487
+ style={{
488
+ position: 'fixed',
489
+ top: `${cellTooltipPosition.top - 152}px`, // Mouse'un hemen altında
490
+ left: `${cellTooltipPosition.left - 168}px`, // Mouse'un hemen sağında
491
+ pointerEvents: 'none',
492
+ zIndex: 10002,
493
+ }}
494
+ >
495
+ <div className="cell-tooltip-content">
496
+ {typeof cellTooltip.content === 'string'
497
+ ? cellTooltip.content
498
+ : cellTooltip.content}
499
+ </div>
500
+ <div className="cell-tooltip-arrow"></div>
501
+ </div>
502
+ )}
503
+
504
+ <div
505
+ ref={containerRef}
506
+ className="timeline-content-container" // Yeni class, stilini timeline.css'e ekleyebilirsin
507
+ >
508
+ {indicatorOn && (
509
+ <Indicator todayIndex={todayIndex} totalDays={totalDays} />
510
+ )}
511
+
512
+ {groupedResources.map((group, groupIndex) => (
513
+ <div key={groupIndex} className="timeline-group-container">
514
+ {/* Grup Başlığı */}
515
+ {resourceSettings.isGrouped && (
516
+ <div className="timeline-group-header-row">
517
+ {dates.map((dateObj, colIndex) => {
518
+ // Hafta sonu kontrolü
519
+ let isWeekend = false;
520
+ if (highlightWeekends) {
521
+ const cellDate = parseDate(dateObj.fullDate);
522
+ const dayOfWeek = cellDate.getDay(); // 0 = Pazar, 6 = Cumartesi
523
+ isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
524
+ }
525
+
526
+ return (
527
+ <div
528
+ key={`group-header-${groupIndex}-${colIndex}`}
529
+ className={`timeline-group-header-cell ${isWeekend ? "timeline-cell-weekend" : ""}`}
530
+ ></div>
531
+ );
532
+ })}
533
+ </div>
534
+ )}
535
+
536
+ {/* Kaynaklar */}
537
+ {!collapsedGroups[group.groupName] &&
538
+ group.resources.map((resource, rowIndex) => {
539
+ // Saatlik rezervasyonları ayrı işle
540
+ const hourlyEvents = events.filter((ev) => ev.resourceId === resource.id && ev.isHourly === true);
541
+ const normalEvents = events.filter((ev) => ev.resourceId === resource.id && ev.isHourly !== true);
542
+
543
+ // Saatlik rezervasyonları günlere göre grupla ve tek event'e dönüştür
544
+ const hourlyEventsGrouped = {};
545
+ hourlyEvents.forEach(event => {
546
+ const eventDate = new Date(event.startDate);
547
+ const dateKey = `${eventDate.getFullYear()}-${eventDate.getMonth()}-${eventDate.getDate()}`;
548
+
549
+ if (!hourlyEventsGrouped[dateKey]) {
550
+ hourlyEventsGrouped[dateKey] = {
551
+ events: [],
552
+ startDate: new Date(eventDate.getFullYear(), eventDate.getMonth(), eventDate.getDate()),
553
+ endDate: new Date(eventDate.getFullYear(), eventDate.getMonth(), eventDate.getDate() + 1),
554
+ };
555
+ }
556
+ hourlyEventsGrouped[dateKey].events.push(event);
557
+ });
558
+
559
+ // Gruplanmış saatlik rezervasyonları tek event'e dönüştür
560
+ const groupedHourlyEvents = Object.values(hourlyEventsGrouped).map(group => {
561
+ const count = group.events.length;
562
+ const totalMinutes = group.events.reduce((sum, ev) => {
563
+ return sum + (new Date(ev.endDate).getTime() - new Date(ev.startDate).getTime()) / (1000 * 60);
564
+ }, 0);
565
+ const totalHours = Math.round(totalMinutes / 60 * 10) / 10; // 1 ondalık basamak
566
+
567
+ return {
568
+ id: `hourly-group-${group.startDate.getTime()}`,
569
+ title: count > 1 ? `${count} Saatlik Rezervasyon (${totalHours} saat)` : `${totalHours} Saatlik Rezervasyon`,
570
+ startDate: group.startDate,
571
+ endDate: group.endDate,
572
+ resourceId: resource.id,
573
+ isHourly: true,
574
+ isGrouped: true,
575
+ hourlyCount: count,
576
+ hourlyTotalHours: totalHours,
577
+ };
578
+ });
579
+
580
+ // Normal event'ler ve gruplanmış saatlik event'leri birleştir
581
+ const resourceEvents = [...normalEvents, ...groupedHourlyEvents];
582
+
583
+ return (
584
+ <div key={resource.id} className="timeline-resource-row">
585
+ {/* Her resource row'u */}
586
+ {resourceEvents.map((event) => {
587
+ const { isVisible, left, width, isPartialStart, isPartialEnd } =
588
+ calculatePosition(event, dates);
589
+ if (!isVisible) return null;
590
+
591
+ // Kullanıcıdan gelen stil
592
+ const eventStyle = eventStyleResolver ? eventStyleResolver(event) : {};
593
+
594
+ // Icon ve Badge bilgilerini al
595
+ const iconType = eventIconsOn && eventIconResolver ? eventIconResolver(event) : null;
596
+ const badgeInfo = eventBadgesOn && eventBadgeResolver ? eventBadgeResolver(event) : null;
597
+
598
+ // Saatlik rezervasyon kontrolü
599
+ const isHourly = event.isHourly === true;
600
+
601
+ return (
602
+ <div
603
+ key={event.id}
604
+ className={`timeline-event timeline-event-enter ${selectedEvents.includes(event.id) ? "selected" : ""} ${isHourly ? "timeline-event-hourly" : ""}`}
605
+ draggable={false}
606
+ onContextMenu={(reactEvent) => handleRightClickEvent(event, reactEvent)}
607
+ onClick={(ev) => handleEventClickInternal(event, ev)}
608
+ onDoubleClick={(e) => handleEventDoubleClickInternal(event, e)}
609
+ style={{
610
+ left,
611
+ width: width, // Hesaplanan genişliği kullan
612
+ maxWidth: isHourly ? width : "none", // Saatlik rezervasyonlar için max genişlik sınırlaması
613
+ top: "5px",
614
+ borderTopLeftRadius: isPartialStart ? "0px" : "20px",
615
+ borderBottomLeftRadius: isPartialStart ? "0px" : "20px",
616
+ borderTopRightRadius: isPartialEnd ? "0px" : "20px",
617
+ borderBottomRightRadius: isPartialEnd ? "0px" : "20px",
618
+ cursor: isHourly ? "default" : (mode === "extend" ? "col-resize" : "default"),
619
+ pointerEvents: isHourly ? "none" : "auto", // Saatlik rezervasyonlarda tıklama/etkileşim yok
620
+ ...eventStyle, // Kullanıcı tarafından tanımlanan stiller
621
+ }}
622
+ >
623
+ {/* Event Badge */}
624
+ {badgeInfo && (
625
+ <EventBadge
626
+ text={badgeInfo.text}
627
+ type={badgeInfo.type || 'default'}
628
+ position={badgeInfo.position || 'top-right'}
629
+ style={badgeInfo.style}
630
+ />
631
+ )}
632
+
633
+ {/* Drag Handle - Sol Taraf - Saatlik rezervasyonlarda gösterilmez */}
634
+ {eventsDragOn && mode !== "extend" && !isHourly && (
635
+ <div
636
+ className="timeline-event-drag-handle"
637
+ draggable={true}
638
+ onDragStart={(e) => {
639
+ if (mode === "extend") {
640
+ e.preventDefault();
641
+ return;
642
+ }
643
+ e.stopPropagation();
644
+
645
+ // Tüm event elementini drag image olarak ayarla
646
+ const eventElement = e.currentTarget.closest('.timeline-event');
647
+ if (eventElement) {
648
+ // Mouse pozisyonunu event elementine göre hesapla
649
+ const eventRect = eventElement.getBoundingClientRect();
650
+ const handleRect = e.currentTarget.getBoundingClientRect();
651
+ const offsetX = handleRect.left - eventRect.left + (handleRect.width / 2);
652
+ const offsetY = handleRect.top - eventRect.top + (handleRect.height / 2);
653
+
654
+ // Geçici bir görüntü oluştur
655
+ const dragImage = eventElement.cloneNode(true);
656
+ dragImage.style.position = 'absolute';
657
+ dragImage.style.top = '-1000px';
658
+ dragImage.style.left = '-1000px';
659
+ dragImage.style.opacity = '0.8';
660
+ dragImage.style.pointerEvents = 'none';
661
+ dragImage.style.transform = 'none';
662
+ dragImage.style.width = eventRect.width + 'px';
663
+ document.body.appendChild(dragImage);
664
+
665
+ // Drag image'i ayarla
666
+ e.dataTransfer.setDragImage(dragImage, offsetX, offsetY);
667
+
668
+ // Geçici elementi temizle
669
+ setTimeout(() => {
670
+ if (document.body.contains(dragImage)) {
671
+ document.body.removeChild(dragImage);
672
+ }
673
+ }, 0);
674
+ }
675
+
676
+ handleDragStartSafe(e, event.id);
677
+ }}
678
+ onDragEnd={(e) => {
679
+ if (mode === "extend") {
680
+ e.preventDefault();
681
+ return;
682
+ }
683
+ handleDragEndSafe(e);
684
+ }}
685
+ onMouseDown={(e) => {
686
+ e.stopPropagation();
687
+ }}
688
+ ></div>
689
+ )}
690
+ {/* Event Icon - Title'dan önce */}
691
+ {iconType && <EventIcon type={iconType} />}
692
+ <span className="timeline-event-title">
693
+ {event.title}
694
+ </span>
695
+ {/* Extend Handle - Saatlik rezervasyonlarda gösterilmez */}
696
+ {eventsExtendOn && !isHourly && (
697
+ <div
698
+ className="timeline-event-extend-handle"
699
+ onMouseDown={(mouseEvent) => {
700
+ mouseEvent.stopPropagation();
701
+ handleMouseDownExtend(mouseEvent, event);
702
+ }}
703
+ ></div>
704
+ )}
705
+ </div>
706
+ );
707
+ })}
708
+
709
+ {/* Geçici (yeni) event */}
710
+ {tempEvent && tempEvent.resourceId === resource.id && (
711
+ <div
712
+ className="timeline-temp-event"
713
+ style={{
714
+ ...calculatePosition(tempEvent, dates),
715
+ ...tempEventStyle, // Kullanıcının geçtiği stiller
716
+ }}
717
+ >
718
+ {tempEvent.title}
719
+ </div>
720
+ )}
721
+
722
+ {/* Tarih Hücreleri */}
723
+ {dates.map((dateObj, colIndex) => {
724
+ // Geçmiş tarih kontrolü
725
+ let isPastDate = false;
726
+ if (preventPastEvents && minDate) {
727
+ const cellDate = parseDate(dateObj.fullDate);
728
+ const minDateObj = parseDate(minDate);
729
+ const cellDateOnly = new Date(cellDate.getFullYear(), cellDate.getMonth(), cellDate.getDate());
730
+ const minDateOnly = new Date(minDateObj.getFullYear(), minDateObj.getMonth(), minDateObj.getDate());
731
+ isPastDate = cellDateOnly < minDateOnly;
732
+ }
733
+
734
+ // Hafta sonu kontrolü
735
+ let isWeekend = false;
736
+ if (highlightWeekends) {
737
+ const cellDate = parseDate(dateObj.fullDate);
738
+ const dayOfWeek = cellDate.getDay(); // 0 = Pazar, 6 = Cumartesi
739
+ isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
740
+ }
741
+
742
+ return (
743
+ <div
744
+ key={`cell-${groupIndex}-${rowIndex}-${colIndex}`}
745
+ className={`timeline-cell ${
746
+ isCellSelected(resource.id, dateObj) ? "selected" : ""
747
+ } ${isPastDate ? "timeline-cell-past" : ""} ${
748
+ isWeekend ? "timeline-cell-weekend" : ""
749
+ }`}
750
+ data-date={JSON.stringify(dateObj)}
751
+ data-resource-id={resource.id}
752
+ onMouseDown={(e) => {
753
+ // Sağ tıklamayı engelle (sadece sol tık ile event oluştur)
754
+ if (e.button === 2 || e.which === 3) {
755
+ return;
756
+ }
757
+ if (!isPastDate) {
758
+ handleCellClick(resource.id, dateObj, e);
759
+ }
760
+ }}
761
+ onContextMenu={(e) => {
762
+ e.preventDefault(); // Varsayılan context menu'yu engelle
763
+ e.stopPropagation(); // Event bubbling'i durdur
764
+ if (cellContextMenuOn) {
765
+ handleCellContextMenu(e, resource, dateObj);
766
+ }
767
+ }}
768
+ onMouseEnter={(e) => {
769
+ if (cellTooltipOn && cellTooltipResolver) {
770
+ const tooltipContent = cellTooltipResolver(resource, dateObj);
771
+ if (tooltipContent) {
772
+ // Mouse pozisyonunu kullan
773
+ setCellTooltip({
774
+ content: tooltipContent,
775
+ resource: resource,
776
+ date: dateObj,
777
+ });
778
+ setCellTooltipPosition({
779
+ top: e.clientY,
780
+ left: e.clientX,
781
+ });
782
+ }
783
+ }
784
+ }}
785
+ onMouseMove={(e) => {
786
+ if (cellTooltipOn && cellTooltip) {
787
+ // Mouse hareket ettikçe tooltip'i takip et
788
+ setCellTooltipPosition({
789
+ top: e.clientY,
790
+ left: e.clientX,
791
+ });
792
+ }
793
+ }}
794
+ onMouseLeave={() => {
795
+ if (cellTooltipOn) {
796
+ setCellTooltip(null);
797
+ }
798
+ }}
799
+ onDragOver={(e) => handleDragOver(e)}
800
+ onDrop={(e) =>
801
+ handleDrop(e, resource.id, parseDate(dateObj.fullDate))
802
+ }
803
+ ></div>
804
+ );
805
+ })}
806
+ </div>
807
+ );
808
+ })}
809
+ </div>
810
+ ))}
811
+
812
+
813
+ {/* Tooltip vb. */}
814
+ {eventTooltipOn && selectedEvent && TooltipComponent && mode !== "extend" && (
815
+ <TooltipComponent
816
+ event={selectedEvent}
817
+ position={tooltipPosition}
818
+ onClose={handleCloseTooltip}
819
+ />
820
+ )}
821
+
822
+ {/* Context Menu */}
823
+ {cellContextMenuOn && (
824
+ <ContextMenu
825
+ isOpen={contextMenu.isOpen}
826
+ position={contextMenu.position}
827
+ onClose={handleCloseContextMenu}
828
+ menuItems={cellContextMenuItems}
829
+ resource={contextMenu.resource}
830
+ date={contextMenu.date}
831
+ />
832
+ )}
833
+ </div>
834
+ </>
835
+ );
836
+ };
837
+
838
+ export default TimelineContent;