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.
- package/CHANGELOG.md +98 -35
- package/dist/Timeline.js +4309 -1677
- package/dist/components/Timeline/AutocompleteSelect.js +150 -0
- package/dist/components/Timeline/ContextMenu.js +149 -0
- package/dist/components/Timeline/DailyView.js +255 -0
- package/dist/components/Timeline/DatePickerComponent.js +13 -0
- package/{public/dist/dist → dist}/components/Timeline/DragAndDropHandler.js +34 -34
- package/dist/components/Timeline/EventBadge.js +26 -0
- package/dist/components/Timeline/EventDetailModal.js +138 -0
- package/dist/components/Timeline/EventIcon.js +47 -0
- package/dist/{dist/components → components}/Timeline/EventTooltip.js +206 -206
- package/dist/components/Timeline/FilterPanel.js +179 -0
- package/dist/{dist/components → components}/Timeline/Indicator.js +26 -26
- package/dist/components/Timeline/LoadingSpinner.js +48 -0
- package/dist/{dist/components → components}/Timeline/MasterHeader.js +104 -68
- package/{public/dist/dist → dist}/components/Timeline/Resources.js +53 -53
- package/dist/{dist/components → components}/Timeline/ResourcesHeader.js +14 -14
- package/dist/components/Timeline/Timeline.css +2491 -0
- package/dist/components/Timeline/Timeline.js +607 -0
- package/dist/{dist/components → components}/Timeline/TimelineCell.js +8 -8
- package/dist/components/Timeline/TimelineContent.js +838 -0
- package/{public/dist/dist → dist}/components/Timeline/TimelineEvents.js +114 -114
- package/dist/components/Timeline/TimelineHeader.js +54 -0
- package/{public/dist/dist → dist}/components/Timeline/TimelineMonthContainer.js +29 -29
- package/{public/dist/dist → dist}/components/Timeline/TimelineResources.js +16 -16
- package/{public/dist/dist → dist}/hooks/useDragAndDrop.js +80 -80
- package/dist/{dist/hooks → hooks}/useEventDragDrop.js +126 -126
- package/dist/hooks/useEventManagement.js +173 -0
- package/dist/hooks/useEventSelection.js +82 -0
- package/{public/dist/dist → dist}/hooks/useExtendEvent.js +28 -28
- package/dist/hooks/useKeyboardShortcuts.js +158 -0
- package/dist/hooks/useTouchGestures.js +90 -0
- package/dist/utils/conflictUtils.js +105 -0
- package/dist/{dist/utils → utils}/dateUtils.js +36 -36
- package/dist/{dist/utils → utils}/filterTimelineData.js +20 -20
- package/dist/utils/filterUtils.js +106 -0
- package/dist/utils/timeUtils.js +179 -0
- package/dist/{dist/utils → utils}/timelineUtils.js +39 -39
- package/dist/utils/viewModeUtils.js +54 -0
- package/package.json +89 -19
- package/src/App.js +300 -19
- package/src/components/Timeline/AutocompleteSelect.js +150 -0
- package/src/components/Timeline/ContextMenu.js +149 -0
- package/src/components/Timeline/DailyView.js +255 -0
- package/src/components/Timeline/DatePickerComponent.js +13 -17
- package/src/components/Timeline/DragAndDropHandler.js +34 -34
- package/src/components/Timeline/EventBadge.js +26 -0
- package/src/components/Timeline/EventDetailModal.js +138 -0
- package/src/components/Timeline/EventIcon.js +47 -0
- package/src/components/Timeline/EventTooltip.js +206 -206
- package/src/components/Timeline/FilterPanel.js +179 -0
- package/src/components/Timeline/Indicator.js +26 -26
- package/src/components/Timeline/LoadingSpinner.js +48 -0
- package/src/components/Timeline/MasterHeader.js +104 -68
- package/src/components/Timeline/Resources.js +53 -53
- package/src/components/Timeline/ResourcesHeader.js +14 -14
- package/src/components/Timeline/Timeline.css +2491 -616
- package/src/components/Timeline/Timeline.js +607 -309
- package/src/components/Timeline/TimelineCell.js +8 -8
- package/src/components/Timeline/TimelineContent.js +838 -446
- package/src/components/Timeline/TimelineEvents.js +114 -114
- package/src/components/Timeline/TimelineHeader.js +54 -43
- package/src/components/Timeline/TimelineMonthContainer.js +29 -29
- package/src/components/Timeline/TimelineResources.js +16 -16
- package/src/demo.css +4 -0
- package/src/hooks/useDragAndDrop.js +80 -80
- package/src/hooks/useEventDragDrop.js +126 -126
- package/src/hooks/useEventManagement.js +173 -0
- package/src/hooks/useEventSelection.js +82 -0
- package/src/hooks/useExtendEvent.js +28 -28
- package/src/hooks/useKeyboardShortcuts.js +158 -0
- package/src/hooks/useTouchGestures.js +90 -0
- package/src/index.js +1 -7
- package/src/library.js +26 -0
- package/src/utils/conflictUtils.js +105 -0
- package/src/utils/dateUtils.js +36 -36
- package/src/utils/filterTimelineData.js +20 -20
- package/src/utils/filterUtils.js +106 -0
- package/src/utils/timeUtils.js +179 -0
- package/src/utils/timelineUtils.js +39 -39
- package/src/utils/viewModeUtils.js +54 -0
- package/.babelrc +0 -6
- package/babel.config.json +0 -4
- package/dist/dist/components/Timeline/DatePickerComponent.js +0 -17
- package/dist/dist/components/Timeline/DragAndDropHandler.js +0 -35
- package/dist/dist/components/Timeline/Resources.js +0 -53
- package/dist/dist/components/Timeline/Timeline.css +0 -616
- package/dist/dist/components/Timeline/Timeline.js +0 -309
- package/dist/dist/components/Timeline/TimelineContent.js +0 -446
- package/dist/dist/components/Timeline/TimelineEvents.js +0 -114
- package/dist/dist/components/Timeline/TimelineHeader.js +0 -43
- package/dist/dist/components/Timeline/TimelineMonthContainer.js +0 -29
- package/dist/dist/components/Timeline/TimelineResources.js +0 -16
- package/dist/dist/hooks/useDragAndDrop.js +0 -80
- package/dist/dist/hooks/useExtendEvent.js +0 -28
- package/public/dist/Timeline.js +0 -3277
- package/public/dist/dist/components/Timeline/DatePickerComponent.js +0 -17
- package/public/dist/dist/components/Timeline/EventTooltip.js +0 -206
- package/public/dist/dist/components/Timeline/Indicator.js +0 -29
- package/public/dist/dist/components/Timeline/MasterHeader.js +0 -68
- package/public/dist/dist/components/Timeline/ResourcesHeader.js +0 -14
- package/public/dist/dist/components/Timeline/Timeline.css +0 -616
- package/public/dist/dist/components/Timeline/Timeline.js +0 -304
- package/public/dist/dist/components/Timeline/TimelineCell.js +0 -8
- package/public/dist/dist/components/Timeline/TimelineContent.js +0 -447
- package/public/dist/dist/components/Timeline/TimelineHeader.js +0 -43
- package/public/dist/dist/hooks/useEventDragDrop.js +0 -126
- package/public/dist/dist/utils/HorizontalVirtualScroll.js +0 -0
- package/public/dist/dist/utils/dateUtils.js +0 -36
- package/public/dist/dist/utils/filterTimelineData.js +0 -21
- package/public/dist/dist/utils/timelineUtils.js +0 -40
- package/public/favicon.ico +0 -0
- package/public/index kutuphane /304/261c/304/261n.html" +0 -43
- package/public/index tasarim icin.html +0 -20
- package/public/index.html +0 -43
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +0 -25
- package/public/robots.txt +0 -3
- package/src/App.css +0 -38
- package/src/App.test.js +0 -8
- package/src/dist/Timeline.js +0 -277
- package/src/index.css +0 -13
- package/src/logo.svg +0 -1
- package/src/reportWebVitals.js +0 -13
- package/src/setupTests.js +0 -5
- package/webpack.config.js +0 -49
- /package/dist/{dist/utils → utils}/HorizontalVirtualScroll.js +0 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from "react";
|
|
2
|
+
import "./Timeline.css";
|
|
3
|
+
|
|
4
|
+
const AutocompleteSelect = ({
|
|
5
|
+
options = [],
|
|
6
|
+
value = null,
|
|
7
|
+
onChange = () => {},
|
|
8
|
+
placeholder = "Seçiniz...",
|
|
9
|
+
getOptionLabel = (option) => option?.label || option?.name || String(option),
|
|
10
|
+
getOptionValue = (option) => option?.value || option?.id || option,
|
|
11
|
+
filterOptions = (options, inputValue) => {
|
|
12
|
+
if (!inputValue) return options;
|
|
13
|
+
const lowerInput = inputValue.toLowerCase();
|
|
14
|
+
return options.filter((option) => {
|
|
15
|
+
const label = getOptionLabel(option).toLowerCase();
|
|
16
|
+
return label.includes(lowerInput);
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
}) => {
|
|
20
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
21
|
+
const [inputValue, setInputValue] = useState("");
|
|
22
|
+
const [filteredOptions, setFilteredOptions] = useState(options);
|
|
23
|
+
const containerRef = useRef(null);
|
|
24
|
+
const inputRef = useRef(null);
|
|
25
|
+
|
|
26
|
+
// Seçili değerin label'ını bul
|
|
27
|
+
const selectedOption = options.find(
|
|
28
|
+
(opt) => getOptionValue(opt) === value
|
|
29
|
+
);
|
|
30
|
+
const displayValue = selectedOption
|
|
31
|
+
? getOptionLabel(selectedOption)
|
|
32
|
+
: inputValue || placeholder;
|
|
33
|
+
|
|
34
|
+
// Input değiştiğinde filtrele
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (isOpen) {
|
|
37
|
+
const filtered = filterOptions(options, inputValue);
|
|
38
|
+
setFilteredOptions(filtered);
|
|
39
|
+
} else {
|
|
40
|
+
setFilteredOptions(options);
|
|
41
|
+
}
|
|
42
|
+
}, [inputValue, isOpen, options, filterOptions]);
|
|
43
|
+
|
|
44
|
+
// Dışarı tıklandığında kapat
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const handleClickOutside = (event) => {
|
|
47
|
+
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
|
48
|
+
setIsOpen(false);
|
|
49
|
+
setInputValue("");
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (isOpen) {
|
|
54
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
55
|
+
return () => {
|
|
56
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}, [isOpen]);
|
|
60
|
+
|
|
61
|
+
const handleInputChange = (e) => {
|
|
62
|
+
const newValue = e.target.value;
|
|
63
|
+
setInputValue(newValue);
|
|
64
|
+
setIsOpen(true);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const handleSelect = (option) => {
|
|
68
|
+
const optionValue = getOptionValue(option);
|
|
69
|
+
onChange(optionValue, option);
|
|
70
|
+
setInputValue("");
|
|
71
|
+
setIsOpen(false);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const handleFocus = () => {
|
|
75
|
+
setIsOpen(true);
|
|
76
|
+
if (selectedOption) {
|
|
77
|
+
setInputValue(getOptionLabel(selectedOption));
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const handleBlur = () => {
|
|
82
|
+
// Input blur olduğunda hemen kapatma, click outside ile kapatılacak
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
if (!containerRef.current?.contains(document.activeElement)) {
|
|
85
|
+
setIsOpen(false);
|
|
86
|
+
if (selectedOption) {
|
|
87
|
+
setInputValue("");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}, 200);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className="autocomplete-select-container" ref={containerRef}>
|
|
95
|
+
<div
|
|
96
|
+
className={`autocomplete-select-input ${isOpen ? "open" : ""}`}
|
|
97
|
+
onClick={() => {
|
|
98
|
+
setIsOpen(!isOpen);
|
|
99
|
+
inputRef.current?.focus();
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
<input
|
|
103
|
+
ref={inputRef}
|
|
104
|
+
type="text"
|
|
105
|
+
value={isOpen ? inputValue : displayValue}
|
|
106
|
+
onChange={handleInputChange}
|
|
107
|
+
onFocus={handleFocus}
|
|
108
|
+
onBlur={handleBlur}
|
|
109
|
+
placeholder={placeholder}
|
|
110
|
+
className="autocomplete-select-input-field"
|
|
111
|
+
/>
|
|
112
|
+
<span className="autocomplete-select-arrow">
|
|
113
|
+
{isOpen ? "▲" : "▼"}
|
|
114
|
+
</span>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{isOpen && (
|
|
118
|
+
<div className="autocomplete-select-dropdown">
|
|
119
|
+
{filteredOptions.length > 0 ? (
|
|
120
|
+
filteredOptions.map((option, index) => {
|
|
121
|
+
const optionValue = getOptionValue(option);
|
|
122
|
+
const optionLabel = getOptionLabel(option);
|
|
123
|
+
const isSelected = optionValue === value;
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div
|
|
127
|
+
key={index}
|
|
128
|
+
className={`autocomplete-select-option ${
|
|
129
|
+
isSelected ? "selected" : ""
|
|
130
|
+
}`}
|
|
131
|
+
onClick={() => handleSelect(option)}
|
|
132
|
+
onMouseDown={(e) => e.preventDefault()} // Blur'u engelle
|
|
133
|
+
>
|
|
134
|
+
{optionLabel}
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
})
|
|
138
|
+
) : (
|
|
139
|
+
<div className="autocomplete-select-no-results">
|
|
140
|
+
Sonuç bulunamadı
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export default AutocompleteSelect;
|
|
150
|
+
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import './Timeline.css';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Context Menu Component
|
|
6
|
+
* Sağ tık menüsü için özelleştirilebilir menü bileşeni
|
|
7
|
+
*/
|
|
8
|
+
const ContextMenu = ({
|
|
9
|
+
isOpen,
|
|
10
|
+
position,
|
|
11
|
+
onClose,
|
|
12
|
+
menuItems = [],
|
|
13
|
+
resource = null,
|
|
14
|
+
date = null,
|
|
15
|
+
}) => {
|
|
16
|
+
const menuRef = useRef(null);
|
|
17
|
+
const [adjustedPosition, setAdjustedPosition] = useState(position);
|
|
18
|
+
|
|
19
|
+
// Menü pozisyonunu mouse'a yakın tut ve ekran sınırları içinde tut
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!menuRef.current || !position || !isOpen) {
|
|
22
|
+
setAdjustedPosition(position);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Menü render edildikten sonra pozisyonu ayarla
|
|
27
|
+
const updatePosition = () => {
|
|
28
|
+
const menuRect = menuRef.current.getBoundingClientRect();
|
|
29
|
+
const viewportWidth = window.innerWidth;
|
|
30
|
+
const viewportHeight = window.innerHeight;
|
|
31
|
+
|
|
32
|
+
let adjustedX = position.x;
|
|
33
|
+
let adjustedY = position.y;
|
|
34
|
+
|
|
35
|
+
// Sağa taşma kontrolü
|
|
36
|
+
if (position.x + menuRect.width > viewportWidth) {
|
|
37
|
+
adjustedX = position.x - menuRect.width;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Aşağıya taşma kontrolü
|
|
41
|
+
if (position.y + menuRect.height > viewportHeight) {
|
|
42
|
+
adjustedY = position.y - menuRect.height;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Sola taşma kontrolü
|
|
46
|
+
if (adjustedX < 10) {
|
|
47
|
+
adjustedX = 10;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Yukarıya taşma kontrolü
|
|
51
|
+
if (adjustedY < 10) {
|
|
52
|
+
adjustedY = 10;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
setAdjustedPosition({ x: adjustedX, y: adjustedY });
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Menü render edildikten sonra pozisyonu güncelle
|
|
59
|
+
setTimeout(updatePosition, 0);
|
|
60
|
+
}, [position, isOpen]);
|
|
61
|
+
|
|
62
|
+
// Menü dışına tıklanınca kapat
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const handleClickOutside = (event) => {
|
|
65
|
+
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
|
66
|
+
onClose();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handleEscape = (event) => {
|
|
71
|
+
if (event.key === 'Escape') {
|
|
72
|
+
onClose();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (isOpen) {
|
|
77
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
78
|
+
document.addEventListener('keydown', handleEscape);
|
|
79
|
+
// Scroll olduğunda menüyü kapat
|
|
80
|
+
document.addEventListener('scroll', onClose, true);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return () => {
|
|
84
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
85
|
+
document.removeEventListener('keydown', handleEscape);
|
|
86
|
+
document.removeEventListener('scroll', onClose, true);
|
|
87
|
+
};
|
|
88
|
+
}, [isOpen, onClose]);
|
|
89
|
+
|
|
90
|
+
if (!isOpen || !position) return null;
|
|
91
|
+
|
|
92
|
+
const handleItemClick = (item) => {
|
|
93
|
+
if (item.onClick) {
|
|
94
|
+
item.onClick(resource, date);
|
|
95
|
+
}
|
|
96
|
+
if (item.closeOnClick !== false) {
|
|
97
|
+
onClose();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div
|
|
103
|
+
ref={menuRef}
|
|
104
|
+
className="context-menu"
|
|
105
|
+
style={{
|
|
106
|
+
position: 'fixed',
|
|
107
|
+
left: `${(adjustedPosition?.x ?? position.x) - 150}px`,
|
|
108
|
+
top: `${(adjustedPosition?.y ?? position.y) - 150}px`,
|
|
109
|
+
zIndex: 10005,
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
<div className="context-menu-content">
|
|
113
|
+
{menuItems.length === 0 ? (
|
|
114
|
+
<div className="context-menu-item context-menu-item-disabled">
|
|
115
|
+
Menü öğesi yok
|
|
116
|
+
</div>
|
|
117
|
+
) : (
|
|
118
|
+
menuItems.map((item, index) => {
|
|
119
|
+
if (item.separator) {
|
|
120
|
+
return <div key={`separator-${index}`} className="context-menu-separator" />;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (item.hidden) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div
|
|
129
|
+
key={item.id || index}
|
|
130
|
+
className={`context-menu-item ${item.disabled ? 'context-menu-item-disabled' : ''} ${item.danger ? 'context-menu-item-danger' : ''}`}
|
|
131
|
+
onClick={() => !item.disabled && handleItemClick(item)}
|
|
132
|
+
title={item.tooltip || item.label}
|
|
133
|
+
>
|
|
134
|
+
{item.icon && <span className="context-menu-item-icon">{item.icon}</span>}
|
|
135
|
+
<span className="context-menu-item-label">{item.label}</span>
|
|
136
|
+
{item.shortcut && (
|
|
137
|
+
<span className="context-menu-item-shortcut">{item.shortcut}</span>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
})
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export default ContextMenu;
|
|
149
|
+
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
|
+
import './Timeline.css';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Daily View Component
|
|
6
|
+
* Seçili resource ve tarih için günlük saat bazlı timeline görünümü
|
|
7
|
+
*/
|
|
8
|
+
const DailyView = ({
|
|
9
|
+
isOpen,
|
|
10
|
+
onClose,
|
|
11
|
+
resource,
|
|
12
|
+
date,
|
|
13
|
+
events = [],
|
|
14
|
+
onEventCreate,
|
|
15
|
+
onEventUpdate,
|
|
16
|
+
onEventDelete,
|
|
17
|
+
themeType = 'light',
|
|
18
|
+
}) => {
|
|
19
|
+
const [isCreating, setIsCreating] = useState(false);
|
|
20
|
+
const [tempEvent, setTempEvent] = useState(null);
|
|
21
|
+
const containerRef = useRef(null);
|
|
22
|
+
const timelineRef = useRef(null);
|
|
23
|
+
|
|
24
|
+
// Saatleri oluştur (0-23)
|
|
25
|
+
const hours = Array.from({ length: 24 }, (_, i) => i);
|
|
26
|
+
|
|
27
|
+
// O güne ait event'leri filtrele
|
|
28
|
+
const dayEvents = events.filter(event => {
|
|
29
|
+
if (!event.startDate || !date) return false;
|
|
30
|
+
const eventDate = new Date(event.startDate);
|
|
31
|
+
const selectedDate = new Date(date.fullDate || date);
|
|
32
|
+
return (
|
|
33
|
+
eventDate.getFullYear() === selectedDate.getFullYear() &&
|
|
34
|
+
eventDate.getMonth() === selectedDate.getMonth() &&
|
|
35
|
+
eventDate.getDate() === selectedDate.getDate() &&
|
|
36
|
+
event.resourceId === resource?.id
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Saat bazlı event pozisyonu hesapla
|
|
41
|
+
const getEventPosition = (event) => {
|
|
42
|
+
const startDate = new Date(event.startDate);
|
|
43
|
+
const endDate = new Date(event.endDate);
|
|
44
|
+
const startHour = startDate.getHours() + startDate.getMinutes() / 60;
|
|
45
|
+
const endHour = endDate.getHours() + endDate.getMinutes() / 60;
|
|
46
|
+
const duration = endHour - startHour;
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
top: `${(startHour / 24) * 100}%`,
|
|
50
|
+
height: `${(duration / 24) * 100}%`,
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Timeline'a tıklandığında
|
|
55
|
+
const handleTimelineClick = (e) => {
|
|
56
|
+
if (!onEventCreate || !timelineRef.current) return;
|
|
57
|
+
|
|
58
|
+
const rect = timelineRef.current.getBoundingClientRect();
|
|
59
|
+
const clickedY = e.clientY - rect.top;
|
|
60
|
+
const timelineHeight = rect.height;
|
|
61
|
+
const totalMinutes = Math.max(0, Math.min((clickedY / timelineHeight) * (24 * 60), 24 * 60));
|
|
62
|
+
|
|
63
|
+
const startHour = Math.floor(totalMinutes / 60);
|
|
64
|
+
const startMinutes = Math.floor(totalMinutes % 60);
|
|
65
|
+
|
|
66
|
+
const startDate = new Date(date.fullDate || date);
|
|
67
|
+
startDate.setHours(startHour, startMinutes, 0, 0);
|
|
68
|
+
|
|
69
|
+
const endDate = new Date(startDate);
|
|
70
|
+
endDate.setTime(startDate.getTime() + 30 * 60 * 1000); // Varsayılan 30 dakika
|
|
71
|
+
|
|
72
|
+
setIsCreating(true);
|
|
73
|
+
setTempEvent({
|
|
74
|
+
startDate,
|
|
75
|
+
endDate,
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Mouse hareketi ile event oluşturma
|
|
80
|
+
const handleMouseMove = useCallback((e) => {
|
|
81
|
+
if (!isCreating || !tempEvent || !timelineRef.current) return;
|
|
82
|
+
|
|
83
|
+
const rect = timelineRef.current.getBoundingClientRect();
|
|
84
|
+
const mouseY = e.clientY - rect.top;
|
|
85
|
+
const timelineHeight = rect.height;
|
|
86
|
+
const totalMinutes = Math.max(0, Math.min((mouseY / timelineHeight) * (24 * 60), 24 * 60));
|
|
87
|
+
|
|
88
|
+
const endHour = Math.floor(totalMinutes / 60);
|
|
89
|
+
const endMinutes = Math.floor(totalMinutes % 60);
|
|
90
|
+
|
|
91
|
+
const newEndDate = new Date(tempEvent.startDate);
|
|
92
|
+
newEndDate.setHours(endHour, endMinutes, 0, 0);
|
|
93
|
+
|
|
94
|
+
// Bitiş saati başlangıçtan önce olamaz
|
|
95
|
+
if (newEndDate <= tempEvent.startDate) {
|
|
96
|
+
newEndDate.setTime(tempEvent.startDate.getTime() + 15 * 60 * 1000); // En az 15 dakika
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
setTempEvent(prev => ({
|
|
100
|
+
...prev,
|
|
101
|
+
endDate: newEndDate,
|
|
102
|
+
}));
|
|
103
|
+
}, [isCreating, tempEvent]);
|
|
104
|
+
|
|
105
|
+
// Mouse bırakıldığında event oluştur
|
|
106
|
+
const handleMouseUp = useCallback(() => {
|
|
107
|
+
if (!isCreating || !tempEvent || !onEventCreate) return;
|
|
108
|
+
|
|
109
|
+
// Minimum süre kontrolü (en az 15 dakika)
|
|
110
|
+
const duration = tempEvent.endDate.getTime() - tempEvent.startDate.getTime();
|
|
111
|
+
if (duration < 15 * 60 * 1000) {
|
|
112
|
+
setIsCreating(false);
|
|
113
|
+
setTempEvent(null);
|
|
114
|
+
return; // Çok kısa, event oluşturma
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const newEvent = {
|
|
118
|
+
id: `daily-${Date.now()}`,
|
|
119
|
+
title: 'Yeni Randevu',
|
|
120
|
+
startDate: tempEvent.startDate,
|
|
121
|
+
endDate: tempEvent.endDate,
|
|
122
|
+
resourceId: resource?.id,
|
|
123
|
+
isHourly: true, // Saatlik rezervasyon flag'i
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
onEventCreate(newEvent);
|
|
127
|
+
setIsCreating(false);
|
|
128
|
+
setTempEvent(null);
|
|
129
|
+
}, [isCreating, tempEvent, onEventCreate, resource]);
|
|
130
|
+
|
|
131
|
+
// Global mouse event listener'ları
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (!isCreating) return;
|
|
134
|
+
|
|
135
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
136
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
137
|
+
|
|
138
|
+
return () => {
|
|
139
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
140
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
141
|
+
};
|
|
142
|
+
}, [isCreating, handleMouseMove, handleMouseUp]);
|
|
143
|
+
|
|
144
|
+
// Tarih formatı
|
|
145
|
+
const formatDate = (dateObj) => {
|
|
146
|
+
if (!dateObj) return '';
|
|
147
|
+
const d = new Date(dateObj.fullDate || dateObj);
|
|
148
|
+
return d.toLocaleDateString('tr-TR', {
|
|
149
|
+
weekday: 'long',
|
|
150
|
+
year: 'numeric',
|
|
151
|
+
month: 'long',
|
|
152
|
+
day: 'numeric',
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Saat formatı
|
|
157
|
+
const formatHour = (hour) => {
|
|
158
|
+
return `${hour.toString().padStart(2, '0')}:00`;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
if (!isOpen || !resource || !date) return null;
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<div
|
|
165
|
+
className={`daily-view-overlay ${themeType === 'dark' ? 'dark-mode' : ''}`}
|
|
166
|
+
onClick={onClose}
|
|
167
|
+
>
|
|
168
|
+
<div
|
|
169
|
+
ref={containerRef}
|
|
170
|
+
className="daily-view-container"
|
|
171
|
+
onClick={(e) => e.stopPropagation()}
|
|
172
|
+
>
|
|
173
|
+
{/* Header */}
|
|
174
|
+
<div className="daily-view-header">
|
|
175
|
+
<div className="daily-view-header-content">
|
|
176
|
+
<h2 className="daily-view-title">{resource.name || resource.id}</h2>
|
|
177
|
+
<p className="daily-view-date">{formatDate(date)}</p>
|
|
178
|
+
</div>
|
|
179
|
+
<button className="daily-view-close" onClick={onClose}>
|
|
180
|
+
✕
|
|
181
|
+
</button>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
{/* Timeline Body */}
|
|
185
|
+
<div className="daily-view-body">
|
|
186
|
+
{/* Saatler (Sol Taraf) */}
|
|
187
|
+
<div className="daily-view-hours">
|
|
188
|
+
{hours.map(hour => (
|
|
189
|
+
<div key={hour} className="daily-view-hour-label">
|
|
190
|
+
{formatHour(hour)}
|
|
191
|
+
</div>
|
|
192
|
+
))}
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
{/* Timeline Content */}
|
|
196
|
+
<div
|
|
197
|
+
ref={timelineRef}
|
|
198
|
+
className="daily-view-timeline"
|
|
199
|
+
onClick={handleTimelineClick}
|
|
200
|
+
>
|
|
201
|
+
{/* Saat hücreleri */}
|
|
202
|
+
{hours.map(hour => (
|
|
203
|
+
<div
|
|
204
|
+
key={hour}
|
|
205
|
+
className="daily-view-hour-cell"
|
|
206
|
+
/>
|
|
207
|
+
))}
|
|
208
|
+
|
|
209
|
+
{/* Event'ler (timeline üzerinde absolute position) */}
|
|
210
|
+
{dayEvents.map(event => {
|
|
211
|
+
const position = getEventPosition(event);
|
|
212
|
+
return (
|
|
213
|
+
<div
|
|
214
|
+
key={event.id}
|
|
215
|
+
className="daily-view-event"
|
|
216
|
+
style={position}
|
|
217
|
+
onClick={(e) => {
|
|
218
|
+
e.stopPropagation();
|
|
219
|
+
// Event detayları göster
|
|
220
|
+
}}
|
|
221
|
+
>
|
|
222
|
+
<span className="daily-view-event-title">{event.title}</span>
|
|
223
|
+
<span className="daily-view-event-time">
|
|
224
|
+
{new Date(event.startDate).toLocaleTimeString('tr-TR', {
|
|
225
|
+
hour: '2-digit',
|
|
226
|
+
minute: '2-digit'
|
|
227
|
+
})} - {new Date(event.endDate).toLocaleTimeString('tr-TR', {
|
|
228
|
+
hour: '2-digit',
|
|
229
|
+
minute: '2-digit'
|
|
230
|
+
})}
|
|
231
|
+
</span>
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
})}
|
|
235
|
+
|
|
236
|
+
{/* Geçici event (oluşturuluyor) */}
|
|
237
|
+
{isCreating && tempEvent && (
|
|
238
|
+
<div
|
|
239
|
+
className="daily-view-temp-event"
|
|
240
|
+
style={getEventPosition({
|
|
241
|
+
startDate: tempEvent.startDate,
|
|
242
|
+
endDate: tempEvent.endDate,
|
|
243
|
+
})}
|
|
244
|
+
>
|
|
245
|
+
Yeni Randevu
|
|
246
|
+
</div>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
export default DailyView;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
const DatePickerComponent = ({ onDateSelect }) => {
|
|
4
|
+
return (
|
|
5
|
+
<input
|
|
6
|
+
type="date"
|
|
7
|
+
onChange={(e) => onDateSelect(new Date(e.target.value))}
|
|
8
|
+
className="master-header-date-picker"
|
|
9
|
+
/>
|
|
10
|
+
);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default DatePickerComponent;
|
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* "dd/mm/yyyy" formatındaki bir tarih string'ini Date objesine dönüştürür.
|
|
3
|
-
* Eğer dateInput bir string değilse, direkt Date objesini döndürür.
|
|
4
|
-
* @param {string | Object | Date} dateInput - "dd/mm/yyyy" formatında tarih stringi veya {fullDate: Date, display: string} objesi veya Date objesi.
|
|
5
|
-
* @returns {Date} - Date objesi.
|
|
6
|
-
*/
|
|
7
|
-
export const parseDate = (dateInput) => {
|
|
8
|
-
if (dateInput instanceof Date) {
|
|
9
|
-
return dateInput;
|
|
10
|
-
}
|
|
11
|
-
if (typeof dateInput === 'string') {
|
|
12
|
-
const [day, month, year] = dateInput.split("/").map(Number);
|
|
13
|
-
return new Date(year, month - 1, day);
|
|
14
|
-
} else if (typeof dateInput === 'object' && dateInput.fullDate instanceof Date) {
|
|
15
|
-
return new Date(dateInput.fullDate);
|
|
16
|
-
}
|
|
17
|
-
else {
|
|
18
|
-
console.error("parseDate received invalid input:", dateInput);
|
|
19
|
-
return new Date(); // veya hata fırlat
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Bir tarihin belirli bir aralık içinde olup olmadığını kontrol eder.
|
|
25
|
-
* @param {string | Object | Date} date - "dd/mm/yyyy" formatında tarih stringi, {fullDate: Date, display: string} objesi veya Date objesi.
|
|
26
|
-
* @param {string | Object | Date} startDate - "dd/mm/yyyy" formatında başlangıç tarihi stringi, {fullDate: Date, display: string} objesi veya Date objesi.
|
|
27
|
-
* @param {string | Object | Date} endDate - "dd/mm/yyyy" formatında bitiş tarihi stringi, {fullDate: Date, display: string} objesi veya Date objesi.
|
|
28
|
-
* @returns {boolean} - Tarih aralık içinde ise true, değilse false.
|
|
29
|
-
*/
|
|
30
|
-
export const isDateInRange = (date, startDate, endDate) => {
|
|
31
|
-
const d = parseDate(date);
|
|
32
|
-
const start = parseDate(startDate);
|
|
33
|
-
const end = parseDate(endDate);
|
|
34
|
-
return d >= start && d <= end;
|
|
1
|
+
/**
|
|
2
|
+
* "dd/mm/yyyy" formatındaki bir tarih string'ini Date objesine dönüştürür.
|
|
3
|
+
* Eğer dateInput bir string değilse, direkt Date objesini döndürür.
|
|
4
|
+
* @param {string | Object | Date} dateInput - "dd/mm/yyyy" formatında tarih stringi veya {fullDate: Date, display: string} objesi veya Date objesi.
|
|
5
|
+
* @returns {Date} - Date objesi.
|
|
6
|
+
*/
|
|
7
|
+
export const parseDate = (dateInput) => {
|
|
8
|
+
if (dateInput instanceof Date) {
|
|
9
|
+
return dateInput;
|
|
10
|
+
}
|
|
11
|
+
if (typeof dateInput === 'string') {
|
|
12
|
+
const [day, month, year] = dateInput.split("/").map(Number);
|
|
13
|
+
return new Date(year, month - 1, day);
|
|
14
|
+
} else if (typeof dateInput === 'object' && dateInput.fullDate instanceof Date) {
|
|
15
|
+
return new Date(dateInput.fullDate);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
console.error("parseDate received invalid input:", dateInput);
|
|
19
|
+
return new Date(); // veya hata fırlat
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Bir tarihin belirli bir aralık içinde olup olmadığını kontrol eder.
|
|
25
|
+
* @param {string | Object | Date} date - "dd/mm/yyyy" formatında tarih stringi, {fullDate: Date, display: string} objesi veya Date objesi.
|
|
26
|
+
* @param {string | Object | Date} startDate - "dd/mm/yyyy" formatında başlangıç tarihi stringi, {fullDate: Date, display: string} objesi veya Date objesi.
|
|
27
|
+
* @param {string | Object | Date} endDate - "dd/mm/yyyy" formatında bitiş tarihi stringi, {fullDate: Date, display: string} objesi veya Date objesi.
|
|
28
|
+
* @returns {boolean} - Tarih aralık içinde ise true, değilse false.
|
|
29
|
+
*/
|
|
30
|
+
export const isDateInRange = (date, startDate, endDate) => {
|
|
31
|
+
const d = parseDate(date);
|
|
32
|
+
const start = parseDate(startDate);
|
|
33
|
+
const end = parseDate(endDate);
|
|
34
|
+
return d >= start && d <= end;
|
|
35
35
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import './Timeline.css';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Event Badge Component
|
|
6
|
+
* Önemli event'ler için badge gösterir
|
|
7
|
+
*/
|
|
8
|
+
const EventBadge = ({
|
|
9
|
+
text,
|
|
10
|
+
type = 'default', // 'default', 'important', 'urgent', 'new', 'custom'
|
|
11
|
+
position = 'top-right', // 'top-right', 'top-left', 'bottom-right', 'bottom-left'
|
|
12
|
+
className = '',
|
|
13
|
+
style = {}
|
|
14
|
+
}) => {
|
|
15
|
+
return (
|
|
16
|
+
<span
|
|
17
|
+
className={`event-badge event-badge-${type} event-badge-${position} ${className}`}
|
|
18
|
+
style={style}
|
|
19
|
+
>
|
|
20
|
+
{text}
|
|
21
|
+
</span>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default EventBadge;
|
|
26
|
+
|