@visns-studio/visns-components 5.15.25 → 5.15.27
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/package.json +11 -11
- package/src/components/DataGrid.jsx +13 -30
- package/src/components/ImportWizardModal.jsx +0 -8
- package/src/components/controls/DataGridSearch.jsx +0 -5
- package/src/components/generic/ExcelStyleGrid.jsx +789 -0
- package/src/components/generic/GenericDashboard.jsx +9 -1
- package/src/components/generic/GenericDetail.jsx +0 -3
- package/src/components/generic/TimelineWidget.jsx +61 -0
- package/src/components/hooks/useExcelGrid.js +366 -0
- package/src/components/styles/ExcelStyleGrid.css +251 -0
- package/src/components/styles/GenericDashboard.module.scss +2 -2
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
import React, { useState, useRef } from 'react';
|
|
2
|
+
import moment from 'moment';
|
|
3
|
+
|
|
4
|
+
const ExcelStyleGrid = ({
|
|
5
|
+
gridData = [],
|
|
6
|
+
dateColumns = [],
|
|
7
|
+
facilitiesByCategory = {},
|
|
8
|
+
facilityColors = {},
|
|
9
|
+
showScrollHint = true,
|
|
10
|
+
setShowScrollHint,
|
|
11
|
+
maxWidth = '1150px',
|
|
12
|
+
maxHeight = '400px',
|
|
13
|
+
onProjectClick,
|
|
14
|
+
}) => {
|
|
15
|
+
const scrollContainerRef = useRef(null);
|
|
16
|
+
const [tooltip, setTooltip] = useState({
|
|
17
|
+
visible: false,
|
|
18
|
+
data: null,
|
|
19
|
+
position: { x: 0, y: 0 },
|
|
20
|
+
});
|
|
21
|
+
const [hoveredColumn, setHoveredColumn] = useState(null);
|
|
22
|
+
const [hoveredRow, setHoveredRow] = useState(null);
|
|
23
|
+
|
|
24
|
+
// Helper function to darken hex color for borders
|
|
25
|
+
const darkenColor = (hex, percent) => {
|
|
26
|
+
const num = parseInt(hex.replace('#', ''), 16);
|
|
27
|
+
const amt = Math.round(2.55 * percent);
|
|
28
|
+
const R = (num >> 16) - amt;
|
|
29
|
+
const G = ((num >> 8) & 0x00ff) - amt;
|
|
30
|
+
const B = (num & 0x0000ff) - amt;
|
|
31
|
+
return (
|
|
32
|
+
'#' +
|
|
33
|
+
(
|
|
34
|
+
0x1000000 +
|
|
35
|
+
(R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
|
|
36
|
+
(G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
|
|
37
|
+
(B < 255 ? (B < 1 ? 0 : B) : 255)
|
|
38
|
+
)
|
|
39
|
+
.toString(16)
|
|
40
|
+
.slice(1)
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Helper function to get intelligent text color based on background
|
|
45
|
+
const getContrastTextColor = (backgroundColor) => {
|
|
46
|
+
// Convert hex to RGB
|
|
47
|
+
const hex = backgroundColor.replace('#', '');
|
|
48
|
+
const r = parseInt(hex.substr(0, 2), 16);
|
|
49
|
+
const g = parseInt(hex.substr(2, 2), 16);
|
|
50
|
+
const b = parseInt(hex.substr(4, 2), 16);
|
|
51
|
+
|
|
52
|
+
// Calculate relative luminance
|
|
53
|
+
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
54
|
+
|
|
55
|
+
// Return black for light backgrounds, white for dark backgrounds
|
|
56
|
+
return luminance > 0.5 ? '#333333' : '#ffffff';
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Helper function to get cell color based on confirmation status
|
|
60
|
+
const getCellColor = (confirmed, isHovered = false) => {
|
|
61
|
+
const baseColors = {
|
|
62
|
+
confirmed: '#c8e6c9', // Soft mint green for confirmed
|
|
63
|
+
unconfirmed: '#ffe0b2', // Soft peach for unconfirmed
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const color = Boolean(confirmed) ? baseColors.confirmed : baseColors.unconfirmed;
|
|
67
|
+
|
|
68
|
+
// Add slight transparency for hover effect
|
|
69
|
+
return isHovered ? `${color}DD` : color;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Find date range for a booking
|
|
73
|
+
const getDateRangeForCell = (row, currentDate) => {
|
|
74
|
+
const currentCell = row.cells[currentDate];
|
|
75
|
+
if (!currentCell.hasData) return null;
|
|
76
|
+
|
|
77
|
+
let startDate = currentDate;
|
|
78
|
+
let endDate = currentDate;
|
|
79
|
+
|
|
80
|
+
// Find start of booking by going backwards
|
|
81
|
+
const sortedDates = Object.keys(row.cells).sort();
|
|
82
|
+
const currentIndex = sortedDates.indexOf(currentDate);
|
|
83
|
+
|
|
84
|
+
for (let i = currentIndex - 1; i >= 0; i--) {
|
|
85
|
+
const date = sortedDates[i];
|
|
86
|
+
const cell = row.cells[date];
|
|
87
|
+
if (
|
|
88
|
+
cell.hasData &&
|
|
89
|
+
cell.data?.resource === currentCell.data?.resource &&
|
|
90
|
+
cell.data?.client === currentCell.data?.client
|
|
91
|
+
) {
|
|
92
|
+
startDate = date;
|
|
93
|
+
} else {
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Find end of booking by going forwards
|
|
99
|
+
for (let i = currentIndex + 1; i < sortedDates.length; i++) {
|
|
100
|
+
const date = sortedDates[i];
|
|
101
|
+
const cell = row.cells[date];
|
|
102
|
+
if (
|
|
103
|
+
cell.hasData &&
|
|
104
|
+
cell.data?.resource === currentCell.data?.resource &&
|
|
105
|
+
cell.data?.client === currentCell.data?.client
|
|
106
|
+
) {
|
|
107
|
+
endDate = date;
|
|
108
|
+
} else {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { startDate, endDate };
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Grid cell tooltip component
|
|
117
|
+
const GridTooltip = ({ data, visible, position }) => {
|
|
118
|
+
if (!visible || !data) return null;
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div
|
|
122
|
+
style={{
|
|
123
|
+
position: 'fixed',
|
|
124
|
+
left: position.x + 10,
|
|
125
|
+
top: position.y - 10,
|
|
126
|
+
background: 'white',
|
|
127
|
+
padding: '10px',
|
|
128
|
+
border: '1px solid #ccc',
|
|
129
|
+
borderRadius: '4px',
|
|
130
|
+
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
|
|
131
|
+
zIndex: 1000,
|
|
132
|
+
fontSize: '12px',
|
|
133
|
+
minWidth: '200px',
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
<div>
|
|
137
|
+
<strong>Resource:</strong> {data.resource}
|
|
138
|
+
</div>
|
|
139
|
+
{data.dateRange ? (
|
|
140
|
+
<div>
|
|
141
|
+
<strong>Date Range:</strong> {data.dateRange}
|
|
142
|
+
</div>
|
|
143
|
+
) : (
|
|
144
|
+
<div>
|
|
145
|
+
<strong>Date:</strong> {data.date}
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
{(data.client || data.resource) && (
|
|
149
|
+
<div>
|
|
150
|
+
<strong>User:</strong> {data.client || 'Not specified'}
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
{data.status && (
|
|
154
|
+
<div>
|
|
155
|
+
<strong>Status:</strong> {data.status}
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
if (gridData.length === 0) {
|
|
163
|
+
return (
|
|
164
|
+
<div style={{ padding: '20px', textAlign: 'center' }}>
|
|
165
|
+
No data available to display in the grid.
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<div
|
|
172
|
+
style={{
|
|
173
|
+
position: 'relative',
|
|
174
|
+
width: '100%',
|
|
175
|
+
maxWidth: maxWidth,
|
|
176
|
+
overflow: 'hidden',
|
|
177
|
+
}}
|
|
178
|
+
>
|
|
179
|
+
<div
|
|
180
|
+
className="scroll-indicators"
|
|
181
|
+
style={{
|
|
182
|
+
position: 'absolute',
|
|
183
|
+
width: '100%',
|
|
184
|
+
height: '100%',
|
|
185
|
+
pointerEvents: 'none',
|
|
186
|
+
zIndex: 1001,
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
<div
|
|
190
|
+
className="scroll-left"
|
|
191
|
+
style={{
|
|
192
|
+
display: 'none',
|
|
193
|
+
position: 'absolute',
|
|
194
|
+
left: '8px',
|
|
195
|
+
top: '50%',
|
|
196
|
+
transform: 'translateY(-50%)',
|
|
197
|
+
pointerEvents: 'auto',
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
200
|
+
<button
|
|
201
|
+
onClick={() => {
|
|
202
|
+
if (scrollContainerRef.current) {
|
|
203
|
+
scrollContainerRef.current.scrollLeft -= 300;
|
|
204
|
+
scrollContainerRef.current.focus();
|
|
205
|
+
}
|
|
206
|
+
}}
|
|
207
|
+
aria-label="Scroll left"
|
|
208
|
+
style={{
|
|
209
|
+
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
|
210
|
+
color: '#007bff',
|
|
211
|
+
border: '2px solid #007bff',
|
|
212
|
+
borderRadius: '50%',
|
|
213
|
+
width: '40px',
|
|
214
|
+
height: '40px',
|
|
215
|
+
fontSize: '16px',
|
|
216
|
+
cursor: 'pointer',
|
|
217
|
+
display: 'flex',
|
|
218
|
+
alignItems: 'center',
|
|
219
|
+
justifyContent: 'center',
|
|
220
|
+
boxShadow: '0 4px 12px rgba(0, 123, 255, 0.3)',
|
|
221
|
+
transition: 'all 0.2s ease',
|
|
222
|
+
fontWeight: 'bold',
|
|
223
|
+
}}
|
|
224
|
+
onMouseEnter={(e) => {
|
|
225
|
+
e.target.style.backgroundColor = '#007bff';
|
|
226
|
+
e.target.style.color = 'white';
|
|
227
|
+
e.target.style.transform = 'scale(1.1)';
|
|
228
|
+
}}
|
|
229
|
+
onMouseLeave={(e) => {
|
|
230
|
+
e.target.style.backgroundColor =
|
|
231
|
+
'rgba(255, 255, 255, 0.95)';
|
|
232
|
+
e.target.style.color = '#007bff';
|
|
233
|
+
e.target.style.transform = 'scale(1)';
|
|
234
|
+
}}
|
|
235
|
+
>
|
|
236
|
+
◀
|
|
237
|
+
</button>
|
|
238
|
+
</div>
|
|
239
|
+
<div
|
|
240
|
+
className="scroll-right"
|
|
241
|
+
style={{
|
|
242
|
+
position: 'absolute',
|
|
243
|
+
right: '8px',
|
|
244
|
+
top: '50%',
|
|
245
|
+
transform: 'translateY(-50%)',
|
|
246
|
+
pointerEvents: 'auto',
|
|
247
|
+
}}
|
|
248
|
+
>
|
|
249
|
+
<button
|
|
250
|
+
onClick={() => {
|
|
251
|
+
if (scrollContainerRef.current) {
|
|
252
|
+
scrollContainerRef.current.scrollLeft += 300;
|
|
253
|
+
scrollContainerRef.current.focus();
|
|
254
|
+
}
|
|
255
|
+
}}
|
|
256
|
+
aria-label="Scroll right"
|
|
257
|
+
style={{
|
|
258
|
+
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
|
259
|
+
color: '#007bff',
|
|
260
|
+
border: '2px solid #007bff',
|
|
261
|
+
borderRadius: '50%',
|
|
262
|
+
width: '40px',
|
|
263
|
+
height: '40px',
|
|
264
|
+
fontSize: '16px',
|
|
265
|
+
cursor: 'pointer',
|
|
266
|
+
display: 'flex',
|
|
267
|
+
alignItems: 'center',
|
|
268
|
+
justifyContent: 'center',
|
|
269
|
+
boxShadow: '0 4px 12px rgba(0, 123, 255, 0.3)',
|
|
270
|
+
transition: 'all 0.2s ease',
|
|
271
|
+
fontWeight: 'bold',
|
|
272
|
+
}}
|
|
273
|
+
onMouseEnter={(e) => {
|
|
274
|
+
e.target.style.backgroundColor = '#007bff';
|
|
275
|
+
e.target.style.color = 'white';
|
|
276
|
+
e.target.style.transform = 'scale(1.1)';
|
|
277
|
+
}}
|
|
278
|
+
onMouseLeave={(e) => {
|
|
279
|
+
e.target.style.backgroundColor =
|
|
280
|
+
'rgba(255, 255, 255, 0.95)';
|
|
281
|
+
e.target.style.color = '#007bff';
|
|
282
|
+
e.target.style.transform = 'scale(1)';
|
|
283
|
+
}}
|
|
284
|
+
>
|
|
285
|
+
▶
|
|
286
|
+
</button>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
{/* Excel-style Grid */}
|
|
291
|
+
<div
|
|
292
|
+
className="dock-usage-grid"
|
|
293
|
+
ref={scrollContainerRef}
|
|
294
|
+
style={{
|
|
295
|
+
overflowX: 'auto',
|
|
296
|
+
overflowY: 'auto',
|
|
297
|
+
maxHeight: maxHeight,
|
|
298
|
+
border: '1px solid #dee2e6',
|
|
299
|
+
backgroundColor: '#fff',
|
|
300
|
+
}}
|
|
301
|
+
tabIndex="0"
|
|
302
|
+
onKeyDown={(e) => {
|
|
303
|
+
if (e.key === 'ArrowLeft') {
|
|
304
|
+
e.preventDefault();
|
|
305
|
+
if (scrollContainerRef.current) {
|
|
306
|
+
scrollContainerRef.current.scrollLeft -= 100;
|
|
307
|
+
}
|
|
308
|
+
} else if (e.key === 'ArrowRight') {
|
|
309
|
+
e.preventDefault();
|
|
310
|
+
if (scrollContainerRef.current) {
|
|
311
|
+
scrollContainerRef.current.scrollLeft += 100;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}}
|
|
315
|
+
onScroll={(e) => {
|
|
316
|
+
const container = e.target;
|
|
317
|
+
const scrollLeft = container.scrollLeft;
|
|
318
|
+
const maxScroll =
|
|
319
|
+
container.scrollWidth - container.clientWidth;
|
|
320
|
+
|
|
321
|
+
const leftButton = document.querySelector('.scroll-left');
|
|
322
|
+
const rightButton = document.querySelector('.scroll-right');
|
|
323
|
+
|
|
324
|
+
if (leftButton)
|
|
325
|
+
leftButton.style.display =
|
|
326
|
+
scrollLeft > 20 ? 'block' : 'none';
|
|
327
|
+
if (rightButton)
|
|
328
|
+
rightButton.style.display =
|
|
329
|
+
scrollLeft < maxScroll - 20 ? 'block' : 'none';
|
|
330
|
+
|
|
331
|
+
if (scrollLeft > 0) {
|
|
332
|
+
if (rightButton)
|
|
333
|
+
rightButton.classList.add('has-scrolled');
|
|
334
|
+
if (setShowScrollHint) setShowScrollHint(false);
|
|
335
|
+
} else {
|
|
336
|
+
if (rightButton)
|
|
337
|
+
rightButton.classList.remove('has-scrolled');
|
|
338
|
+
}
|
|
339
|
+
}}
|
|
340
|
+
onMouseLeave={() => {
|
|
341
|
+
setHoveredColumn(null);
|
|
342
|
+
setHoveredRow(null);
|
|
343
|
+
setTooltip({
|
|
344
|
+
visible: false,
|
|
345
|
+
data: null,
|
|
346
|
+
position: { x: 0, y: 0 },
|
|
347
|
+
});
|
|
348
|
+
}}
|
|
349
|
+
>
|
|
350
|
+
<div
|
|
351
|
+
className="grid-container"
|
|
352
|
+
style={{
|
|
353
|
+
minWidth: `${Math.max(600, dateColumns.length * 45 + 200)}px`,
|
|
354
|
+
}}
|
|
355
|
+
>
|
|
356
|
+
{/* Header Row */}
|
|
357
|
+
<div
|
|
358
|
+
className="grid-header"
|
|
359
|
+
style={{
|
|
360
|
+
display: 'grid',
|
|
361
|
+
gridTemplateColumns: `200px repeat(${dateColumns.length}, 45px)`,
|
|
362
|
+
backgroundColor: '#f8f9fa',
|
|
363
|
+
borderBottom: '2px solid #dee2e6',
|
|
364
|
+
position: 'sticky',
|
|
365
|
+
top: 0,
|
|
366
|
+
zIndex: 20,
|
|
367
|
+
}}
|
|
368
|
+
>
|
|
369
|
+
<div
|
|
370
|
+
className="header-cell resource-header"
|
|
371
|
+
style={{
|
|
372
|
+
padding: '4px 8px',
|
|
373
|
+
fontWeight: 'bold',
|
|
374
|
+
border: '1px solid #dee2e6',
|
|
375
|
+
backgroundColor: '#e9ecef',
|
|
376
|
+
fontSize: '11px',
|
|
377
|
+
}}
|
|
378
|
+
>
|
|
379
|
+
Project
|
|
380
|
+
</div>
|
|
381
|
+
{dateColumns.map((dateCol) => (
|
|
382
|
+
<div
|
|
383
|
+
key={`header-${dateCol.date}`}
|
|
384
|
+
className="header-cell date-header"
|
|
385
|
+
style={{
|
|
386
|
+
padding: '2px 1px',
|
|
387
|
+
fontWeight: 'bold',
|
|
388
|
+
border: '1px solid #dee2e6',
|
|
389
|
+
fontSize: '9px',
|
|
390
|
+
textAlign: 'center',
|
|
391
|
+
backgroundColor:
|
|
392
|
+
hoveredColumn === dateCol.date
|
|
393
|
+
? '#e1f0ff'
|
|
394
|
+
: '#e9ecef',
|
|
395
|
+
writingMode: 'horizontal-tb',
|
|
396
|
+
lineHeight: 1.1,
|
|
397
|
+
transition: 'background-color 0.2s ease',
|
|
398
|
+
display: 'flex',
|
|
399
|
+
flexDirection: 'column',
|
|
400
|
+
justifyContent: 'center',
|
|
401
|
+
}}
|
|
402
|
+
onMouseEnter={() =>
|
|
403
|
+
setHoveredColumn(dateCol.date)
|
|
404
|
+
}
|
|
405
|
+
>
|
|
406
|
+
<div
|
|
407
|
+
style={{
|
|
408
|
+
fontSize: '8px',
|
|
409
|
+
marginBottom: '1px',
|
|
410
|
+
}}
|
|
411
|
+
>
|
|
412
|
+
{moment(dateCol.date).format('ddd')}
|
|
413
|
+
</div>
|
|
414
|
+
<div style={{ fontSize: '9px' }}>
|
|
415
|
+
{dateCol.display}
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
))}
|
|
419
|
+
</div>
|
|
420
|
+
|
|
421
|
+
{/* Grid Body - Group by Facility */}
|
|
422
|
+
{Object.keys(facilitiesByCategory).sort((a, b) => {
|
|
423
|
+
// Custom sorting function for wharf facilities (Jetty first, then AMC 1-6)
|
|
424
|
+
// Jetty facilities first
|
|
425
|
+
const isAJetty = a.toLowerCase().includes('jetty');
|
|
426
|
+
const isBJetty = b.toLowerCase().includes('jetty');
|
|
427
|
+
|
|
428
|
+
if (isAJetty && !isBJetty) return -1;
|
|
429
|
+
if (!isAJetty && isBJetty) return 1;
|
|
430
|
+
|
|
431
|
+
// Both are jetties, sort alphabetically
|
|
432
|
+
if (isAJetty && isBJetty) {
|
|
433
|
+
return a.localeCompare(b);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// AMC facilities sorting (AMC 1-6)
|
|
437
|
+
const isAAMC = a.startsWith('AMC ');
|
|
438
|
+
const isBAMC = b.startsWith('AMC ');
|
|
439
|
+
|
|
440
|
+
if (isAAMC && !isBAMC) return -1;
|
|
441
|
+
if (!isAAMC && isBAMC) return 1;
|
|
442
|
+
|
|
443
|
+
// Both are AMC, sort numerically
|
|
444
|
+
if (isAAMC && isBAMC) {
|
|
445
|
+
const numA = parseInt(a.split(' ')[1]) || 0;
|
|
446
|
+
const numB = parseInt(b.split(' ')[1]) || 0;
|
|
447
|
+
return numA - numB;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Default alphabetical sort for other facilities
|
|
451
|
+
return a.localeCompare(b);
|
|
452
|
+
}).map((facility) => (
|
|
453
|
+
<div key={facility} className="facility-group">
|
|
454
|
+
{/* Facility Header - Only show if there are multiple facility categories */}
|
|
455
|
+
{Object.keys(facilitiesByCategory).length > 1 && (
|
|
456
|
+
<div
|
|
457
|
+
className="facility-header"
|
|
458
|
+
style={{
|
|
459
|
+
display: 'grid',
|
|
460
|
+
gridTemplateColumns: `200px repeat(${dateColumns.length}, 45px)`,
|
|
461
|
+
backgroundColor:
|
|
462
|
+
facilityColors[facility] || '#a8c8ec',
|
|
463
|
+
color: getContrastTextColor(
|
|
464
|
+
facilityColors[facility] || '#a8c8ec'
|
|
465
|
+
),
|
|
466
|
+
fontWeight: 'bold',
|
|
467
|
+
position: 'relative',
|
|
468
|
+
zIndex: 15,
|
|
469
|
+
}}
|
|
470
|
+
>
|
|
471
|
+
<div
|
|
472
|
+
style={{
|
|
473
|
+
padding: '3px 8px',
|
|
474
|
+
border: `1px solid ${facilityColors[facility] ? darkenColor(facilityColors[facility], 20) : '#0056b3'}`,
|
|
475
|
+
fontSize: '11px',
|
|
476
|
+
}}
|
|
477
|
+
>
|
|
478
|
+
{facility}
|
|
479
|
+
</div>
|
|
480
|
+
{dateColumns.map((dateCol) => (
|
|
481
|
+
<div
|
|
482
|
+
key={`${facility}-header-${dateCol.date}`}
|
|
483
|
+
style={{
|
|
484
|
+
border: `1px solid ${facilityColors[facility] ? darkenColor(facilityColors[facility], 20) : '#0056b3'}`,
|
|
485
|
+
}}
|
|
486
|
+
></div>
|
|
487
|
+
))}
|
|
488
|
+
</div>
|
|
489
|
+
)}
|
|
490
|
+
|
|
491
|
+
{/* Facility Rows */}
|
|
492
|
+
{gridData
|
|
493
|
+
.filter((row) => row.facility === facility)
|
|
494
|
+
.map((row, rowIndex) => (
|
|
495
|
+
<div
|
|
496
|
+
key={`${facility}-${row.id}`}
|
|
497
|
+
className="grid-row"
|
|
498
|
+
style={{
|
|
499
|
+
display: 'grid',
|
|
500
|
+
gridTemplateColumns: `200px repeat(${dateColumns.length}, 45px)`,
|
|
501
|
+
backgroundColor:
|
|
502
|
+
hoveredRow === row.id
|
|
503
|
+
? '#f0f8ff'
|
|
504
|
+
: rowIndex % 2 === 0
|
|
505
|
+
? '#fff'
|
|
506
|
+
: '#f8f9fa',
|
|
507
|
+
transition:
|
|
508
|
+
'background-color 0.2s ease',
|
|
509
|
+
}}
|
|
510
|
+
>
|
|
511
|
+
<div
|
|
512
|
+
className="resource-cell"
|
|
513
|
+
style={{
|
|
514
|
+
padding: '3px 8px',
|
|
515
|
+
border: '1px solid #dee2e6',
|
|
516
|
+
fontSize: '10px',
|
|
517
|
+
backgroundColor:
|
|
518
|
+
hoveredRow === row.id
|
|
519
|
+
? '#e1f0ff'
|
|
520
|
+
: '#fff',
|
|
521
|
+
fontWeight: '500',
|
|
522
|
+
lineHeight: 1.2,
|
|
523
|
+
transition:
|
|
524
|
+
'background-color 0.2s ease',
|
|
525
|
+
cursor: row.lead_id ? 'pointer' : 'default',
|
|
526
|
+
}}
|
|
527
|
+
onMouseEnter={() =>
|
|
528
|
+
setHoveredRow(row.id)
|
|
529
|
+
}
|
|
530
|
+
onClick={() => {
|
|
531
|
+
if (row.lead_id && onProjectClick) {
|
|
532
|
+
onProjectClick(row.lead_id);
|
|
533
|
+
}
|
|
534
|
+
}}
|
|
535
|
+
>
|
|
536
|
+
<span style={{
|
|
537
|
+
color: row.lead_id ? '#007bff' : 'inherit',
|
|
538
|
+
textDecoration: row.lead_id ? 'underline' : 'none'
|
|
539
|
+
}}>
|
|
540
|
+
{row.resourceName}
|
|
541
|
+
</span>
|
|
542
|
+
</div>
|
|
543
|
+
{dateColumns.map(
|
|
544
|
+
(dateCol, colIndex) => {
|
|
545
|
+
const cell =
|
|
546
|
+
row.cells[dateCol.date];
|
|
547
|
+
const prevCell =
|
|
548
|
+
colIndex > 0
|
|
549
|
+
? row.cells[
|
|
550
|
+
dateColumns[
|
|
551
|
+
colIndex - 1
|
|
552
|
+
].date
|
|
553
|
+
]
|
|
554
|
+
: null;
|
|
555
|
+
const nextCell =
|
|
556
|
+
colIndex <
|
|
557
|
+
dateColumns.length - 1
|
|
558
|
+
? row.cells[
|
|
559
|
+
dateColumns[
|
|
560
|
+
colIndex + 1
|
|
561
|
+
].date
|
|
562
|
+
]
|
|
563
|
+
: null;
|
|
564
|
+
|
|
565
|
+
// Check if this cell and adjacent cells have the same data/booking
|
|
566
|
+
const hasSamePrev =
|
|
567
|
+
prevCell &&
|
|
568
|
+
prevCell.hasData &&
|
|
569
|
+
cell.hasData &&
|
|
570
|
+
prevCell.data?.resource ===
|
|
571
|
+
cell.data?.resource &&
|
|
572
|
+
prevCell.data?.client ===
|
|
573
|
+
cell.data?.client;
|
|
574
|
+
const hasSameNext =
|
|
575
|
+
nextCell &&
|
|
576
|
+
nextCell.hasData &&
|
|
577
|
+
cell.hasData &&
|
|
578
|
+
nextCell.data?.resource ===
|
|
579
|
+
cell.data?.resource &&
|
|
580
|
+
nextCell.data?.client ===
|
|
581
|
+
cell.data?.client;
|
|
582
|
+
|
|
583
|
+
const isHovered =
|
|
584
|
+
hoveredColumn ===
|
|
585
|
+
dateCol.date &&
|
|
586
|
+
hoveredRow === row.id;
|
|
587
|
+
|
|
588
|
+
return (
|
|
589
|
+
<div
|
|
590
|
+
key={`${row.id}-${dateCol.date}`}
|
|
591
|
+
className="data-cell"
|
|
592
|
+
style={{
|
|
593
|
+
borderTop: isHovered
|
|
594
|
+
? '2px solid #007bff'
|
|
595
|
+
: cell.hasData
|
|
596
|
+
? '2px solid #333'
|
|
597
|
+
: '1px solid #dee2e6',
|
|
598
|
+
borderBottom:
|
|
599
|
+
isHovered
|
|
600
|
+
? '2px solid #007bff'
|
|
601
|
+
: cell.hasData
|
|
602
|
+
? '2px solid #333'
|
|
603
|
+
: '1px solid #dee2e6',
|
|
604
|
+
borderLeft:
|
|
605
|
+
isHovered
|
|
606
|
+
? '2px solid #007bff'
|
|
607
|
+
: hasSamePrev
|
|
608
|
+
? 'none'
|
|
609
|
+
: cell.hasData
|
|
610
|
+
? '2px solid #333'
|
|
611
|
+
: '1px solid #dee2e6',
|
|
612
|
+
borderRight:
|
|
613
|
+
isHovered
|
|
614
|
+
? '2px solid #007bff'
|
|
615
|
+
: hasSameNext
|
|
616
|
+
? 'none'
|
|
617
|
+
: cell.hasData
|
|
618
|
+
? '2px solid #333'
|
|
619
|
+
: '1px solid #dee2e6',
|
|
620
|
+
backgroundColor:
|
|
621
|
+
isHovered
|
|
622
|
+
? cell.hasData
|
|
623
|
+
? getCellColor(cell.data?.confirmed, true)
|
|
624
|
+
: '#e6f3ff'
|
|
625
|
+
: cell.hasData
|
|
626
|
+
? getCellColor(cell.data?.confirmed, false)
|
|
627
|
+
: '#fff',
|
|
628
|
+
minHeight: '22px',
|
|
629
|
+
cursor: cell.hasData
|
|
630
|
+
? 'pointer'
|
|
631
|
+
: 'default',
|
|
632
|
+
opacity: 1,
|
|
633
|
+
position:
|
|
634
|
+
'relative',
|
|
635
|
+
boxShadow: isHovered
|
|
636
|
+
? '0 0 6px rgba(0, 123, 255, 0.4)'
|
|
637
|
+
: cell.hasData
|
|
638
|
+
? '0 2px 4px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.2), inset 0 -1px 0 rgba(0, 0, 0, 0.1)'
|
|
639
|
+
: 'none',
|
|
640
|
+
zIndex: isHovered
|
|
641
|
+
? 5
|
|
642
|
+
: cell.hasData
|
|
643
|
+
? 2
|
|
644
|
+
: 1,
|
|
645
|
+
transition:
|
|
646
|
+
'all 0.15s ease',
|
|
647
|
+
}}
|
|
648
|
+
onMouseEnter={(e) => {
|
|
649
|
+
setHoveredRow(
|
|
650
|
+
row.id
|
|
651
|
+
);
|
|
652
|
+
setHoveredColumn(
|
|
653
|
+
dateCol.date
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
if (
|
|
657
|
+
cell.hasData &&
|
|
658
|
+
cell.data
|
|
659
|
+
) {
|
|
660
|
+
const dateRange =
|
|
661
|
+
getDateRangeForCell(
|
|
662
|
+
row,
|
|
663
|
+
dateCol.date
|
|
664
|
+
);
|
|
665
|
+
const tooltipData =
|
|
666
|
+
{
|
|
667
|
+
...cell.data,
|
|
668
|
+
dateRange:
|
|
669
|
+
dateRange
|
|
670
|
+
? `${moment(dateRange.startDate).format('DD/MM/YYYY')} - ${moment(dateRange.endDate).format('DD/MM/YYYY')}`
|
|
671
|
+
: null,
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
setTooltip({
|
|
675
|
+
visible: true,
|
|
676
|
+
data: tooltipData,
|
|
677
|
+
position: {
|
|
678
|
+
x: e.clientX,
|
|
679
|
+
y: e.clientY,
|
|
680
|
+
},
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}}
|
|
684
|
+
onMouseLeave={() => {
|
|
685
|
+
// Don't clear hover states here - let the container handle it
|
|
686
|
+
}}
|
|
687
|
+
onMouseMove={(e) => {
|
|
688
|
+
if (
|
|
689
|
+
cell.hasData &&
|
|
690
|
+
tooltip.visible
|
|
691
|
+
) {
|
|
692
|
+
setTooltip(
|
|
693
|
+
(prev) => ({
|
|
694
|
+
...prev,
|
|
695
|
+
position:
|
|
696
|
+
{
|
|
697
|
+
x: e.clientX,
|
|
698
|
+
y: e.clientY,
|
|
699
|
+
},
|
|
700
|
+
})
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
}}
|
|
704
|
+
></div>
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
)}
|
|
708
|
+
</div>
|
|
709
|
+
))}
|
|
710
|
+
</div>
|
|
711
|
+
))}
|
|
712
|
+
</div>
|
|
713
|
+
</div>
|
|
714
|
+
|
|
715
|
+
{/* Color Legend */}
|
|
716
|
+
<div
|
|
717
|
+
className="color-legend"
|
|
718
|
+
style={{
|
|
719
|
+
display: 'flex',
|
|
720
|
+
alignItems: 'center',
|
|
721
|
+
justifyContent: 'center',
|
|
722
|
+
marginTop: '15px',
|
|
723
|
+
padding: '10px',
|
|
724
|
+
backgroundColor: '#f8f9fa',
|
|
725
|
+
borderRadius: '6px',
|
|
726
|
+
fontSize: '12px',
|
|
727
|
+
fontWeight: '500',
|
|
728
|
+
gap: '20px',
|
|
729
|
+
}}
|
|
730
|
+
>
|
|
731
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
|
732
|
+
<div
|
|
733
|
+
style={{
|
|
734
|
+
width: '16px',
|
|
735
|
+
height: '16px',
|
|
736
|
+
backgroundColor: getCellColor(true),
|
|
737
|
+
borderRadius: '3px',
|
|
738
|
+
border: '1px solid #dee2e6',
|
|
739
|
+
}}
|
|
740
|
+
></div>
|
|
741
|
+
<span>Confirmed</span>
|
|
742
|
+
</div>
|
|
743
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
|
744
|
+
<div
|
|
745
|
+
style={{
|
|
746
|
+
width: '16px',
|
|
747
|
+
height: '16px',
|
|
748
|
+
backgroundColor: getCellColor(false),
|
|
749
|
+
borderRadius: '3px',
|
|
750
|
+
border: '1px solid #dee2e6',
|
|
751
|
+
}}
|
|
752
|
+
></div>
|
|
753
|
+
<span>Unconfirmed</span>
|
|
754
|
+
</div>
|
|
755
|
+
</div>
|
|
756
|
+
|
|
757
|
+
{/* Visual indicator that shows there's more content to scroll */}
|
|
758
|
+
{showScrollHint && (
|
|
759
|
+
<div
|
|
760
|
+
className="scroll-hint"
|
|
761
|
+
style={{
|
|
762
|
+
textAlign: 'center',
|
|
763
|
+
marginTop: '10px',
|
|
764
|
+
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
|
765
|
+
color: '#666',
|
|
766
|
+
padding: '8px 12px',
|
|
767
|
+
borderRadius: '6px',
|
|
768
|
+
fontSize: '12px',
|
|
769
|
+
fontWeight: '500',
|
|
770
|
+
transition: 'opacity 0.5s ease',
|
|
771
|
+
opacity: 0.8,
|
|
772
|
+
whiteSpace: 'nowrap',
|
|
773
|
+
}}
|
|
774
|
+
>
|
|
775
|
+
Use arrow keys or scroll buttons to navigate horizontally
|
|
776
|
+
</div>
|
|
777
|
+
)}
|
|
778
|
+
|
|
779
|
+
{/* Tooltip */}
|
|
780
|
+
<GridTooltip
|
|
781
|
+
data={tooltip.data}
|
|
782
|
+
visible={tooltip.visible}
|
|
783
|
+
position={tooltip.position}
|
|
784
|
+
/>
|
|
785
|
+
</div>
|
|
786
|
+
);
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
export default ExcelStyleGrid;
|