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