@visns-studio/visns-components 5.15.26 → 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.
@@ -9,10 +9,12 @@ import parse from 'html-react-parser';
9
9
  import { createSwapy } from 'swapy';
10
10
  import { Minus, Plus, Star, Trash2, Eye, RotateCcw, Clock, Database, BarChart3, AlertCircle } from 'lucide-react';
11
11
  import { confirmDialog } from '../utils/ConfirmDialog';
12
+ import moment from 'moment';
12
13
 
13
14
  import CustomFetch from '../Fetch';
14
15
  import MultiSelect from '../MultiSelect';
15
16
  import StandardModal from './StandardModal';
17
+ import TimelineWidget from './TimelineWidget';
16
18
 
17
19
  import styles from '../styles/GenericDashboard.module.scss';
18
20
 
@@ -1506,7 +1508,11 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
1506
1508
  return (
1507
1509
  <div
1508
1510
  className={styles['table-container']}
1509
- style={{ maxWidth: getTableMaxWidth() }}
1511
+ style={{
1512
+ maxWidth: getTableMaxWidth(),
1513
+ maxHeight: widget.height || 'none',
1514
+ overflowY: widget.height ? 'auto' : 'visible'
1515
+ }}
1510
1516
  >
1511
1517
  {tableData.length > 0 ? (
1512
1518
  <div className={styles['table-scroll']}>
@@ -1796,6 +1802,8 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
1796
1802
  </div>
1797
1803
  );
1798
1804
  }
1805
+ case 'timeline':
1806
+ return <TimelineWidget widgetData={widgetData} widget={widget} />;
1799
1807
  default:
1800
1808
  return null;
1801
1809
  }
