@punch-in/strapi-admin 1.2.15 → 1.2.17
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/GlobalStyle/index.js +19 -0
- package/admin/src/components/LeftMenu/LeftMenuLinkHeader/Title.js +1 -0
- package/admin/src/components/LeftMenu/LeftMenuLinkSection/LeftMenuListLink.js +8 -1
- package/admin/src/components/LeftMenu/LinksContainer/index.js +2 -0
- package/admin/src/containers/HomePage/PunchInToday.js +1 -40
- package/admin/src/containers/HomePage/RollCall.js +355 -0
- package/admin/src/containers/HomePage/VisitorApprovals.js +295 -0
- package/admin/src/containers/HomePage/components.js +122 -1
- package/admin/src/containers/HomePage/index.js +4 -0
- package/admin/src/containers/HomePage/punchUtils.js +42 -0
- package/admin/src/containers/LocaleToggle/Wrapper.js +4 -0
- package/admin/src/translations/en.json +18 -1
- package/package.json +1 -1
- package/strapi-admin/node_modules/is-wsl/index.d.ts +0 -15
- package/strapi-admin/node_modules/is-wsl/index.js +0 -31
- package/strapi-admin/node_modules/is-wsl/license +0 -9
- package/strapi-admin/node_modules/is-wsl/package.json +0 -77
- package/strapi-admin/node_modules/is-wsl/readme.md +0 -36
- package/strapi-admin/node_modules/qs/.editorconfig +0 -39
- package/strapi-admin/node_modules/qs/.eslintignore +0 -2
- package/strapi-admin/node_modules/qs/.eslintrc +0 -35
- package/strapi-admin/node_modules/qs/.github/FUNDING.yml +0 -12
- package/strapi-admin/node_modules/qs/.nycrc +0 -13
- package/strapi-admin/node_modules/qs/CHANGELOG.md +0 -372
- package/strapi-admin/node_modules/qs/LICENSE.md +0 -29
- package/strapi-admin/node_modules/qs/README.md +0 -611
- package/strapi-admin/node_modules/qs/dist/qs.js +0 -1903
- package/strapi-admin/node_modules/qs/lib/formats.js +0 -23
- package/strapi-admin/node_modules/qs/lib/index.js +0 -11
- package/strapi-admin/node_modules/qs/lib/parse.js +0 -263
- package/strapi-admin/node_modules/qs/lib/stringify.js +0 -290
- package/strapi-admin/node_modules/qs/lib/utils.js +0 -251
- package/strapi-admin/node_modules/qs/package.json +0 -100
- package/strapi-admin/node_modules/qs/test/parse.js +0 -781
- package/strapi-admin/node_modules/qs/test/stringify.js +0 -848
- package/strapi-admin/node_modules/qs/test/utils.js +0 -136
|
@@ -173,6 +173,25 @@ const GlobalStyle = createGlobalStyle`
|
|
|
173
173
|
a::-moz-focus-inner {
|
|
174
174
|
border: 0;
|
|
175
175
|
}
|
|
176
|
+
|
|
177
|
+
/*
|
|
178
|
+
* Date picker (react-dates, styled by @punch-in/buffet-modern-styles).
|
|
179
|
+
* That package colours the weekday names with its near-white "greyIconBkgd"
|
|
180
|
+
* (#F6F6F9, invisible on a white popup) and shortens the month caption, so the
|
|
181
|
+
* absolutely positioned weekday header (z-index 2) sits on top of the first row of
|
|
182
|
+
* dates and swallows their clicks. Fix both here until the package is fixed.
|
|
183
|
+
*/
|
|
184
|
+
.DayPicker_weekHeader {
|
|
185
|
+
pointer-events: none;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.DayPicker_weekHeader li small {
|
|
189
|
+
color: #666687 !important;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.CalendarMonth_caption {
|
|
193
|
+
padding-bottom: 52px !important;
|
|
194
|
+
}
|
|
176
195
|
`;
|
|
177
196
|
|
|
178
197
|
export default GlobalStyle;
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import styled from 'styled-components';
|
|
2
2
|
|
|
3
3
|
const LeftMenuListLink = styled.div`
|
|
4
|
-
|
|
4
|
+
/* No fixed height: sections share the menu's height and each scrolls
|
|
5
|
+
on its own, so later sections are never pushed below the viewport. */
|
|
6
|
+
flex: 0 1 auto;
|
|
7
|
+
min-height: 0;
|
|
8
|
+
/* The Plugins (modules) section never shrinks, so all its items stay
|
|
9
|
+
visible; the long collection/single type lists give up space instead.
|
|
10
|
+
The cap only kicks in on very small screens, where it scrolls. */
|
|
11
|
+
${({ section }) => (section === 'plugins' ? 'flex-shrink: 0; max-height: 60%;' : '')}
|
|
5
12
|
margin-bottom: 19px;
|
|
6
13
|
overflow-y: hidden;
|
|
7
14
|
width: 22rem;
|
|
@@ -12,10 +12,8 @@ import {
|
|
|
12
12
|
faCheckCircle,
|
|
13
13
|
faClock,
|
|
14
14
|
} from '@fortawesome/free-solid-svg-icons';
|
|
15
|
-
import moment from 'moment';
|
|
16
15
|
import { Block, SiteStrip, SiteTab, SiteNav, SiteNavBtn, PunchList, PunchRow, PunchMeta, PunchName, PunchTimes, PunchAction, EmptyPunch, StatusPill, ApproveBtn } from './components';
|
|
17
|
-
|
|
18
|
-
const NONE_SITE = 'none';
|
|
16
|
+
import { NONE_SITE, displayName, clockLabel, siteIdOf, siteNameOf } from './punchUtils';
|
|
19
17
|
|
|
20
18
|
const startOfTodayIso = () => {
|
|
21
19
|
const d = new Date();
|
|
@@ -23,43 +21,6 @@ const startOfTodayIso = () => {
|
|
|
23
21
|
return d.toISOString();
|
|
24
22
|
};
|
|
25
23
|
|
|
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
24
|
const PunchInToday = () => {
|
|
64
25
|
const stripRef = useRef(null);
|
|
65
26
|
const [loading, setLoading] = useState(true);
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, 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
|
+
faClipboardCheck,
|
|
7
|
+
faCheck,
|
|
8
|
+
faMapMarkerAlt,
|
|
9
|
+
faSignInAlt,
|
|
10
|
+
faSignOutAlt,
|
|
11
|
+
faSyncAlt,
|
|
12
|
+
} from '@fortawesome/free-solid-svg-icons';
|
|
13
|
+
import moment from 'moment';
|
|
14
|
+
import {
|
|
15
|
+
Block,
|
|
16
|
+
SiteStrip,
|
|
17
|
+
SiteTab,
|
|
18
|
+
SiteNav,
|
|
19
|
+
PunchList,
|
|
20
|
+
PunchRow,
|
|
21
|
+
PunchMeta,
|
|
22
|
+
PunchName,
|
|
23
|
+
PunchTimes,
|
|
24
|
+
PunchAction,
|
|
25
|
+
EmptyPunch,
|
|
26
|
+
StatusPill,
|
|
27
|
+
RollCallButton,
|
|
28
|
+
RollCallHeader,
|
|
29
|
+
RollCallSummary,
|
|
30
|
+
RefreshBtn,
|
|
31
|
+
VisitorBadge,
|
|
32
|
+
CheckToggle,
|
|
33
|
+
CheckedTag,
|
|
34
|
+
UndoBtn,
|
|
35
|
+
} from './components';
|
|
36
|
+
import { displayName, clockLabel, siteIdOf, siteNameOf } from './punchUtils';
|
|
37
|
+
|
|
38
|
+
// Site roll call: everyone clocked in (employees and visitors) who hasn't
|
|
39
|
+
// clocked out, or clocked out within the last hour - see the server's
|
|
40
|
+
// time-sheet onSite controller. The officer ticks people off as they're
|
|
41
|
+
// accounted for.
|
|
42
|
+
//
|
|
43
|
+
// Ticks are kept in this browser only (localStorage, keyed by time-sheet id)
|
|
44
|
+
// so a refresh mid-evacuation doesn't lose them; they are not shared with
|
|
45
|
+
// other admins or the tablet.
|
|
46
|
+
|
|
47
|
+
const ALL_SITES = 'all';
|
|
48
|
+
const REFRESH_MS = 60 * 1000;
|
|
49
|
+
const CHECKS_KEY = 'punchin.rollCall.checks';
|
|
50
|
+
// Ticks older than this are dropped on load - a roll call is a same-day job.
|
|
51
|
+
const CHECK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
52
|
+
|
|
53
|
+
const loadChecks = () => {
|
|
54
|
+
try {
|
|
55
|
+
const raw = JSON.parse(window.localStorage.getItem(CHECKS_KEY) || '{}');
|
|
56
|
+
const cutoff = Date.now() - CHECK_TTL_MS;
|
|
57
|
+
return Object.keys(raw).reduce((acc, id) => {
|
|
58
|
+
if (typeof raw[id] === 'number' && raw[id] >= cutoff) {
|
|
59
|
+
acc[id] = raw[id];
|
|
60
|
+
}
|
|
61
|
+
return acc;
|
|
62
|
+
}, {});
|
|
63
|
+
} catch (err) {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const saveChecks = (checks) => {
|
|
69
|
+
try {
|
|
70
|
+
window.localStorage.setItem(CHECKS_KEY, JSON.stringify(checks));
|
|
71
|
+
} catch (err) {
|
|
72
|
+
// Storage unavailable (private window, quota) - ticks still work for this page view.
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Clock-in time, with the date when it wasn't today so a forgotten clock-out
|
|
77
|
+
// from an earlier day stands out instead of reading as "on site now".
|
|
78
|
+
const clockInLabel = (value) => {
|
|
79
|
+
const parsed = moment(value);
|
|
80
|
+
if (!parsed.isValid()) {
|
|
81
|
+
return '—';
|
|
82
|
+
}
|
|
83
|
+
return parsed.isSame(moment(), 'day') ? parsed.format('HH:mm') : parsed.format('D MMM HH:mm');
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const RollCall = () => {
|
|
87
|
+
const [open, setOpen] = useState(false);
|
|
88
|
+
const [loading, setLoading] = useState(false);
|
|
89
|
+
const [error, setError] = useState(null);
|
|
90
|
+
const [sheets, setSheets] = useState([]);
|
|
91
|
+
const [loadedAt, setLoadedAt] = useState(null);
|
|
92
|
+
const [selectedSite, setSelectedSite] = useState(ALL_SITES);
|
|
93
|
+
const [checks, setChecks] = useState(loadChecks);
|
|
94
|
+
|
|
95
|
+
const load = useCallback(async () => {
|
|
96
|
+
setLoading(true);
|
|
97
|
+
setError(null);
|
|
98
|
+
try {
|
|
99
|
+
const list = await request('/time-sheets/on-site', { method: 'GET' });
|
|
100
|
+
setSheets(Array.isArray(list) ? list : []);
|
|
101
|
+
setLoadedAt(new Date());
|
|
102
|
+
} catch (err) {
|
|
103
|
+
setError('Could not load the roll call. Check your connection and try again.');
|
|
104
|
+
} finally {
|
|
105
|
+
setLoading(false);
|
|
106
|
+
}
|
|
107
|
+
}, []);
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (!open) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
load();
|
|
114
|
+
const timer = setInterval(load, REFRESH_MS);
|
|
115
|
+
return () => clearInterval(timer);
|
|
116
|
+
}, [open, load]);
|
|
117
|
+
|
|
118
|
+
const setChecked = (id, isChecked) => {
|
|
119
|
+
setChecks((prev) => {
|
|
120
|
+
const next = Object.assign({}, prev);
|
|
121
|
+
if (isChecked) {
|
|
122
|
+
next[id] = Date.now();
|
|
123
|
+
} else {
|
|
124
|
+
delete next[id];
|
|
125
|
+
}
|
|
126
|
+
saveChecks(next);
|
|
127
|
+
return next;
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const sites = useMemo(() => {
|
|
132
|
+
const map = {};
|
|
133
|
+
sheets.forEach((sheet) => {
|
|
134
|
+
const id = siteIdOf(sheet.site);
|
|
135
|
+
if (!map[id]) {
|
|
136
|
+
map[id] = { id: id, name: siteNameOf(sheet.site), count: 0 };
|
|
137
|
+
}
|
|
138
|
+
map[id].count += 1;
|
|
139
|
+
});
|
|
140
|
+
return Object.keys(map)
|
|
141
|
+
.map((key) => map[key])
|
|
142
|
+
.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
143
|
+
}, [sheets]);
|
|
144
|
+
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (selectedSite !== ALL_SITES && !sites.some((site) => site.id === selectedSite)) {
|
|
147
|
+
setSelectedSite(ALL_SITES);
|
|
148
|
+
}
|
|
149
|
+
}, [sites, selectedSite]);
|
|
150
|
+
|
|
151
|
+
// Still on site first, then those who've left; alphabetical within each.
|
|
152
|
+
// Order doesn't change when someone is ticked, so rows don't jump under
|
|
153
|
+
// the officer's cursor.
|
|
154
|
+
const rows = useMemo(
|
|
155
|
+
() =>
|
|
156
|
+
sheets
|
|
157
|
+
.filter((sheet) => selectedSite === ALL_SITES || siteIdOf(sheet.site) === selectedSite)
|
|
158
|
+
.slice()
|
|
159
|
+
.sort((a, b) => {
|
|
160
|
+
if (!a.until !== !b.until) {
|
|
161
|
+
return a.until ? 1 : -1;
|
|
162
|
+
}
|
|
163
|
+
return String(displayName(a.users)).localeCompare(String(displayName(b.users)));
|
|
164
|
+
}),
|
|
165
|
+
[sheets, selectedSite]
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const onSiteCount = rows.filter((sheet) => !sheet.until).length;
|
|
169
|
+
const leftCount = rows.length - onSiteCount;
|
|
170
|
+
const checkedCount = rows.filter((sheet) => checks[sheet.id]).length;
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<>
|
|
174
|
+
<RollCallButton type="button" $open={open} aria-expanded={open} onClick={() => setOpen(!open)}>
|
|
175
|
+
<FontAwesomeIcon icon={faClipboardCheck} />
|
|
176
|
+
{open ? (
|
|
177
|
+
<FormattedMessage id="HomePage.rollCall.hide" defaultMessage="Hide site roll call" />
|
|
178
|
+
) : (
|
|
179
|
+
<FormattedMessage id="HomePage.rollCall.button" defaultMessage="Site roll call" />
|
|
180
|
+
)}
|
|
181
|
+
</RollCallButton>
|
|
182
|
+
|
|
183
|
+
{open && (
|
|
184
|
+
<Block>
|
|
185
|
+
<RollCallHeader>
|
|
186
|
+
<div>
|
|
187
|
+
<h2>
|
|
188
|
+
<FontAwesomeIcon icon={faClipboardCheck} style={{ marginRight: 10 }} />
|
|
189
|
+
<FormattedMessage id="HomePage.rollCall.title" defaultMessage="Site roll call" />
|
|
190
|
+
</h2>
|
|
191
|
+
<p style={{ marginTop: 8, color: '#5c5f66', fontSize: 14 }}>
|
|
192
|
+
<FormattedMessage
|
|
193
|
+
id="HomePage.rollCall.subtitle"
|
|
194
|
+
defaultMessage="Everyone clocked in, including visitors, plus anyone who clocked out in the last hour."
|
|
195
|
+
/>
|
|
196
|
+
</p>
|
|
197
|
+
</div>
|
|
198
|
+
<RefreshBtn type="button" onClick={load} disabled={loading}>
|
|
199
|
+
<FontAwesomeIcon icon={faSyncAlt} spin={loading} />
|
|
200
|
+
{loadedAt ? (
|
|
201
|
+
<FormattedMessage
|
|
202
|
+
id="HomePage.rollCall.updated"
|
|
203
|
+
defaultMessage="Updated {time}"
|
|
204
|
+
values={{ time: moment(loadedAt).format('HH:mm') }}
|
|
205
|
+
/>
|
|
206
|
+
) : (
|
|
207
|
+
<FormattedMessage id="HomePage.rollCall.refresh" defaultMessage="Refresh" />
|
|
208
|
+
)}
|
|
209
|
+
</RefreshBtn>
|
|
210
|
+
</RollCallHeader>
|
|
211
|
+
|
|
212
|
+
{error && <EmptyPunch>{error}</EmptyPunch>}
|
|
213
|
+
|
|
214
|
+
{!error && loading && !loadedAt && (
|
|
215
|
+
<EmptyPunch>
|
|
216
|
+
<FormattedMessage id="HomePage.rollCall.loading" defaultMessage="Loading roll call…" />
|
|
217
|
+
</EmptyPunch>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
{!error && loadedAt && (
|
|
221
|
+
<>
|
|
222
|
+
<RollCallSummary>
|
|
223
|
+
<span>
|
|
224
|
+
<FormattedMessage
|
|
225
|
+
id="HomePage.rollCall.onSite"
|
|
226
|
+
defaultMessage="{count} on site"
|
|
227
|
+
values={{ count: <b>{onSiteCount}</b> }}
|
|
228
|
+
/>
|
|
229
|
+
</span>
|
|
230
|
+
<span>
|
|
231
|
+
<FormattedMessage
|
|
232
|
+
id="HomePage.rollCall.left"
|
|
233
|
+
defaultMessage="{count} left in the last hour"
|
|
234
|
+
values={{ count: <b>{leftCount}</b> }}
|
|
235
|
+
/>
|
|
236
|
+
</span>
|
|
237
|
+
<span>
|
|
238
|
+
<FormattedMessage
|
|
239
|
+
id="HomePage.rollCall.checkedCount"
|
|
240
|
+
defaultMessage="{checked} of {total} checked"
|
|
241
|
+
values={{ checked: <b>{checkedCount}</b>, total: rows.length }}
|
|
242
|
+
/>
|
|
243
|
+
</span>
|
|
244
|
+
</RollCallSummary>
|
|
245
|
+
|
|
246
|
+
{sites.length > 1 && (
|
|
247
|
+
<SiteNav>
|
|
248
|
+
<SiteStrip>
|
|
249
|
+
<SiteTab
|
|
250
|
+
type="button"
|
|
251
|
+
$active={selectedSite === ALL_SITES}
|
|
252
|
+
onClick={() => setSelectedSite(ALL_SITES)}
|
|
253
|
+
>
|
|
254
|
+
<FormattedMessage id="HomePage.rollCall.allSites" defaultMessage="All sites" />
|
|
255
|
+
<em>{sheets.length}</em>
|
|
256
|
+
</SiteTab>
|
|
257
|
+
{sites.map((site) => (
|
|
258
|
+
<SiteTab
|
|
259
|
+
key={site.id}
|
|
260
|
+
type="button"
|
|
261
|
+
$active={site.id === selectedSite}
|
|
262
|
+
onClick={() => setSelectedSite(site.id)}
|
|
263
|
+
>
|
|
264
|
+
<FontAwesomeIcon icon={faMapMarkerAlt} />
|
|
265
|
+
<span>{site.name}</span>
|
|
266
|
+
<em>{site.count}</em>
|
|
267
|
+
</SiteTab>
|
|
268
|
+
))}
|
|
269
|
+
</SiteStrip>
|
|
270
|
+
</SiteNav>
|
|
271
|
+
)}
|
|
272
|
+
|
|
273
|
+
{rows.length === 0 ? (
|
|
274
|
+
<EmptyPunch>
|
|
275
|
+
<FormattedMessage
|
|
276
|
+
id="HomePage.rollCall.empty"
|
|
277
|
+
defaultMessage="No one is clocked in, and no one has clocked out in the last hour."
|
|
278
|
+
/>
|
|
279
|
+
</EmptyPunch>
|
|
280
|
+
) : (
|
|
281
|
+
<PunchList style={{ maxHeight: 520 }}>
|
|
282
|
+
{rows.map((sheet) => {
|
|
283
|
+
const isChecked = Boolean(checks[sheet.id]);
|
|
284
|
+
return (
|
|
285
|
+
<PunchRow key={sheet.id}>
|
|
286
|
+
<PunchMeta>
|
|
287
|
+
<PunchName>
|
|
288
|
+
{displayName(sheet.users)}
|
|
289
|
+
{sheet.isVisitor && (
|
|
290
|
+
<VisitorBadge>
|
|
291
|
+
<FormattedMessage id="HomePage.rollCall.visitor" defaultMessage="Visitor" />
|
|
292
|
+
</VisitorBadge>
|
|
293
|
+
)}
|
|
294
|
+
</PunchName>
|
|
295
|
+
<PunchTimes>
|
|
296
|
+
<span>
|
|
297
|
+
<FontAwesomeIcon icon={faSignInAlt} /> {clockInLabel(sheet.from)}
|
|
298
|
+
</span>
|
|
299
|
+
{sheet.until ? (
|
|
300
|
+
<span>
|
|
301
|
+
<FontAwesomeIcon icon={faSignOutAlt} />
|
|
302
|
+
<FormattedMessage
|
|
303
|
+
id="HomePage.rollCall.leftAt"
|
|
304
|
+
defaultMessage="Left at {time}"
|
|
305
|
+
values={{ time: clockLabel(sheet.until) || '—' }}
|
|
306
|
+
/>
|
|
307
|
+
</span>
|
|
308
|
+
) : (
|
|
309
|
+
<StatusPill style={{ height: 18, padding: '0 8px', fontSize: 11 }}>
|
|
310
|
+
<FormattedMessage id="HomePage.rollCall.stillOnSite" defaultMessage="On site" />
|
|
311
|
+
</StatusPill>
|
|
312
|
+
)}
|
|
313
|
+
{selectedSite === ALL_SITES && sites.length > 1 && (
|
|
314
|
+
<span>
|
|
315
|
+
<FontAwesomeIcon icon={faMapMarkerAlt} /> {siteNameOf(sheet.site)}
|
|
316
|
+
</span>
|
|
317
|
+
)}
|
|
318
|
+
</PunchTimes>
|
|
319
|
+
</PunchMeta>
|
|
320
|
+
<PunchAction>
|
|
321
|
+
{isChecked ? (
|
|
322
|
+
<>
|
|
323
|
+
<CheckedTag>
|
|
324
|
+
<FontAwesomeIcon icon={faCheck} />
|
|
325
|
+
<FormattedMessage id="HomePage.rollCall.checked" defaultMessage="Checked" />
|
|
326
|
+
</CheckedTag>
|
|
327
|
+
<UndoBtn type="button" onClick={() => setChecked(sheet.id, false)}>
|
|
328
|
+
<FormattedMessage id="HomePage.rollCall.undo" defaultMessage="Undo" />
|
|
329
|
+
</UndoBtn>
|
|
330
|
+
</>
|
|
331
|
+
) : (
|
|
332
|
+
<CheckToggle>
|
|
333
|
+
<input
|
|
334
|
+
type="checkbox"
|
|
335
|
+
checked={false}
|
|
336
|
+
onChange={() => setChecked(sheet.id, true)}
|
|
337
|
+
/>
|
|
338
|
+
<FormattedMessage id="HomePage.rollCall.checked" defaultMessage="Checked" />
|
|
339
|
+
</CheckToggle>
|
|
340
|
+
)}
|
|
341
|
+
</PunchAction>
|
|
342
|
+
</PunchRow>
|
|
343
|
+
);
|
|
344
|
+
})}
|
|
345
|
+
</PunchList>
|
|
346
|
+
)}
|
|
347
|
+
</>
|
|
348
|
+
)}
|
|
349
|
+
</Block>
|
|
350
|
+
)}
|
|
351
|
+
</>
|
|
352
|
+
);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
export default RollCall;
|