@hellobetterdigitalnz/betterui 0.0.3-310 → 0.0.3-312
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Components/DataDisplay/GanttChart/GanttChart.d.ts +11 -3
- package/dist/Components/DataDisplay/index.d.ts +1 -0
- package/dist/betterui.css +1 -1
- package/dist/index.cjs.js +10 -10
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +3042 -2773
- package/dist/index.es.js.map +1 -1
- package/package.json +1 -1
- package/src/Components/DataDisplay/GanttChart/{GanttChart.scss → GanttChart.module.scss} +66 -55
- package/src/Components/DataDisplay/GanttChart/GanttChart.tsx +230 -174
- package/src/Components/DataDisplay/index.ts +2 -0
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
useCallback,
|
|
3
|
+
useMemo,
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
useEffect,
|
|
7
|
+
ReactNode,
|
|
8
|
+
} from "react";
|
|
2
9
|
import {
|
|
3
10
|
addDays,
|
|
4
11
|
addMinutes,
|
|
@@ -7,27 +14,33 @@ import {
|
|
|
7
14
|
formatISO,
|
|
8
15
|
startOfDay,
|
|
9
16
|
} from "date-fns";
|
|
10
|
-
import "./GanttChart.scss";
|
|
17
|
+
import styles from "./GanttChart.module.scss";
|
|
11
18
|
import { DotsSixVertical } from "../../Icons";
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
import CaretLeft from "../../Icons/Arrows/CaretLeft/CaretLeft.tsx";
|
|
20
|
+
import CaretRight from "../../Icons/Arrows/CaretRight/CaretRight.tsx";
|
|
14
21
|
|
|
15
22
|
type GanttTask = {
|
|
16
23
|
id: string;
|
|
17
24
|
name: string;
|
|
18
|
-
start: string;
|
|
19
|
-
end: string;
|
|
25
|
+
start: string;
|
|
26
|
+
end: string;
|
|
20
27
|
color?: string;
|
|
21
28
|
user?: string;
|
|
29
|
+
userId?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type GanttTaskChange = {
|
|
33
|
+
task: GanttTask;
|
|
34
|
+
previousTask: GanttTask;
|
|
35
|
+
changeType: "move" | "resize-start" | "resize-end";
|
|
22
36
|
};
|
|
23
37
|
|
|
24
38
|
type GanttChartProps = {
|
|
25
39
|
tasks?: GanttTask[];
|
|
26
|
-
onChange?: (tasks: GanttTask[]) => void;
|
|
40
|
+
onChange?: (tasks: GanttTask[], change?: GanttTaskChange) => void;
|
|
41
|
+
dropdown?:ReactNode
|
|
27
42
|
};
|
|
28
43
|
|
|
29
|
-
// -------------------- Utils --------------------
|
|
30
|
-
|
|
31
44
|
function dateToMinuteIndex(base: Date, d: Date) {
|
|
32
45
|
return differenceInMinutes(d, base);
|
|
33
46
|
}
|
|
@@ -40,60 +53,18 @@ function clamp(n: number, min: number, max: number) {
|
|
|
40
53
|
return Math.min(max, Math.max(min, n));
|
|
41
54
|
}
|
|
42
55
|
|
|
43
|
-
// -------------------- Component --------------------
|
|
44
56
|
|
|
45
|
-
|
|
57
|
+
const GanttChart = ({ tasks: initialTasks, onChange, dropdown }: GanttChartProps)=> {
|
|
46
58
|
const [currentDay, setCurrentDay] = useState<Date>(startOfDay(new Date()));
|
|
47
59
|
const startDate = startOfDay(currentDay);
|
|
48
|
-
const totalDays =
|
|
60
|
+
const totalDays = 7;
|
|
61
|
+
|
|
49
62
|
|
|
50
|
-
// Use header hours for sizing (stable across rows). Avoid a shared ref across many rows.
|
|
51
63
|
const headerHoursRef = useRef<HTMLDivElement>(null);
|
|
52
|
-
const [cellSize, setCellSize] = useState<number>(60);
|
|
64
|
+
const [cellSize, setCellSize] = useState<number>(60);
|
|
53
65
|
|
|
54
66
|
const [tasks, setTasks] = useState<GanttTask[]>(
|
|
55
|
-
initialTasks ?? [
|
|
56
|
-
{
|
|
57
|
-
id: "1",
|
|
58
|
-
name: "Design",
|
|
59
|
-
start: "2025-08-27T05:30:00",
|
|
60
|
-
end: "2025-08-27T10:30:00",
|
|
61
|
-
color: "blue",
|
|
62
|
-
user: "Alice",
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
id: "2",
|
|
66
|
-
name: "Development",
|
|
67
|
-
start: "2025-08-27T09:45:00",
|
|
68
|
-
end: "2025-08-27T17:30:00", // FIX: valid 24h time and correct date
|
|
69
|
-
color: "green",
|
|
70
|
-
user: "Bob",
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
id: "3",
|
|
74
|
-
name: "QA",
|
|
75
|
-
start: "2025-08-27T08:10:00",
|
|
76
|
-
end: "2025-08-27T09:40:00",
|
|
77
|
-
color: "orange",
|
|
78
|
-
user: "Charlie",
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
id: "4",
|
|
82
|
-
name: "QA",
|
|
83
|
-
start: "2025-08-27T08:10:00",
|
|
84
|
-
end: "2025-08-27T09:40:00",
|
|
85
|
-
color: "red",
|
|
86
|
-
user: "Mamudu",
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
id: "5",
|
|
90
|
-
name: "QA",
|
|
91
|
-
start: "2025-08-27T08:10:00",
|
|
92
|
-
end: "2025-08-27T09:40:00",
|
|
93
|
-
color: "black",
|
|
94
|
-
user: "",
|
|
95
|
-
},
|
|
96
|
-
]
|
|
67
|
+
initialTasks ?? []
|
|
97
68
|
);
|
|
98
69
|
|
|
99
70
|
const [dragGhost, setDragGhost] = useState<null | {
|
|
@@ -101,21 +72,62 @@ export default function GanttChart({ tasks: initialTasks, onChange }: GanttChart
|
|
|
101
72
|
x: number;
|
|
102
73
|
y: number;
|
|
103
74
|
rowUser: string;
|
|
75
|
+
rowUserId: string;
|
|
104
76
|
}>(null);
|
|
105
77
|
|
|
106
78
|
const minuteWidth = useMemo(() => (cellSize > 0 ? cellSize / 60 : 1), [cellSize]);
|
|
107
79
|
const pxToMinutes = useCallback((px: number) => px / minuteWidth, [minuteWidth]);
|
|
108
80
|
const minutesToPx = useCallback((minutes: number) => minutes * minuteWidth, [minuteWidth]);
|
|
109
81
|
|
|
110
|
-
const
|
|
111
|
-
return Array.from({ length:
|
|
112
|
-
}, [startDate]);
|
|
82
|
+
const daysArray = useMemo(() => {
|
|
83
|
+
return Array.from({ length: totalDays }).map((_, i) => addDays(startDate, i));
|
|
84
|
+
}, [startDate, totalDays]);
|
|
85
|
+
|
|
86
|
+
// const timeUnits = useMemo(() => {
|
|
87
|
+
// return Array.from({ length: 24 }).map((_, h) => addMinutes(startDate, h * 60));
|
|
88
|
+
// }, [startDate]);
|
|
89
|
+
|
|
90
|
+
// Group tasks by user and maintain all unique users
|
|
91
|
+
const groupedTasks = useMemo(() => {
|
|
92
|
+
// Collect all unique users from tasks with their userIds
|
|
93
|
+
const allUsers = new Map<string, string>();
|
|
94
|
+
tasks.forEach((task) => {
|
|
95
|
+
const userName = task.user?.trim() || "";
|
|
96
|
+
allUsers.set(userName, task.userId || "");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const groups = new Map<string, GanttTask[]>();
|
|
100
|
+
|
|
101
|
+
// Initialize groups for all users
|
|
102
|
+
allUsers.forEach((_, userName) => {
|
|
103
|
+
groups.set(userName, []);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
// Populate groups with tasks
|
|
108
|
+
tasks.forEach((task) => {
|
|
109
|
+
const key = task.user?.trim() || "";
|
|
110
|
+
groups.get(key)!.push(task);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Sort so unassigned ("") comes first, and store userId alongside
|
|
114
|
+
const sorted = Array.from(groups.entries()).map(([userName, tasks]) => {
|
|
115
|
+
const userId = allUsers.get(userName) || "";
|
|
116
|
+
return { userName, userId, tasks };
|
|
117
|
+
}).sort((a, b) => {
|
|
118
|
+
if (!a.userName) return -1;
|
|
119
|
+
if (!b.userName) return 1;
|
|
120
|
+
return a.userName.localeCompare(b.userName);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return sorted;
|
|
124
|
+
}, [tasks]);
|
|
113
125
|
|
|
114
126
|
useEffect(() => {
|
|
115
127
|
const updateSize = () => {
|
|
116
|
-
const firstHour = headerHoursRef.current?.querySelector
|
|
128
|
+
const firstHour = headerHoursRef.current?.querySelector(`.${styles.hour}`);
|
|
117
129
|
if (firstHour) {
|
|
118
|
-
setCellSize(firstHour.offsetWidth);
|
|
130
|
+
setCellSize((firstHour as HTMLElement).offsetWidth);
|
|
119
131
|
}
|
|
120
132
|
};
|
|
121
133
|
|
|
@@ -124,12 +136,11 @@ export default function GanttChart({ tasks: initialTasks, onChange }: GanttChart
|
|
|
124
136
|
return () => window.removeEventListener("resize", updateSize);
|
|
125
137
|
}, []);
|
|
126
138
|
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}, [
|
|
139
|
+
// useEffect(() => {
|
|
140
|
+
// // Only call onChange for initial load, not for drag operations
|
|
141
|
+
// // Drag operations handle onChange in onPointerUp
|
|
142
|
+
// }, []);
|
|
131
143
|
|
|
132
|
-
// Drag state
|
|
133
144
|
const dragState = useRef<null | {
|
|
134
145
|
mode: "move" | "resize-start" | "resize-end";
|
|
135
146
|
taskId: string;
|
|
@@ -137,17 +148,19 @@ export default function GanttChart({ tasks: initialTasks, onChange }: GanttChart
|
|
|
137
148
|
originLeftMin: number;
|
|
138
149
|
originRightMin: number;
|
|
139
150
|
originUser?: string;
|
|
151
|
+
originalTask?: GanttTask;
|
|
140
152
|
}>(null);
|
|
141
153
|
|
|
142
|
-
function getRowInfoFromPoint(x: number, y: number): { user: string; daysEl: HTMLElement | null } {
|
|
154
|
+
function getRowInfoFromPoint(x: number, y: number): { user: string; userId: string; daysEl: HTMLElement | null } {
|
|
143
155
|
const el = document.elementFromPoint(x, y) as HTMLElement | null;
|
|
144
|
-
const row = el?.closest(
|
|
145
|
-
if (!row) return { user: "", daysEl: null };
|
|
146
|
-
|
|
147
|
-
const label = row.querySelector(
|
|
148
|
-
const user = label && label !== "Unassigned" ? label : "";
|
|
149
|
-
const
|
|
150
|
-
|
|
156
|
+
const row = el?.closest(`.${styles.ganttRow}`) as HTMLElement | null;
|
|
157
|
+
if (!row) return { user: "", userId: "", daysEl: null };
|
|
158
|
+
|
|
159
|
+
const label = row.querySelector(`.${styles.taskColLabel}`)?.textContent?.trim();
|
|
160
|
+
const user = label && label !== "Unassigned" ? label : "";
|
|
161
|
+
const userId = row.getAttribute("data-user-id") || "";
|
|
162
|
+
const daysEl = row.querySelector(`.${styles.days}`) as HTMLElement | null;
|
|
163
|
+
return { user, userId, daysEl };
|
|
151
164
|
}
|
|
152
165
|
|
|
153
166
|
const onPointerDownBar = (
|
|
@@ -156,6 +169,7 @@ export default function GanttChart({ tasks: initialTasks, onChange }: GanttChart
|
|
|
156
169
|
mode: "move" | "resize-start" | "resize-end"
|
|
157
170
|
) => {
|
|
158
171
|
e.preventDefault();
|
|
172
|
+
e.stopPropagation();
|
|
159
173
|
const task = tasks.find((t) => t.id === taskId);
|
|
160
174
|
if (!task) return;
|
|
161
175
|
|
|
@@ -172,53 +186,44 @@ export default function GanttChart({ tasks: initialTasks, onChange }: GanttChart
|
|
|
172
186
|
originLeftMin: dateToMinuteIndex(startDate, taskStart),
|
|
173
187
|
originRightMin: dateToMinuteIndex(startDate, taskEnd),
|
|
174
188
|
originUser: task.user ?? "",
|
|
189
|
+
originalTask: { ...task },
|
|
175
190
|
};
|
|
176
191
|
|
|
177
192
|
if (mode === "move") {
|
|
178
|
-
setDragGhost({ taskId, x: e.clientX, y: e.clientY, rowUser: task.user ?? "" });
|
|
193
|
+
setDragGhost({ taskId, x: e.clientX, y: e.clientY, rowUser: task.user ?? "", rowUserId: task.userId ?? "" });
|
|
179
194
|
}
|
|
180
195
|
|
|
181
196
|
const onPointerMove = (ev: PointerEvent) => {
|
|
182
197
|
if (!dragState.current) return;
|
|
183
198
|
if (minuteWidth <= 0) return;
|
|
184
199
|
const ds = dragState.current;
|
|
185
|
-
const dx = ev.clientX - ds.originX;
|
|
186
|
-
const deltaMinutes = Math.round(pxToMinutes(dx));
|
|
187
200
|
|
|
188
201
|
if (ds.mode === "move") {
|
|
189
|
-
const { user: hoverUser } = getRowInfoFromPoint(ev.clientX, ev.clientY);
|
|
202
|
+
const { user: hoverUser, userId: hoverUserId } = getRowInfoFromPoint(ev.clientX, ev.clientY);
|
|
190
203
|
setDragGhost((ghost) =>
|
|
191
|
-
ghost ? { ...ghost, x: ev.clientX, y: ev.clientY, rowUser: hoverUser } : null
|
|
204
|
+
ghost ? { ...ghost, x: ev.clientX, y: ev.clientY, rowUser: hoverUser, rowUserId: hoverUserId } : null
|
|
192
205
|
);
|
|
193
|
-
}
|
|
206
|
+
} else {
|
|
207
|
+
// Only update task position for resize operations
|
|
208
|
+
const dx = ev.clientX - ds.originX;
|
|
209
|
+
const deltaMinutes = Math.round(pxToMinutes(dx));
|
|
194
210
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (ds.mode === "move") {
|
|
200
|
-
const widthMinutes = minutesBetween(new Date(t.start), new Date(t.end));
|
|
201
|
-
const newLeft = clamp(ds.originLeftMin + deltaMinutes, 0, totalDays * 24 * 60 - widthMinutes);
|
|
202
|
-
const newStart = addMinutes(startDate, newLeft);
|
|
203
|
-
const newEnd = addMinutes(startDate, newLeft + widthMinutes);
|
|
204
|
-
return {
|
|
205
|
-
...t,
|
|
206
|
-
start: formatISO(newStart, { representation: "complete" }),
|
|
207
|
-
end: formatISO(newEnd, { representation: "complete" }),
|
|
208
|
-
};
|
|
209
|
-
}
|
|
211
|
+
setTasks((prev) =>
|
|
212
|
+
prev.map((t) => {
|
|
213
|
+
if (t.id !== ds.taskId) return t;
|
|
210
214
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
215
|
+
if (ds.mode === "resize-start") {
|
|
216
|
+
const newLeft = clamp(ds.originLeftMin + deltaMinutes, 0, ds.originRightMin - 1);
|
|
217
|
+
const newStart = addMinutes(startDate, newLeft);
|
|
218
|
+
return { ...t, start: formatISO(newStart, { representation: "complete" }) };
|
|
219
|
+
}
|
|
216
220
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
221
|
+
const newRight = clamp(ds.originRightMin + deltaMinutes, ds.originLeftMin + 1, totalDays * 24 * 60);
|
|
222
|
+
const newEnd = addMinutes(startDate, newRight);
|
|
223
|
+
return { ...t, end: formatISO(newEnd, { representation: "complete" }) };
|
|
224
|
+
})
|
|
225
|
+
);
|
|
226
|
+
}
|
|
222
227
|
};
|
|
223
228
|
|
|
224
229
|
const onPointerUp = (ev: PointerEvent) => {
|
|
@@ -226,30 +231,62 @@ export default function GanttChart({ tasks: initialTasks, onChange }: GanttChart
|
|
|
226
231
|
target.releasePointerCapture(e.pointerId);
|
|
227
232
|
} catch {}
|
|
228
233
|
|
|
229
|
-
const { user: dropUser, daysEl } = dragGhost
|
|
230
|
-
? getRowInfoFromPoint(dragGhost.x, dragGhost.y)
|
|
231
|
-
: getRowInfoFromPoint(ev.clientX, ev.clientY);
|
|
232
|
-
|
|
233
234
|
const ds = dragState.current;
|
|
234
|
-
if (ds && daysEl) {
|
|
235
|
-
const rect = daysEl.getBoundingClientRect();
|
|
236
|
-
const dropX = (dragGhost?.x ?? ev.clientX) - rect.left;
|
|
237
|
-
const dropMinutes = Math.round(pxToMinutes(dropX));
|
|
238
235
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
236
|
+
if (ds && ds.mode === "move") {
|
|
237
|
+
const { user: dropUser, userId: dropUserId, daysEl } = dragGhost
|
|
238
|
+
? getRowInfoFromPoint(dragGhost.x, dragGhost.y)
|
|
239
|
+
: getRowInfoFromPoint(ev.clientX, ev.clientY);
|
|
240
|
+
|
|
241
|
+
if (daysEl) {
|
|
242
|
+
const rect = daysEl.getBoundingClientRect();
|
|
243
|
+
const dropX = (dragGhost?.x ?? ev.clientX) - rect.left;
|
|
244
|
+
const dropMinutes = Math.round(pxToMinutes(dropX));
|
|
245
|
+
|
|
246
|
+
setTasks((prev) => {
|
|
247
|
+
const updatedTasks = prev.map((t) => {
|
|
248
|
+
if (t.id !== ds.taskId) return t;
|
|
249
|
+
const duration = minutesBetween(new Date(t.start), new Date(t.end));
|
|
250
|
+
const maxMinutes = totalDays * 24 * 60 - duration;
|
|
251
|
+
const newLeft = clamp(dropMinutes, 0, maxMinutes);
|
|
252
|
+
const newStart = addMinutes(startDate, newLeft);
|
|
253
|
+
const newEnd = addMinutes(newStart, duration);
|
|
254
|
+
return {
|
|
255
|
+
...t,
|
|
256
|
+
start: formatISO(newStart, { representation: "complete" }),
|
|
257
|
+
end: formatISO(newEnd, { representation: "complete" }),
|
|
258
|
+
user: dropUser !== undefined ? dropUser : t.user,
|
|
259
|
+
userId: dropUserId !== undefined ? dropUserId : t.userId,
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Call onChange with change details
|
|
264
|
+
if (onChange && ds.originalTask) {
|
|
265
|
+
const updatedTask = updatedTasks.find(t => t.id === ds.taskId);
|
|
266
|
+
if (updatedTask) {
|
|
267
|
+
onChange(updatedTasks, {
|
|
268
|
+
task: updatedTask,
|
|
269
|
+
previousTask: ds.originalTask,
|
|
270
|
+
changeType: ds.mode,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return updatedTasks;
|
|
251
276
|
});
|
|
252
|
-
}
|
|
277
|
+
}
|
|
278
|
+
} else if (ds && (ds.mode === "resize-start" || ds.mode === "resize-end")) {
|
|
279
|
+
// For resize operations, call onChange
|
|
280
|
+
if (onChange && ds.originalTask) {
|
|
281
|
+
const currentTask = tasks.find(t => t.id === ds.taskId);
|
|
282
|
+
if (currentTask) {
|
|
283
|
+
onChange(tasks, {
|
|
284
|
+
task: currentTask,
|
|
285
|
+
previousTask: ds.originalTask,
|
|
286
|
+
changeType: ds.mode,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
253
290
|
}
|
|
254
291
|
|
|
255
292
|
setDragGhost(null);
|
|
@@ -266,15 +303,15 @@ export default function GanttChart({ tasks: initialTasks, onChange }: GanttChart
|
|
|
266
303
|
const taskStart = new Date(task.start);
|
|
267
304
|
const taskEnd = new Date(task.end);
|
|
268
305
|
|
|
269
|
-
const
|
|
270
|
-
const
|
|
306
|
+
const rangeStart = startDate;
|
|
307
|
+
const rangeEnd = addDays(startDate, totalDays);
|
|
271
308
|
|
|
272
|
-
const displayStart = taskStart <
|
|
273
|
-
const displayEnd = taskEnd >
|
|
309
|
+
const displayStart = taskStart < rangeStart ? rangeStart : taskStart;
|
|
310
|
+
const displayEnd = taskEnd > rangeEnd ? rangeEnd : taskEnd;
|
|
274
311
|
|
|
275
312
|
if (displayStart >= displayEnd) return null;
|
|
276
313
|
|
|
277
|
-
const minutesFromStart = dateToMinuteIndex(
|
|
314
|
+
const minutesFromStart = dateToMinuteIndex(rangeStart, displayStart);
|
|
278
315
|
const minutesWidth = minutesBetween(displayStart, displayEnd);
|
|
279
316
|
|
|
280
317
|
const leftPx = minutesToPx(minutesFromStart);
|
|
@@ -282,7 +319,8 @@ export default function GanttChart({ tasks: initialTasks, onChange }: GanttChart
|
|
|
282
319
|
|
|
283
320
|
return (
|
|
284
321
|
<div
|
|
285
|
-
|
|
322
|
+
key={task.id}
|
|
323
|
+
className={styles.taskBar}
|
|
286
324
|
style={{
|
|
287
325
|
left: leftPx,
|
|
288
326
|
width: widthPx,
|
|
@@ -290,61 +328,71 @@ export default function GanttChart({ tasks: initialTasks, onChange }: GanttChart
|
|
|
290
328
|
opacity: dragGhost?.taskId === task.id ? 0.4 : 1,
|
|
291
329
|
}}
|
|
292
330
|
>
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
<div onPointerDown={(e) => onPointerDownBar(e, task.id, "move")} className={"bar-drag"}>
|
|
331
|
+
<div className={styles.bar}>
|
|
332
|
+
<div onPointerDown={(e) => onPointerDownBar(e, task.id, "move")} className={styles.barDrag}>
|
|
296
333
|
<DotsSixVertical />
|
|
297
334
|
</div>
|
|
298
335
|
<span>{task.name}</span>
|
|
299
336
|
</div>
|
|
300
|
-
{/* <div className="resize-handle right" onPointerDown={(e) => onPointerDownBar(e, task.id, "resize-end")} /> */}
|
|
301
337
|
</div>
|
|
302
338
|
);
|
|
303
339
|
};
|
|
304
340
|
|
|
305
341
|
return (
|
|
306
342
|
<>
|
|
307
|
-
<div className=
|
|
308
|
-
<
|
|
309
|
-
|
|
310
|
-
|
|
343
|
+
<div className={styles.monthControls}>
|
|
344
|
+
<div className={styles.monthControlLeft}>
|
|
345
|
+
<div className={styles.prevBtn} onClick={() => setCurrentDay((d) => addDays(d, -1))}><CaretLeft/></div>
|
|
346
|
+
<div className={styles.nxtBtn} onClick={() => setCurrentDay((d) => addDays(d, 1))}><CaretRight/></div>
|
|
347
|
+
<span className={styles.period}>{format(currentDay, "MMMM dd, yyyy")}</span>
|
|
348
|
+
</div>
|
|
349
|
+
<div className={styles.monthControlRight}>
|
|
350
|
+
{dropdown}
|
|
351
|
+
</div>
|
|
311
352
|
</div>
|
|
312
353
|
|
|
313
|
-
<div className=
|
|
314
|
-
<div className=
|
|
315
|
-
<div className=
|
|
316
|
-
<div className=
|
|
317
|
-
|
|
318
|
-
<div
|
|
319
|
-
|
|
320
|
-
{
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
354
|
+
<div className={styles.ganttChart}>
|
|
355
|
+
<div className={styles.ganttHeader}>
|
|
356
|
+
<div className={styles.taskCol}></div>
|
|
357
|
+
<div className={styles.days}>
|
|
358
|
+
{daysArray.map((day, idx) => (
|
|
359
|
+
<div
|
|
360
|
+
key={day.toISOString()}
|
|
361
|
+
className={`${styles.dateDayView} ${idx === 0 ? styles.currentDay : ''}`}
|
|
362
|
+
>
|
|
363
|
+
<div className={styles.singleDate}>{format(day, "MM/dd")}</div>
|
|
364
|
+
<div className={styles.hours} ref={idx === 0 ? headerHoursRef : null}>
|
|
365
|
+
{Array.from({ length: 24 }).map((_, h) => (
|
|
366
|
+
<div className={styles.hour} key={h}>
|
|
367
|
+
{`${String(h).padStart(2, "0")}:00`}
|
|
368
|
+
</div>
|
|
369
|
+
))}
|
|
370
|
+
</div>
|
|
325
371
|
</div>
|
|
326
|
-
|
|
372
|
+
))}
|
|
327
373
|
</div>
|
|
328
374
|
</div>
|
|
329
375
|
|
|
330
|
-
<div className=
|
|
331
|
-
{
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
<div
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
376
|
+
<div className={styles.ganttBody}>
|
|
377
|
+
{groupedTasks.map(({ userName, userId, tasks: userTasks }) => (
|
|
378
|
+
<div
|
|
379
|
+
key={userName || "unassigned"}
|
|
380
|
+
className={styles.ganttRow}
|
|
381
|
+
data-user-id={userId}
|
|
382
|
+
>
|
|
383
|
+
<div className={styles.taskColLabel}>{userName || "Unassigned"}</div>
|
|
384
|
+
<div className={styles.days}>
|
|
385
|
+
{daysArray.map((day) => (
|
|
386
|
+
<div key={day.toISOString()} className={styles.dayColumn}>
|
|
387
|
+
{Array.from({ length: 24 }).map((_, h) => (
|
|
388
|
+
<div className={styles.dayHrs} key={h} />
|
|
389
|
+
))}
|
|
390
|
+
</div>
|
|
391
|
+
))}
|
|
392
|
+
{userTasks.map((task) => renderBar(task))}
|
|
346
393
|
</div>
|
|
347
|
-
|
|
394
|
+
</div>
|
|
395
|
+
))}
|
|
348
396
|
</div>
|
|
349
397
|
</div>
|
|
350
398
|
|
|
@@ -354,15 +402,21 @@ export default function GanttChart({ tasks: initialTasks, onChange }: GanttChart
|
|
|
354
402
|
style={{
|
|
355
403
|
position: "fixed",
|
|
356
404
|
left: dragGhost.x,
|
|
357
|
-
top: dragGhost.y,
|
|
405
|
+
top: dragGhost.y - 12,
|
|
358
406
|
pointerEvents: "none",
|
|
359
|
-
opacity: 0.
|
|
407
|
+
opacity: 0.9,
|
|
360
408
|
background: tasks.find((t) => t.id === dragGhost.taskId)?.color ?? "gray",
|
|
361
|
-
padding: "
|
|
409
|
+
padding: "4px 8px",
|
|
362
410
|
height: "24px",
|
|
363
|
-
|
|
411
|
+
minWidth: "100px",
|
|
364
412
|
borderRadius: 4,
|
|
365
413
|
zIndex: 9999,
|
|
414
|
+
display: "flex",
|
|
415
|
+
alignItems: "center",
|
|
416
|
+
fontSize: "12px",
|
|
417
|
+
color: "white",
|
|
418
|
+
fontWeight: 500,
|
|
419
|
+
boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
|
|
366
420
|
}}
|
|
367
421
|
>
|
|
368
422
|
<span>{tasks.find((t) => t.id === dragGhost.taskId)?.name}</span>
|
|
@@ -371,3 +425,5 @@ export default function GanttChart({ tasks: initialTasks, onChange }: GanttChart
|
|
|
371
425
|
</>
|
|
372
426
|
);
|
|
373
427
|
}
|
|
428
|
+
|
|
429
|
+
export default GanttChart
|
|
@@ -85,3 +85,5 @@ export type { default as TableRowProps} from './Table/TableRowProps';
|
|
|
85
85
|
export {default as CalendarView} from './Calendar/Calendar.tsx'
|
|
86
86
|
export type {default as CalendarProps} from './Calendar/CalendarProps.tsx'
|
|
87
87
|
|
|
88
|
+
export {default as GanttChart} from './GanttChart/GanttChart.tsx'
|
|
89
|
+
|