@@ -0,0 +1,61 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import moment from 'moment';
4
+ import ExcelStyleGrid from './ExcelStyleGrid';
5
+ import { useExcelGrid } from '../hooks/useExcelGrid';
6
+ import styles from '../styles/GenericDashboard.module.scss';
7
+
8
+ const TimelineWidget = ({ widgetData, widget }) => {
9
+ const { dateColumns, facilitiesByCategory, gridData, facilityColors, processDataForLeadCentricGrid, processDataForGrid } = useExcelGrid();
10
+ const [showScrollHint, setShowScrollHint] = useState(true);
11
+ const navigate = useNavigate();
12
+
13
+ const handleProjectClick = (leadId) => {
14
+ if (leadId) {
15
+ navigate(`/leads/${leadId}`);
16
+ }
17
+ };
18
+
19
+ // Process the timeline data when widget data changes
20
+ useEffect(() => {
21
+ if (widgetData && widgetData.length > 0) {
22
+ const startDate = widget.startDate || moment().startOf('month').toDate();
23
+ const endDate = widget.endDate || moment().add(5, 'months').endOf('month').toDate();
24
+
25
+ // Use grouping type from widget config: 'lead' for lead-centric, 'facility' for facility-centric
26
+ const groupingType = widget.groupingType || 'lead';
27
+
28
+ if (groupingType === 'facility') {
29
+ processDataForGrid(widgetData, startDate, endDate);
30
+ } else {
31
+ processDataForLeadCentricGrid(widgetData, startDate, endDate);
32
+ }
33
+ }
34
+ }, [widgetData, widget.startDate, widget.endDate, widget.groupingType, processDataForLeadCentricGrid, processDataForGrid]);
35
+
36
+ if (!widgetData || widgetData.length === 0) {
37
+ return (
38
+ <div className={styles.noData}>
39
+ No timeline data available
40
+ </div>
41
+ );
42
+ }
43
+
44
+ return (
45
+ <div className={styles['timeline-container']} style={{ width: '100%', maxWidth: '100%', overflow: 'hidden' }}>
46
+ <ExcelStyleGrid
47
+ gridData={gridData}
48
+ dateColumns={dateColumns}
49
+ facilitiesByCategory={facilitiesByCategory}
50
+ facilityColors={facilityColors}
51
+ showScrollHint={showScrollHint}
52
+ setShowScrollHint={setShowScrollHint}
53
+ maxWidth="100%"
54
+ maxHeight={widget.height || '500px'}
55
+ onProjectClick={handleProjectClick}
56
+ />
57
+ </div>
58
+ );
59
+ };
60
+
61
+ export default TimelineWidget;
@@ -0,0 +1,366 @@
1
+ import { useState } from 'react';
2
+ import moment from 'moment';
3
+
4
+ export const useExcelGrid = () => {
5
+ const [dateColumns, setDateColumns] = useState([]);
6
+ const [facilitiesByCategory, setFacilitiesByCategory] = useState({});
7
+ const [gridData, setGridData] = useState([]);
8
+ const [facilityColors, setFacilityColors] = useState({});
9
+
10
+ // Generate date columns for the grid
11
+ const generateDateColumns = (startDate, endDate) => {
12
+ const dates = [];
13
+ const current = moment(startDate).utcOffset('+08:00');
14
+ const end = moment(endDate).utcOffset('+08:00');
15
+
16
+ while (current.isSameOrBefore(end, 'day')) {
17
+ dates.push({
18
+ date: current.format('YYYY-MM-DD'),
19
+ display: current.format('DD/MM'),
20
+ fullDate: current.clone()
21
+ });
22
+ current.add(1, 'day');
23
+ }
24
+
25
+ return dates;
26
+ };
27
+
28
+ // Generate colors for facility categories
29
+ const generateFacilityColors = (facilities) => {
30
+ const colors = [
31
+ '#a8c8ec', // Pastel Blue
32
+ '#b8e6b8', // Pastel Green
33
+ '#f8b4b4', // Pastel Red
34
+ '#fce68a', // Pastel Yellow
35
+ '#d1c4e9', // Pastel Purple
36
+ '#ffcc9c', // Pastel Orange
37
+ '#9fdfcc', // Pastel Teal
38
+ '#f4c2c2', // Pastel Pink
39
+ '#c5c5c5', // Light Gray
40
+ '#b3e0e0', // Pastel Cyan
41
+ ];
42
+
43
+ const facilityColors = {};
44
+ const facilityKeys = Object.keys(facilities);
45
+
46
+ facilityKeys.forEach((facility, index) => {
47
+ facilityColors[facility] = colors[index % colors.length];
48
+ });
49
+
50
+ return facilityColors;
51
+ };
52
+
53
+ // Group facilities by category
54
+ const groupFacilitiesByCategory = (data) => {
55
+ const grouped = {};
56
+
57
+ data.forEach(item => {
58
+ const facility = item.facility || 'Uncategorized';
59
+ if (!grouped[facility]) {
60
+ grouped[facility] = [];
61
+ }
62
+ grouped[facility].push(item);
63
+ });
64
+
65
+ return grouped;
66
+ };
67
+
68
+ // Convert segmented data to grid format
69
+ const convertToGridData = (segmentedData, dateColumns, facilityColors) => {
70
+ const gridRows = [];
71
+ const facilityGroups = groupFacilitiesByCategory(segmentedData);
72
+
73
+ Object.keys(facilityGroups).forEach(facility => {
74
+ const facilityItems = facilityGroups[facility];
75
+
76
+ facilityItems.forEach(item => {
77
+ const row = {
78
+ id: item.id,
79
+ facility: facility,
80
+ resourceName: item.id,
81
+ client: item.client,
82
+ project: item.project,
83
+ lead_id: item.lead_id,
84
+ status: item.status,
85
+ confirmed: item.confirmed,
86
+ cells: {}
87
+ };
88
+
89
+ // Initialize all cells as empty
90
+ dateColumns.forEach(dateCol => {
91
+ row.cells[dateCol.date] = {
92
+ hasData: false,
93
+ color: '#f8f9fa',
94
+ data: null
95
+ };
96
+ });
97
+
98
+ // Fill in data for occupied dates
99
+ if (item.data && item.data.length >= 2) {
100
+ const startDate = moment(item.data[0].x).utcOffset('+08:00');
101
+ const endDate = moment(item.data[1].x).utcOffset('+08:00');
102
+
103
+ const current = startDate.clone();
104
+ while (current.isSameOrBefore(endDate, 'day')) {
105
+ const dateKey = current.format('YYYY-MM-DD');
106
+ if (row.cells[dateKey]) {
107
+ row.cells[dateKey] = {
108
+ hasData: true,
109
+ color: item.data[0].color || item.color || '#007bff',
110
+ facility: facility,
111
+ data: {
112
+ resource: item.id,
113
+ client: item.client,
114
+ project: item.project,
115
+ status: item.status,
116
+ confirmed: item.confirmed,
117
+ date: current.format('DD-MM-YYYY')
118
+ }
119
+ };
120
+ }
121
+ current.add(1, 'day');
122
+ }
123
+ }
124
+
125
+ gridRows.push(row);
126
+ });
127
+ });
128
+
129
+ return gridRows;
130
+ };
131
+
132
+ // Transform data for lead-centric view (floating dock)
133
+ const transformDataForLeadCentricView = (segmentedData) => {
134
+ const leadGroups = {};
135
+ const allFacilities = new Set();
136
+
137
+ // Group by lead and collect all facilities
138
+ segmentedData.forEach(item => {
139
+ const leadKey = `${item.client} - ${item.project || item.id.split(' (')[0]}`;
140
+
141
+ if (!leadGroups[leadKey]) {
142
+ leadGroups[leadKey] = {
143
+ id: leadKey,
144
+ client: item.client,
145
+ project: item.project || item.id.split(' (')[0],
146
+ lead_id: item.lead_id,
147
+ status: item.status,
148
+ confirmed: item.confirmed,
149
+ facilities: {}
150
+ };
151
+ }
152
+
153
+ // Extract facility from the item
154
+ const facility = item.facility || item.id.match(/\(([^)]+)\)$/)?.[1] || 'Unknown';
155
+ allFacilities.add(facility);
156
+
157
+ // Store facility booking data
158
+ if (!leadGroups[leadKey].facilities[facility]) {
159
+ leadGroups[leadKey].facilities[facility] = [];
160
+ }
161
+
162
+ leadGroups[leadKey].facilities[facility].push({
163
+ data: item.data,
164
+ confirmed: item.confirmed
165
+ });
166
+ });
167
+
168
+ // Custom sorting function for wharf facilities (Jetty first, then AMC 1-6)
169
+ const sortWharfFacilities = (facilities) => {
170
+ return facilities.sort((a, b) => {
171
+ // Jetty facilities first
172
+ const isAJetty = a.toLowerCase().includes('jetty');
173
+ const isBJetty = b.toLowerCase().includes('jetty');
174
+
175
+ if (isAJetty && !isBJetty) return -1;
176
+ if (!isAJetty && isBJetty) return 1;
177
+
178
+ // Both are jetties, sort alphabetically
179
+ if (isAJetty && isBJetty) {
180
+ return a.localeCompare(b);
181
+ }
182
+
183
+ // AMC facilities sorting (AMC 1-6)
184
+ const isAAMC = a.startsWith('AMC ');
185
+ const isBAMC = b.startsWith('AMC ');
186
+
187
+ if (isAAMC && !isBAMC) return -1;
188
+ if (!isAAMC && isBAMC) return 1;
189
+
190
+ // Both are AMC, sort numerically
191
+ if (isAAMC && isBAMC) {
192
+ const numA = parseInt(a.split(' ')[1]) || 0;
193
+ const numB = parseInt(b.split(' ')[1]) || 0;
194
+ return numA - numB;
195
+ }
196
+
197
+ // Default alphabetical sort for other facilities
198
+ return a.localeCompare(b);
199
+ });
200
+ };
201
+
202
+ return {
203
+ leadGroups: Object.values(leadGroups),
204
+ facilities: sortWharfFacilities(Array.from(allFacilities))
205
+ };
206
+ };
207
+
208
+ // Convert lead-centric data to grid format
209
+ const convertLeadCentricToGridData = (leadGroups, facilities, dateColumns) => {
210
+ const gridRows = [];
211
+
212
+ leadGroups.forEach(lead => {
213
+ const row = {
214
+ id: lead.id,
215
+ facility: 'Lead', // All leads are grouped under "Lead" category
216
+ resourceName: lead.id,
217
+ client: lead.client,
218
+ project: lead.project,
219
+ lead_id: lead.lead_id,
220
+ status: lead.status,
221
+ confirmed: lead.confirmed,
222
+ cells: {}
223
+ };
224
+
225
+ // Initialize all cells as empty
226
+ dateColumns.forEach(dateCol => {
227
+ row.cells[dateCol.date] = {
228
+ hasData: false,
229
+ color: '#f8f9fa',
230
+ data: null
231
+ };
232
+ });
233
+
234
+ // Fill in facility bookings for each date
235
+ facilities.forEach(facility => {
236
+ if (lead.facilities[facility]) {
237
+ lead.facilities[facility].forEach(booking => {
238
+ if (booking.data && booking.data.length >= 2) {
239
+ const startDate = moment(booking.data[0].x).utcOffset('+08:00');
240
+ const endDate = moment(booking.data[1].x).utcOffset('+08:00');
241
+
242
+ const current = startDate.clone();
243
+ while (current.isSameOrBefore(endDate, 'day')) {
244
+ const dateKey = current.format('YYYY-MM-DD');
245
+ if (row.cells[dateKey]) {
246
+ // If cell already has data, we need to combine facilities
247
+ if (row.cells[dateKey].hasData) {
248
+ // Combine multiple facilities for the same date
249
+ const existingResource = row.cells[dateKey].data.resource;
250
+ const combinedResource = existingResource.includes(facility)
251
+ ? existingResource
252
+ : `${existingResource}, ${facility}`;
253
+
254
+ row.cells[dateKey].data.resource = combinedResource;
255
+ // Use the highest confirmation status
256
+ row.cells[dateKey].data.confirmed = Math.max(
257
+ row.cells[dateKey].data.confirmed || 0,
258
+ booking.confirmed || 0
259
+ );
260
+ } else {
261
+ // First facility for this date
262
+ row.cells[dateKey] = {
263
+ hasData: true,
264
+ color: '#007bff',
265
+ facility: facility,
266
+ data: {
267
+ resource: facility, // Now facility is the resource
268
+ client: lead.client,
269
+ project: lead.project,
270
+ status: lead.status,
271
+ confirmed: booking.confirmed,
272
+ date: current.format('DD-MM-YYYY')
273
+ }
274
+ };
275
+ }
276
+ }
277
+ current.add(1, 'day');
278
+ }
279
+ }
280
+ });
281
+ }
282
+ });
283
+
284
+ gridRows.push(row);
285
+ });
286
+
287
+ return gridRows;
288
+ };
289
+
290
+ // Process data for grid (original facility-centric)
291
+ const processDataForGrid = (segmentedData, startDate, endDate) => {
292
+ // Generate date columns for the grid
293
+ const dates = generateDateColumns(startDate, endDate);
294
+ setDateColumns(dates);
295
+
296
+ // Group facilities
297
+ const grouped = groupFacilitiesByCategory(segmentedData);
298
+ setFacilitiesByCategory(grouped);
299
+
300
+ // Generate colors for facilities
301
+ const colors = generateFacilityColors(grouped);
302
+ setFacilityColors(colors);
303
+
304
+ // Convert to grid data
305
+ const grid = convertToGridData(segmentedData, dates, colors);
306
+ setGridData(grid);
307
+
308
+ return {
309
+ dateColumns: dates,
310
+ facilitiesByCategory: grouped,
311
+ facilityColors: colors,
312
+ gridData: grid
313
+ };
314
+ };
315
+
316
+ // Process data for lead-centric grid (floating dock)
317
+ const processDataForLeadCentricGrid = (segmentedData, startDate, endDate) => {
318
+ // Generate date columns for the grid
319
+ const dates = generateDateColumns(startDate, endDate);
320
+ setDateColumns(dates);
321
+
322
+ // Transform data to lead-centric structure
323
+ const { leadGroups, facilities } = transformDataForLeadCentricView(segmentedData);
324
+
325
+ // Create grouped structure with single "Lead" category
326
+ const grouped = { 'Lead': leadGroups };
327
+ setFacilitiesByCategory(grouped);
328
+
329
+ // Generate colors (single color for leads)
330
+ const colors = { 'Lead': '#a8c8ec' };
331
+ setFacilityColors(colors);
332
+
333
+ // Convert to grid data
334
+ const grid = convertLeadCentricToGridData(leadGroups, facilities, dates);
335
+ setGridData(grid);
336
+
337
+ return {
338
+ dateColumns: dates,
339
+ facilitiesByCategory: grouped,
340
+ facilityColors: colors,
341
+ gridData: grid,
342
+ facilities: facilities
343
+ };
344
+ };
345
+
346
+ return {
347
+ dateColumns,
348
+ facilitiesByCategory,
349
+ gridData,
350
+ facilityColors,
351
+ setDateColumns,
352
+ setFacilitiesByCategory,
353
+ setGridData,
354
+ setFacilityColors,
355
+ generateDateColumns,
356
+ generateFacilityColors,
357
+ groupFacilitiesByCategory,
358
+ convertToGridData,
359
+ processDataForGrid,
360
+ processDataForLeadCentricGrid,
361
+ transformDataForLeadCentricView,
362
+ convertLeadCentricToGridData
363
+ };
364
+ };
365
+
366
+ export default useExcelGrid;
@@ -0,0 +1,251 @@
1
+ .scroll-indicators {
2
+ position: absolute;
3
+ width: 100%;
4
+ height: 100%;
5
+ pointer-events: none;
6
+ z-index: 10;
7
+ }
8
+
9
+ .scroll-left,
10
+ .scroll-right {
11
+ position: absolute;
12
+ top: 50%;
13
+ transform: translateY(-50%);
14
+ pointer-events: auto;
15
+ transition: opacity 0.3s ease;
16
+ }
17
+
18
+ .scroll-left {
19
+ left: 10px;
20
+ }
21
+
22
+ .scroll-right {
23
+ right: 10px;
24
+ }
25
+
26
+ .scroll-left button,
27
+ .scroll-right button {
28
+ background-color: rgba(0, 123, 255, 0.8);
29
+ color: white;
30
+ border: 1px solid #0056b3;
31
+ border-radius: 50%;
32
+ width: 40px;
33
+ height: 40px;
34
+ font-size: 18px;
35
+ cursor: pointer;
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
40
+ transition: all 0.2s ease;
41
+ }
42
+
43
+ .scroll-left button:hover,
44
+ .scroll-right button:hover {
45
+ background-color: rgba(0, 86, 179, 0.9);
46
+ transform: scale(1.1);
47
+ }
48
+
49
+ .scroll-left button:focus,
50
+ .scroll-right button:focus {
51
+ outline: none;
52
+ box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.3);
53
+ }
54
+
55
+ /* Custom scrollbar styles for webkit browsers */
56
+ #chart-scroll-container::-webkit-scrollbar,
57
+ .dock-usage-grid::-webkit-scrollbar {
58
+ height: 12px;
59
+ width: 12px;
60
+ }
61
+
62
+ #chart-scroll-container::-webkit-scrollbar-track,
63
+ .dock-usage-grid::-webkit-scrollbar-track {
64
+ background: #f1f1f1;
65
+ border-radius: 6px;
66
+ border: 1px solid #ddd;
67
+ }
68
+
69
+ #chart-scroll-container::-webkit-scrollbar-thumb,
70
+ .dock-usage-grid::-webkit-scrollbar-thumb {
71
+ background: #0077cc;
72
+ border-radius: 6px;
73
+ border: 2px solid #f1f1f1;
74
+ }
75
+
76
+ #chart-scroll-container::-webkit-scrollbar-thumb:hover,
77
+ .dock-usage-grid::-webkit-scrollbar-thumb:hover {
78
+ background: #005fa3;
79
+ }
80
+
81
+ .dock-usage-grid::-webkit-scrollbar-corner {
82
+ background: #f1f1f1;
83
+ }
84
+
85
+ /* Scroll indicator animation */
86
+ @keyframes pulseRight {
87
+ 0% {
88
+ transform: translateX(0);
89
+ }
90
+ 50% {
91
+ transform: translateX(5px);
92
+ }
93
+ 100% {
94
+ transform: translateX(0);
95
+ }
96
+ }
97
+
98
+ .scroll-right button {
99
+ animation: pulseRight 1.5s infinite;
100
+ }
101
+
102
+ /* Hide animation when user has scrolled */
103
+ .scroll-right.has-scrolled button {
104
+ animation: none;
105
+ }
106
+
107
+ /* Fade in/out effect for scroll buttons */
108
+ .scroll-left,
109
+ .scroll-right {
110
+ opacity: 0.7;
111
+ }
112
+
113
+ .scroll-left:hover,
114
+ .scroll-right:hover {
115
+ opacity: 1;
116
+ }
117
+
118
+ /* Excel-style Grid Styles */
119
+ .dock-usage-grid {
120
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
121
+ font-size: 10px;
122
+ border-radius: 4px;
123
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
124
+ }
125
+
126
+ .dock-usage-grid:focus {
127
+ outline: 2px solid #007bff;
128
+ outline-offset: 2px;
129
+ }
130
+
131
+ .grid-container {
132
+ position: relative;
133
+ }
134
+
135
+ .grid-header {
136
+ position: sticky;
137
+ top: 0;
138
+ z-index: 100;
139
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
140
+ }
141
+
142
+ .header-cell {
143
+ user-select: none;
144
+ white-space: nowrap;
145
+ overflow: hidden;
146
+ text-overflow: ellipsis;
147
+ }
148
+
149
+ .resource-header {
150
+ position: sticky;
151
+ left: 0;
152
+ z-index: 101;
153
+ box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
154
+ }
155
+
156
+ .date-header {
157
+ writing-mode: horizontal-tb;
158
+ height: 24px;
159
+ display: flex;
160
+ align-items: center;
161
+ justify-content: center;
162
+ line-height: 1;
163
+ }
164
+
165
+ .facility-group {
166
+ border-bottom: 2px solid #007bff;
167
+ }
168
+
169
+ .facility-header {
170
+ position: sticky;
171
+ z-index: 50;
172
+ }
173
+
174
+ .facility-header > div:first-child {
175
+ position: sticky;
176
+ left: 0;
177
+ z-index: 51;
178
+ box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
179
+ }
180
+
181
+ .grid-row {
182
+ transition: background-color 0.2s ease;
183
+ }
184
+
185
+ .grid-row:hover {
186
+ background-color: #e3f2fd !important;
187
+ }
188
+
189
+ .grid-row:hover .resource-cell {
190
+ background-color: #bbdefb !important;
191
+ }
192
+
193
+ .resource-cell {
194
+ position: sticky;
195
+ left: 0;
196
+ z-index: 10;
197
+ box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05);
198
+ white-space: nowrap;
199
+ overflow: hidden;
200
+ text-overflow: ellipsis;
201
+ transition: background-color 0.2s ease;
202
+ }
203
+
204
+ .data-cell {
205
+ transition: all 0.2s ease;
206
+ position: relative;
207
+ }
208
+
209
+ .data-cell:hover {
210
+ transform: scale(1.05);
211
+ z-index: 5;
212
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
213
+ border-color: #007bff !important;
214
+ }
215
+
216
+ /* Responsive design */
217
+ @media (max-width: 768px) {
218
+ .dock-usage-grid {
219
+ font-size: 9px;
220
+ }
221
+
222
+ .header-cell,
223
+ .resource-cell,
224
+ .data-cell {
225
+ padding: 2px 4px;
226
+ }
227
+
228
+ .date-header {
229
+ height: 20px;
230
+ font-size: 8px;
231
+ }
232
+ }
233
+
234
+ @media (max-width: 480px) {
235
+ .dock-usage-grid {
236
+ font-size: 8px;
237
+ }
238
+
239
+ .grid-container {
240
+ min-width: 500px;
241
+ }
242
+
243
+ .resource-header {
244
+ width: 150px;
245
+ }
246
+
247
+ .date-header {
248
+ height: 18px;
249
+ font-size: 7px;
250
+ }
251
+ }