@punch-in/strapi-admin 1.2.4 → 1.2.6
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/admin/src/components/LeftMenu/LeftMenuLink/LeftMenuIcon.js +1 -1
- package/admin/src/components/LeftMenu/LeftMenuLink/LeftMenuLinkContent.js +1 -1
- package/admin/src/containers/HomePage/PunchInToday.js +298 -0
- package/admin/src/containers/HomePage/components.js +158 -1
- package/admin/src/containers/HomePage/index.js +6 -0
- package/admin/src/translations/en.json +7 -0
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
|
5
5
|
|
|
6
6
|
const FaIcon = styled(({ small, ...props }) => <FontAwesomeIcon {...props} />)`
|
|
7
7
|
position: absolute;
|
|
8
|
-
top: ${({ small }) => (small ? 'calc(50% - 0.3rem)' : 'calc(50% - 0.
|
|
8
|
+
top: ${({ small }) => (small ? 'calc(50% - 0.3rem)' : 'calc(50% - 0.95rem)')};
|
|
9
9
|
left: ${({ small }) => (small ? '2.2rem' : '1.6rem')};
|
|
10
10
|
width: ${({ small }) => (small ? 'auto' : '1.9rem')} !important;
|
|
11
11
|
font-size: ${({ small }) => (small ? '.5rem' : '1.9rem')};
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { FormattedMessage } from 'react-intl';
|
|
3
|
+
import { request } from 'strapi-helper-plugin';
|
|
4
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
5
|
+
import {
|
|
6
|
+
faMapMarkerAlt,
|
|
7
|
+
faChevronLeft,
|
|
8
|
+
faChevronRight,
|
|
9
|
+
faSignInAlt,
|
|
10
|
+
faSignOutAlt,
|
|
11
|
+
faThumbsUp,
|
|
12
|
+
faCheckCircle,
|
|
13
|
+
faClock,
|
|
14
|
+
} from '@fortawesome/free-solid-svg-icons';
|
|
15
|
+
import moment from 'moment';
|
|
16
|
+
import { Block, SiteStrip, SiteTab, SiteNav, SiteNavBtn, PunchList, PunchRow, PunchMeta, PunchName, PunchTimes, PunchAction, EmptyPunch, StatusPill, ApproveBtn } from './components';
|
|
17
|
+
|
|
18
|
+
const NONE_SITE = 'none';
|
|
19
|
+
|
|
20
|
+
const startOfTodayIso = () => {
|
|
21
|
+
const d = new Date();
|
|
22
|
+
d.setHours(0, 0, 0, 0);
|
|
23
|
+
return d.toISOString();
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const displayName = (user) => {
|
|
27
|
+
if (!user) {
|
|
28
|
+
return 'Unknown employee';
|
|
29
|
+
}
|
|
30
|
+
const last = user.lastName || '';
|
|
31
|
+
const first = user.firstName || '';
|
|
32
|
+
if (last && first) {
|
|
33
|
+
return last + ', ' + first;
|
|
34
|
+
}
|
|
35
|
+
return last || first || user.email || user.username || 'Employee';
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const clockLabel = (value) => {
|
|
39
|
+
if (!value) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const parsed = moment(value);
|
|
43
|
+
return parsed.isValid() ? parsed.format('HH:mm') : null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const siteIdOf = (site) => {
|
|
47
|
+
if (!site) {
|
|
48
|
+
return NONE_SITE;
|
|
49
|
+
}
|
|
50
|
+
if (typeof site === 'object') {
|
|
51
|
+
return site.id != null ? String(site.id) : NONE_SITE;
|
|
52
|
+
}
|
|
53
|
+
return String(site);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const siteNameOf = (site) => {
|
|
57
|
+
if (site && typeof site === 'object' && site.name) {
|
|
58
|
+
return site.name;
|
|
59
|
+
}
|
|
60
|
+
return 'No site';
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const PunchInToday = () => {
|
|
64
|
+
const stripRef = useRef(null);
|
|
65
|
+
const [loading, setLoading] = useState(true);
|
|
66
|
+
const [error, setError] = useState(null);
|
|
67
|
+
const [sheets, setSheets] = useState([]);
|
|
68
|
+
const [sites, setSites] = useState([]);
|
|
69
|
+
const [selectedSite, setSelectedSite] = useState(null);
|
|
70
|
+
const [approvingId, setApprovingId] = useState(null);
|
|
71
|
+
|
|
72
|
+
const load = useCallback(async () => {
|
|
73
|
+
setLoading(true);
|
|
74
|
+
setError(null);
|
|
75
|
+
try {
|
|
76
|
+
const from = startOfTodayIso();
|
|
77
|
+
const [siteList, sheetList] = await Promise.all([
|
|
78
|
+
request('/sites?_limit=-1', { method: 'GET' }),
|
|
79
|
+
request('/time-sheets?from_gte=' + encodeURIComponent(from) + '&_limit=-1', { method: 'GET' }),
|
|
80
|
+
]);
|
|
81
|
+
setSites(Array.isArray(siteList) ? siteList : []);
|
|
82
|
+
setSheets(Array.isArray(sheetList) ? sheetList : []);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
setError('Could not load today\'s punch-ins.');
|
|
85
|
+
setSites([]);
|
|
86
|
+
setSheets([]);
|
|
87
|
+
} finally {
|
|
88
|
+
setLoading(false);
|
|
89
|
+
}
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
load();
|
|
94
|
+
}, [load]);
|
|
95
|
+
|
|
96
|
+
const grouped = useMemo(() => {
|
|
97
|
+
const map = {};
|
|
98
|
+
(sites || []).forEach((site) => {
|
|
99
|
+
if (!site || site.id == null) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const id = String(site.id);
|
|
103
|
+
map[id] = { id: id, name: site.name || 'Unnamed site', rows: [] };
|
|
104
|
+
});
|
|
105
|
+
(sheets || []).forEach((sheet) => {
|
|
106
|
+
if (!sheet || !sheet.from) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const id = siteIdOf(sheet.site);
|
|
110
|
+
if (!map[id]) {
|
|
111
|
+
map[id] = { id: id, name: siteNameOf(sheet.site), rows: [] };
|
|
112
|
+
}
|
|
113
|
+
map[id].rows.push(sheet);
|
|
114
|
+
});
|
|
115
|
+
const list = Object.keys(map)
|
|
116
|
+
.map((key) => map[key])
|
|
117
|
+
.sort((a, b) => {
|
|
118
|
+
if (a.id === NONE_SITE) {
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
if (b.id === NONE_SITE) {
|
|
122
|
+
return -1;
|
|
123
|
+
}
|
|
124
|
+
return String(a.name).localeCompare(String(b.name));
|
|
125
|
+
});
|
|
126
|
+
return list;
|
|
127
|
+
}, [sites, sheets]);
|
|
128
|
+
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (!grouped.length) {
|
|
131
|
+
setSelectedSite(null);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const stillThere = grouped.some((site) => site.id === selectedSite);
|
|
135
|
+
if (stillThere) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const withPeople = grouped.find((site) => site.rows.length > 0);
|
|
139
|
+
setSelectedSite(withPeople ? withPeople.id : grouped[0].id);
|
|
140
|
+
}, [grouped, selectedSite]);
|
|
141
|
+
|
|
142
|
+
const currentIndex = grouped.findIndex((site) => site.id === selectedSite);
|
|
143
|
+
const current = currentIndex >= 0 ? grouped[currentIndex] : null;
|
|
144
|
+
|
|
145
|
+
const changeSite = (dir) => {
|
|
146
|
+
if (!grouped.length) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const next = (currentIndex + dir + grouped.length) % grouped.length;
|
|
150
|
+
setSelectedSite(grouped[next].id);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (!stripRef.current) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const active = stripRef.current.querySelector('[data-active="true"]');
|
|
158
|
+
if (active && active.scrollIntoView) {
|
|
159
|
+
active.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
|
|
160
|
+
}
|
|
161
|
+
}, [selectedSite]);
|
|
162
|
+
|
|
163
|
+
const approve = async (id) => {
|
|
164
|
+
setApprovingId(id);
|
|
165
|
+
try {
|
|
166
|
+
await request('/time-sheets/' + id + '/approve', { method: 'PUT' });
|
|
167
|
+
setSheets((prev) =>
|
|
168
|
+
prev.map((sheet) => (sheet.id === id ? Object.assign({}, sheet, { approved: true }) : sheet))
|
|
169
|
+
);
|
|
170
|
+
strapi.notification.toggle({
|
|
171
|
+
type: 'success',
|
|
172
|
+
message: { id: 'HomePage.punchIn.approved', defaultMessage: 'Time approved' },
|
|
173
|
+
});
|
|
174
|
+
} catch (err) {
|
|
175
|
+
strapi.notification.toggle({
|
|
176
|
+
type: 'warning',
|
|
177
|
+
message: { id: 'HomePage.punchIn.approveError', defaultMessage: 'Could not approve this time-sheet' },
|
|
178
|
+
});
|
|
179
|
+
} finally {
|
|
180
|
+
setApprovingId(null);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const punchedCount = sheets.filter((sheet) => sheet && sheet.from).length;
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<Block>
|
|
188
|
+
<h2>
|
|
189
|
+
<FontAwesomeIcon icon={faClock} style={{ marginRight: 10 }} />
|
|
190
|
+
<FormattedMessage id="HomePage.punchIn.title" defaultMessage="Punched in today" />
|
|
191
|
+
</h2>
|
|
192
|
+
<p style={{ marginTop: 8, marginBottom: 16, color: '#5c5f66', fontSize: 14 }}>
|
|
193
|
+
<FormattedMessage
|
|
194
|
+
id="HomePage.punchIn.subtitle"
|
|
195
|
+
defaultMessage="{count} {count, plural, one {person} other {people}} clocked in · grouped by site"
|
|
196
|
+
values={{ count: punchedCount }}
|
|
197
|
+
/>
|
|
198
|
+
</p>
|
|
199
|
+
|
|
200
|
+
{loading && (
|
|
201
|
+
<EmptyPunch>
|
|
202
|
+
<FormattedMessage id="HomePage.punchIn.loading" defaultMessage="Loading today's punch-ins…" />
|
|
203
|
+
</EmptyPunch>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
{!loading && error && <EmptyPunch>{error}</EmptyPunch>}
|
|
207
|
+
|
|
208
|
+
{!loading && !error && grouped.length === 0 && (
|
|
209
|
+
<EmptyPunch>
|
|
210
|
+
<FormattedMessage id="HomePage.punchIn.empty" defaultMessage="No one has punched in yet today." />
|
|
211
|
+
</EmptyPunch>
|
|
212
|
+
)}
|
|
213
|
+
|
|
214
|
+
{!loading && !error && grouped.length > 0 && (
|
|
215
|
+
<>
|
|
216
|
+
<SiteNav>
|
|
217
|
+
<SiteNavBtn type="button" onClick={() => changeSite(-1)} aria-label="Previous site">
|
|
218
|
+
<FontAwesomeIcon icon={faChevronLeft} />
|
|
219
|
+
</SiteNavBtn>
|
|
220
|
+
<SiteStrip ref={stripRef}>
|
|
221
|
+
{grouped.map((site) => (
|
|
222
|
+
<SiteTab
|
|
223
|
+
key={site.id}
|
|
224
|
+
type="button"
|
|
225
|
+
$active={site.id === selectedSite}
|
|
226
|
+
data-active={site.id === selectedSite ? 'true' : undefined}
|
|
227
|
+
onClick={() => setSelectedSite(site.id)}
|
|
228
|
+
>
|
|
229
|
+
<FontAwesomeIcon icon={faMapMarkerAlt} />
|
|
230
|
+
<span>{site.name}</span>
|
|
231
|
+
<em>{site.rows.length}</em>
|
|
232
|
+
</SiteTab>
|
|
233
|
+
))}
|
|
234
|
+
</SiteStrip>
|
|
235
|
+
<SiteNavBtn type="button" onClick={() => changeSite(1)} aria-label="Next site">
|
|
236
|
+
<FontAwesomeIcon icon={faChevronRight} />
|
|
237
|
+
</SiteNavBtn>
|
|
238
|
+
</SiteNav>
|
|
239
|
+
|
|
240
|
+
{!current || current.rows.length === 0 ? (
|
|
241
|
+
<EmptyPunch>
|
|
242
|
+
<FormattedMessage
|
|
243
|
+
id="HomePage.punchIn.noneAtSite"
|
|
244
|
+
defaultMessage="No one has punched in at this site today."
|
|
245
|
+
/>
|
|
246
|
+
</EmptyPunch>
|
|
247
|
+
) : (
|
|
248
|
+
<PunchList>
|
|
249
|
+
{current.rows
|
|
250
|
+
.slice()
|
|
251
|
+
.sort((a, b) => String(displayName(a.users)).localeCompare(String(displayName(b.users))))
|
|
252
|
+
.map((sheet) => {
|
|
253
|
+
const fromLabel = clockLabel(sheet.from);
|
|
254
|
+
const untilLabel = clockLabel(sheet.until);
|
|
255
|
+
const canApprove = Boolean(sheet.from && sheet.until && !sheet.approved);
|
|
256
|
+
return (
|
|
257
|
+
<PunchRow key={sheet.id}>
|
|
258
|
+
<PunchMeta>
|
|
259
|
+
<PunchName>{displayName(sheet.users)}</PunchName>
|
|
260
|
+
<PunchTimes>
|
|
261
|
+
<span>
|
|
262
|
+
<FontAwesomeIcon icon={faSignInAlt} /> {fromLabel || '—'}
|
|
263
|
+
</span>
|
|
264
|
+
<span>
|
|
265
|
+
<FontAwesomeIcon icon={faSignOutAlt} /> {untilLabel || 'On site'}
|
|
266
|
+
</span>
|
|
267
|
+
</PunchTimes>
|
|
268
|
+
</PunchMeta>
|
|
269
|
+
<PunchAction>
|
|
270
|
+
{sheet.approved ? (
|
|
271
|
+
<StatusPill $ok>
|
|
272
|
+
<FontAwesomeIcon icon={faCheckCircle} /> Approved
|
|
273
|
+
</StatusPill>
|
|
274
|
+
) : canApprove ? (
|
|
275
|
+
<ApproveBtn
|
|
276
|
+
type="button"
|
|
277
|
+
disabled={approvingId === sheet.id}
|
|
278
|
+
onClick={() => approve(sheet.id)}
|
|
279
|
+
>
|
|
280
|
+
<FontAwesomeIcon icon={faThumbsUp} />
|
|
281
|
+
{approvingId === sheet.id ? 'Saving…' : 'Approve'}
|
|
282
|
+
</ApproveBtn>
|
|
283
|
+
) : (
|
|
284
|
+
<StatusPill>Awaiting clock-out</StatusPill>
|
|
285
|
+
)}
|
|
286
|
+
</PunchAction>
|
|
287
|
+
</PunchRow>
|
|
288
|
+
);
|
|
289
|
+
})}
|
|
290
|
+
</PunchList>
|
|
291
|
+
)}
|
|
292
|
+
</>
|
|
293
|
+
)}
|
|
294
|
+
</Block>
|
|
295
|
+
);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
export default PunchInToday;
|
|
@@ -257,4 +257,161 @@ const SocialLinkWrapper = styled.div`
|
|
|
257
257
|
}
|
|
258
258
|
`;
|
|
259
259
|
|
|
260
|
-
|
|
260
|
+
const SiteNav = styled.div`
|
|
261
|
+
display: flex;
|
|
262
|
+
align-items: center;
|
|
263
|
+
gap: 8px;
|
|
264
|
+
margin-bottom: 16px;
|
|
265
|
+
`;
|
|
266
|
+
|
|
267
|
+
const SiteStrip = styled.div`
|
|
268
|
+
display: flex;
|
|
269
|
+
gap: 8px;
|
|
270
|
+
overflow-x: auto;
|
|
271
|
+
flex: 1;
|
|
272
|
+
min-width: 0;
|
|
273
|
+
padding: 4px 2px 8px;
|
|
274
|
+
scroll-behavior: smooth;
|
|
275
|
+
|
|
276
|
+
&::-webkit-scrollbar {
|
|
277
|
+
height: 6px;
|
|
278
|
+
}
|
|
279
|
+
&::-webkit-scrollbar-thumb {
|
|
280
|
+
background: #c0c4cc;
|
|
281
|
+
border-radius: 3px;
|
|
282
|
+
}
|
|
283
|
+
`;
|
|
284
|
+
|
|
285
|
+
const SiteTab = styled.button`
|
|
286
|
+
display: inline-flex;
|
|
287
|
+
align-items: center;
|
|
288
|
+
gap: 8px;
|
|
289
|
+
flex: 0 0 auto;
|
|
290
|
+
height: 34px;
|
|
291
|
+
padding: 0 12px;
|
|
292
|
+
border: 1px solid ${props => (props.$active ? '#007eff' : '#e3e9f3')};
|
|
293
|
+
border-radius: 17px;
|
|
294
|
+
background: ${props => (props.$active ? '#007eff' : '#ffffff')};
|
|
295
|
+
color: ${props => (props.$active ? '#ffffff' : '#333740')};
|
|
296
|
+
font-size: 13px;
|
|
297
|
+
font-weight: 600;
|
|
298
|
+
cursor: pointer;
|
|
299
|
+
white-space: nowrap;
|
|
300
|
+
|
|
301
|
+
em {
|
|
302
|
+
font-style: normal;
|
|
303
|
+
min-width: 18px;
|
|
304
|
+
height: 18px;
|
|
305
|
+
padding: 0 5px;
|
|
306
|
+
border-radius: 9px;
|
|
307
|
+
background: ${props => (props.$active ? 'rgba(255,255,255,0.2)' : '#f0f3f8')};
|
|
308
|
+
font-size: 11px;
|
|
309
|
+
line-height: 18px;
|
|
310
|
+
text-align: center;
|
|
311
|
+
}
|
|
312
|
+
`;
|
|
313
|
+
|
|
314
|
+
const SiteNavBtn = styled.button`
|
|
315
|
+
width: 32px;
|
|
316
|
+
height: 32px;
|
|
317
|
+
flex: 0 0 32px;
|
|
318
|
+
border: 1px solid #e3e9f3;
|
|
319
|
+
border-radius: 4px;
|
|
320
|
+
background: #ffffff;
|
|
321
|
+
color: #007eff;
|
|
322
|
+
cursor: pointer;
|
|
323
|
+
|
|
324
|
+
&:hover {
|
|
325
|
+
background: #f7f8f8;
|
|
326
|
+
}
|
|
327
|
+
`;
|
|
328
|
+
|
|
329
|
+
const PunchList = styled.div`
|
|
330
|
+
max-height: 360px;
|
|
331
|
+
overflow-y: auto;
|
|
332
|
+
`;
|
|
333
|
+
|
|
334
|
+
const PunchRow = styled.div`
|
|
335
|
+
display: flex;
|
|
336
|
+
align-items: center;
|
|
337
|
+
justify-content: space-between;
|
|
338
|
+
gap: 12px;
|
|
339
|
+
padding: 10px 0;
|
|
340
|
+
border-bottom: 1px solid #f0f3f8;
|
|
341
|
+
|
|
342
|
+
&:last-child {
|
|
343
|
+
border-bottom: none;
|
|
344
|
+
}
|
|
345
|
+
`;
|
|
346
|
+
|
|
347
|
+
const PunchMeta = styled.div`
|
|
348
|
+
min-width: 0;
|
|
349
|
+
`;
|
|
350
|
+
|
|
351
|
+
const PunchName = styled.div`
|
|
352
|
+
font-size: 14px;
|
|
353
|
+
font-weight: 600;
|
|
354
|
+
color: #333740;
|
|
355
|
+
`;
|
|
356
|
+
|
|
357
|
+
const PunchTimes = styled.div`
|
|
358
|
+
display: flex;
|
|
359
|
+
gap: 14px;
|
|
360
|
+
margin-top: 4px;
|
|
361
|
+
font-size: 12px;
|
|
362
|
+
color: #5c5f66;
|
|
363
|
+
|
|
364
|
+
svg {
|
|
365
|
+
margin-right: 4px;
|
|
366
|
+
color: #007eff;
|
|
367
|
+
}
|
|
368
|
+
`;
|
|
369
|
+
|
|
370
|
+
const PunchAction = styled.div`
|
|
371
|
+
flex: 0 0 auto;
|
|
372
|
+
`;
|
|
373
|
+
|
|
374
|
+
const EmptyPunch = styled.p`
|
|
375
|
+
margin: 8px 0 0;
|
|
376
|
+
color: #5c5f66;
|
|
377
|
+
font-size: 14px;
|
|
378
|
+
`;
|
|
379
|
+
|
|
380
|
+
const StatusPill = styled.span`
|
|
381
|
+
display: inline-flex;
|
|
382
|
+
align-items: center;
|
|
383
|
+
gap: 6px;
|
|
384
|
+
height: 28px;
|
|
385
|
+
padding: 0 10px;
|
|
386
|
+
border-radius: 14px;
|
|
387
|
+
font-size: 12px;
|
|
388
|
+
font-weight: 600;
|
|
389
|
+
color: ${props => (props.$ok ? '#27b97c' : '#8e8ea9')};
|
|
390
|
+
background: ${props => (props.$ok ? '#eafbe7' : '#f0f3f8')};
|
|
391
|
+
`;
|
|
392
|
+
|
|
393
|
+
const ApproveBtn = styled.button`
|
|
394
|
+
display: inline-flex;
|
|
395
|
+
align-items: center;
|
|
396
|
+
gap: 6px;
|
|
397
|
+
height: 32px;
|
|
398
|
+
padding: 0 12px;
|
|
399
|
+
border: none;
|
|
400
|
+
border-radius: 4px;
|
|
401
|
+
background: #007eff;
|
|
402
|
+
color: #ffffff;
|
|
403
|
+
font-size: 13px;
|
|
404
|
+
font-weight: 600;
|
|
405
|
+
cursor: pointer;
|
|
406
|
+
|
|
407
|
+
&:disabled {
|
|
408
|
+
opacity: 0.6;
|
|
409
|
+
cursor: default;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
&:hover:not(:disabled) {
|
|
413
|
+
background: #005fea;
|
|
414
|
+
}
|
|
415
|
+
`;
|
|
416
|
+
|
|
417
|
+
export { ALink, Block, Container, LinkWrapper, P, Separator, SocialLinkWrapper, Wave, SiteStrip, SiteTab, SiteNav, SiteNavBtn, PunchList, PunchRow, PunchMeta, PunchName, PunchTimes, PunchAction, EmptyPunch, StatusPill, ApproveBtn };
|
|
@@ -14,6 +14,7 @@ import { useModels } from '../../hooks';
|
|
|
14
14
|
import useFetch from './hooks';
|
|
15
15
|
import { ALink, Block, Container, LinkWrapper, P, Wave, Separator } from './components';
|
|
16
16
|
import SocialLink from './SocialLink';
|
|
17
|
+
import PunchInToday from './PunchInToday';
|
|
17
18
|
|
|
18
19
|
const FIRST_BLOCK_LINKS = [
|
|
19
20
|
{
|
|
@@ -86,6 +87,11 @@ const HomePage = ({ history: { push } }) => {
|
|
|
86
87
|
{title => <PageTitle title={title} />}
|
|
87
88
|
</FormattedMessage>
|
|
88
89
|
<Container className="container-fluid">
|
|
90
|
+
<div className="row">
|
|
91
|
+
<div className="col-12">
|
|
92
|
+
<PunchInToday />
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
89
95
|
<div className="row">
|
|
90
96
|
<div className="col-lg-8 col-md-12">
|
|
91
97
|
<Block>
|
|
@@ -54,6 +54,13 @@
|
|
|
54
54
|
"HomePage.community": "Join Punch-in Community",
|
|
55
55
|
"HomePage.greetings": "Hi {name}!",
|
|
56
56
|
"HomePage.helmet.title": "Homepage",
|
|
57
|
+
"HomePage.punchIn.title": "Punched in today",
|
|
58
|
+
"HomePage.punchIn.subtitle": "{count} {count, plural, one {person} other {people}} clocked in · grouped by site",
|
|
59
|
+
"HomePage.punchIn.loading": "Loading today's punch-ins…",
|
|
60
|
+
"HomePage.punchIn.empty": "No one has punched in yet today.",
|
|
61
|
+
"HomePage.punchIn.noneAtSite": "No one has punched in at this site today.",
|
|
62
|
+
"HomePage.punchIn.approved": "Time approved",
|
|
63
|
+
"HomePage.punchIn.approveError": "Could not approve this time-sheet",
|
|
57
64
|
"HomePage.roadmap": "See our roadmap",
|
|
58
65
|
"HomePage.welcome.congrats": "Congrats!",
|
|
59
66
|
"HomePage.welcome.congrats.content": "You are logged in as the first administrator. To discover the powerful features provided by Punch-in,",
|
package/package.json
CHANGED