@spokane-folio/security-incident 1.0.28

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.
Files changed (188) hide show
  1. package/.eslintrc +32 -0
  2. package/.github/workflows/CODEOWNERS +8 -0
  3. package/.github/workflows/pr-validation.yml +44 -0
  4. package/.github/workflows/release.yml +64 -0
  5. package/.prettierrc +6 -0
  6. package/.stripesclirc +4 -0
  7. package/CHANGELOG.md +8 -0
  8. package/CONTRIBUTING.md +4 -0
  9. package/LICENSE +201 -0
  10. package/README.md +16 -0
  11. package/administrator-documentation/roles-and-permissions.md +65 -0
  12. package/administrator-documentation/track-settings-admin-guide-sketch.md +192 -0
  13. package/administrator-documentation/using-the-application.md +192 -0
  14. package/icons/app.png +0 -0
  15. package/icons/app.svg +1 -0
  16. package/icons/playButton.png +0 -0
  17. package/icons/profilePicThumbnail.png +0 -0
  18. package/jest.config.js +10 -0
  19. package/module-descriptor.json +75 -0
  20. package/output/service-worker.js +0 -0
  21. package/package.json +146 -0
  22. package/src/components/incidents/ColumnChooser.js +37 -0
  23. package/src/components/incidents/CreateMedia.js +132 -0
  24. package/src/components/incidents/CreatePane.js +1215 -0
  25. package/src/components/incidents/CreatePane.test.js +138 -0
  26. package/src/components/incidents/CreateReport.js +102 -0
  27. package/src/components/incidents/DetailsPane.js +1267 -0
  28. package/src/components/incidents/DetailsPane.test.js +150 -0
  29. package/src/components/incidents/EditPane.js +2334 -0
  30. package/src/components/incidents/EditPane.test.js +187 -0
  31. package/src/components/incidents/GetDetails.js +55 -0
  32. package/src/components/incidents/GetListDQLinkIncident.js +81 -0
  33. package/src/components/incidents/GetListDynamicQuery.js +66 -0
  34. package/src/components/incidents/GetLocations.js +57 -0
  35. package/src/components/incidents/GetMedia.js +98 -0
  36. package/src/components/incidents/GetName.js +111 -0
  37. package/src/components/incidents/GetNameCreatedBy.js +94 -0
  38. package/src/components/incidents/GetOrgLocaleSettings.js +61 -0
  39. package/src/components/incidents/GetPatronGroups.js +52 -0
  40. package/src/components/incidents/GetSelf.js +65 -0
  41. package/src/components/incidents/GetSummary.js +110 -0
  42. package/src/components/incidents/IncidentTypeCard.js +53 -0
  43. package/src/components/incidents/IncidentTypeCard.test.js +133 -0
  44. package/src/components/incidents/IncidentsPaneset.js +810 -0
  45. package/src/components/incidents/IncidentsPaneset.test.js +128 -0
  46. package/src/components/incidents/LinkedIncident.js +86 -0
  47. package/src/components/incidents/ModalAddMedia.js +262 -0
  48. package/src/components/incidents/ModalAddMedia.test.js +97 -0
  49. package/src/components/incidents/ModalAttentionDecOfService.js +111 -0
  50. package/src/components/incidents/ModalCustomWitness.js +469 -0
  51. package/src/components/incidents/ModalCustomWitness.test.js +147 -0
  52. package/src/components/incidents/ModalCustomerDetails.js +480 -0
  53. package/src/components/incidents/ModalCustomerDetails.test.js +116 -0
  54. package/src/components/incidents/ModalDescribeCustomer.js +361 -0
  55. package/src/components/incidents/ModalDescribeCustomer.test.js +156 -0
  56. package/src/components/incidents/ModalDirtyFormWarn.js +62 -0
  57. package/src/components/incidents/ModalLinkIncident.js +1213 -0
  58. package/src/components/incidents/ModalLinkIncidentStyle.css +32 -0
  59. package/src/components/incidents/ModalSelectIncidentTypes.js +178 -0
  60. package/src/components/incidents/ModalSelectIncidentTypes.test.js +273 -0
  61. package/src/components/incidents/ModalSelectKnownCustomer.js +395 -0
  62. package/src/components/incidents/ModalSelectWitness.js +406 -0
  63. package/src/components/incidents/ModalSelectWitness.test.js +308 -0
  64. package/src/components/incidents/ModalStyle.css +44 -0
  65. package/src/components/incidents/ModalTrespass.js +741 -0
  66. package/src/components/incidents/ModalViewCustomerDetails.js +241 -0
  67. package/src/components/incidents/ModalViewMedia.js +86 -0
  68. package/src/components/incidents/ModalViewTrespass.js +210 -0
  69. package/src/components/incidents/ResultsPane.js +437 -0
  70. package/src/components/incidents/ResultsPane.test.js +120 -0
  71. package/src/components/incidents/SearchCustomerOrWitness.js +108 -0
  72. package/src/components/incidents/Thumbnail.js +72 -0
  73. package/src/components/incidents/ThumbnailMarkRemoval.js +38 -0
  74. package/src/components/incidents/ThumbnailSkeleton.js +30 -0
  75. package/src/components/incidents/ThumbnailStyles.js +49 -0
  76. package/src/components/incidents/ThumbnailTempPreSave.js +71 -0
  77. package/src/components/incidents/UpdateReport.js +84 -0
  78. package/src/components/incidents/__snapshots__/CreatePane.test.js.snap +3 -0
  79. package/src/components/incidents/__snapshots__/DetailsPane.test.js.snap +3 -0
  80. package/src/components/incidents/__snapshots__/EditPane.test.js.snap +3 -0
  81. package/src/components/incidents/__snapshots__/IncidentTypeCard.test.js.snap +3 -0
  82. package/src/components/incidents/__snapshots__/IncidentsPaneset.test.js.snap +3 -0
  83. package/src/components/incidents/__snapshots__/ModalAddMedia.test.js.snap +3 -0
  84. package/src/components/incidents/__snapshots__/ModalCustomerDetails.test.js.snap +3 -0
  85. package/src/components/incidents/__snapshots__/ModalSelectWitness.test.js.snap +3 -0
  86. package/src/components/incidents/__snapshots__/ResultsPane.test.js.snap +3 -0
  87. package/src/components/incidents/helpers/ProfilePicture/ProfilePicture.css +5 -0
  88. package/src/components/incidents/helpers/ProfilePicture/ProfilePicture.js +51 -0
  89. package/src/components/incidents/helpers/ProfilePicture/isAValidURL.js +3 -0
  90. package/src/components/incidents/helpers/ProfilePicture/useProfilePicture.js +127 -0
  91. package/src/components/incidents/helpers/buildQueryString.js +28 -0
  92. package/src/components/incidents/helpers/cleanFormValues.js +53 -0
  93. package/src/components/incidents/helpers/computeEditedCustomers.js +124 -0
  94. package/src/components/incidents/helpers/convertDateIgnoringTZ.js +8 -0
  95. package/src/components/incidents/helpers/convertUTCISOToLocalePrettyTime.js +15 -0
  96. package/src/components/incidents/helpers/convertUTCISOToPrettyDate.js +19 -0
  97. package/src/components/incidents/helpers/decodeParamsToForm.js +20 -0
  98. package/src/components/incidents/helpers/deepNormalizeForComparison.js +39 -0
  99. package/src/components/incidents/helpers/extractFilterString.js +12 -0
  100. package/src/components/incidents/helpers/formatDateAndTimeToUTCISO.js +14 -0
  101. package/src/components/incidents/helpers/formatDateToUTCISO.js +14 -0
  102. package/src/components/incidents/helpers/formatTimeToUTCISO.js +28 -0
  103. package/src/components/incidents/helpers/getCurrentTime.js +20 -0
  104. package/src/components/incidents/helpers/getTodayDate.js +12 -0
  105. package/src/components/incidents/helpers/handlebarsHelpers.js +148 -0
  106. package/src/components/incidents/helpers/hasFormChangedAtCreate.js +50 -0
  107. package/src/components/incidents/helpers/hasTopLevelChangeAffectedDeclaration.js +90 -0
  108. package/src/components/incidents/helpers/hasTopLevelFormChanged.js +111 -0
  109. package/src/components/incidents/helpers/identifyCurrentTrespassDocs.js +109 -0
  110. package/src/components/incidents/helpers/isSameHtml.js +13 -0
  111. package/src/components/incidents/helpers/isValidDateFormat.js +14 -0
  112. package/src/components/incidents/helpers/isValidTimeInput.js +11 -0
  113. package/src/components/incidents/helpers/isValidUTCTimeFormat.js +14 -0
  114. package/src/components/incidents/helpers/parseMMDDYYYY.js +7 -0
  115. package/src/components/incidents/helpers/parseQueryString.js +16 -0
  116. package/src/components/incidents/helpers/sortTrespassDocuments.js +44 -0
  117. package/src/components/incidents/helpers/stripHTML.js +11 -0
  118. package/src/components/incidents/helpers/trespassDocUtils.js +197 -0
  119. package/src/components/incidents/helpers/validateTrespassDetails.js +37 -0
  120. package/src/components/incidents/usePersistedColModalLink.js +70 -0
  121. package/src/components/incidents/usePersistedColumns.js +70 -0
  122. package/src/components/incidents/usePersistedSort.js +23 -0
  123. package/src/components/incidents/usePersistedSortModalLink.js +23 -0
  124. package/src/contexts/IncidentContext.js +433 -0
  125. package/src/index.js +61 -0
  126. package/src/routes/Application.js +13 -0
  127. package/src/settings/GetIncidentCategories.js +56 -0
  128. package/src/settings/GetIncidentTypesDetails.js +88 -0
  129. package/src/settings/GetIncidentTypesIds.js +74 -0
  130. package/src/settings/GetLocationsInService.js +54 -0
  131. package/src/settings/GetSingleCustomLocationDetails.js +60 -0
  132. package/src/settings/GetSingleIncidentTypeDetails.js +60 -0
  133. package/src/settings/GetTrespassReasons.js +67 -0
  134. package/src/settings/GetTrespassTemplates.js +51 -0
  135. package/src/settings/IncidentCategoriesPane.js +285 -0
  136. package/src/settings/IncidentCategoriesPane.test.js +229 -0
  137. package/src/settings/IncidentTypeDetailsPane.js +215 -0
  138. package/src/settings/IncidentTypeDetailsPane.test.js +220 -0
  139. package/src/settings/IncidentTypeEditPane.js +211 -0
  140. package/src/settings/IncidentTypeEditPane.test.js +170 -0
  141. package/src/settings/IncidentTypesPaneset.js +167 -0
  142. package/src/settings/IncidentTypesPaneset.test.js +124 -0
  143. package/src/settings/LocationInServiceEditPane.js +320 -0
  144. package/src/settings/LocationsPaneset.js +415 -0
  145. package/src/settings/LocationsPaneset.test.js +106 -0
  146. package/src/settings/ModalDeleteCategory.js +47 -0
  147. package/src/settings/ModalDeleteIncidentType.js +49 -0
  148. package/src/settings/ModalDeleteLocationInService.js +49 -0
  149. package/src/settings/ModalDeleteTrespassReason.js +49 -0
  150. package/src/settings/ModalPreviewTrespassDoc.js +65 -0
  151. package/src/settings/ModalTrespassDocTokens.js +83 -0
  152. package/src/settings/NewIncidentTypePane.js +182 -0
  153. package/src/settings/PutIncidentType.js +60 -0
  154. package/src/settings/PutLocationsInService.js +52 -0
  155. package/src/settings/PutTrespassReasons.js +61 -0
  156. package/src/settings/PutTrespassTemplate.js +50 -0
  157. package/src/settings/TrespassDoc.css +17 -0
  158. package/src/settings/TrespassDocDetailsPane.js +215 -0
  159. package/src/settings/TrespassDocEditPane.js +538 -0
  160. package/src/settings/TrespassDocPaneset.js +581 -0
  161. package/src/settings/TrespassReasonDetailsPane.js +171 -0
  162. package/src/settings/TrespassReasonEditPane.js +221 -0
  163. package/src/settings/TrespassReasonsPaneset.js +282 -0
  164. package/src/settings/__snapshots__/IncidentCategoriesPane.test.js.snap +3 -0
  165. package/src/settings/__snapshots__/IncidentTypeDetailsPane.test.js.snap +3 -0
  166. package/src/settings/__snapshots__/IncidentTypeEditPane.test.js.snap +3 -0
  167. package/src/settings/__snapshots__/IncidentTypesPaneset.test.js.snap +3 -0
  168. package/src/settings/__snapshots__/LocationsPaneset.test.js.snap +3 -0
  169. package/src/settings/data/exampleJSON.json +92 -0
  170. package/src/settings/data/templateTokens.js +396 -0
  171. package/src/settings/helpers/alphabetize.js +18 -0
  172. package/src/settings/helpers/getCategoryTitleById.js +13 -0
  173. package/src/settings/helpers/makeId.js +15 -0
  174. package/src/settings/index.js +48 -0
  175. package/stripes.config.js +10 -0
  176. package/test/jest/__mock__/index.js +8 -0
  177. package/test/jest/__mock__/intl.mock.js +27 -0
  178. package/test/jest/__mock__/stripes.mock.js +26 -0
  179. package/test/jest/__mock__/stripesComponents.mock.js +151 -0
  180. package/test/jest/__mock__/stripesConfig.mock.js +1 -0
  181. package/test/jest/__mock__/stripesCore.mock.js +9 -0
  182. package/test/jest/__mock__/stripesIcon.mock.js +5 -0
  183. package/test/jest/__mock__/stripesSmartComponents.mock.js +7 -0
  184. package/test/jest/__mock__/stripesUtils.mock.js +3 -0
  185. package/test/jest/eslintrc.js +12 -0
  186. package/test/jest/setupFiles.js +5 -0
  187. package/translations/ui-security-incident/en_US.json +542 -0
  188. package/ui-module-acceptance-criteria.md +34 -0
@@ -0,0 +1,480 @@
1
+ import React, { useState, useEffect, useMemo } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormattedMessage } from 'react-intl';
4
+ import DOMPurify from 'dompurify';
5
+ import {
6
+ AccordionSet,
7
+ Accordion,
8
+ Button,
9
+ Col,
10
+ Datepicker,
11
+ Editor,
12
+ Modal,
13
+ ModalFooter,
14
+ Pane,
15
+ Paneset,
16
+ Row,
17
+ TextField,
18
+ } from '@folio/stripes/components';
19
+ import { useIncidents } from '../../contexts/IncidentContext';
20
+ import isValidDateFormat from './helpers/isValidDateFormat';
21
+ import stripHTML from './helpers/stripHTML';
22
+ import css from './ModalStyle.css';
23
+
24
+ const ModalCustomerDetails = ({
25
+ customerID,
26
+ allCustomers, //merged array from EditPane context [...selectedCustomers, formData.customers(instance custs)]
27
+ setAllCustomers // setter for allCustomers
28
+ }) => {
29
+ const {
30
+ isModalCustomerDetails,
31
+ closeModalCustomerDetails,
32
+ selectedCustomers, // customers array from CreatePane context
33
+ setSelectedCustomers, // setter for selectedCustomers
34
+ } = useIncidents();
35
+
36
+ const [customersArray, setCustomersArray] = useState([]);
37
+ const [workWithEdit, setWorkWithEdit] = useState(false);
38
+ const [isCustomerRegistered, setIsCustomerRegistered] = useState(true);
39
+ const [unregisteredNames, setUnregisteredNames] = useState({
40
+ firstName: '',
41
+ lastName: ''
42
+ });
43
+ const [useNames, setUseNames] = useState(false);
44
+ const [localCustomerDescription, setLocalCustomerDescription] = useState('');
45
+ const [localCustomerDetailsData, setLocalCustomerDetailsData] = useState({
46
+ sex: '',
47
+ race: '',
48
+ height: '',
49
+ weight: '',
50
+ hair: '',
51
+ eyes: '',
52
+ dateOfBirth: '',
53
+ streetAddress: '',
54
+ city: '',
55
+ state: '',
56
+ zipcode: '',
57
+ });
58
+
59
+ // set local customersArray based on context use
60
+ useEffect(() => {
61
+ if (allCustomers) {
62
+ // setting if rendered via @EditPane
63
+ setWorkWithEdit(true);
64
+ setCustomersArray(allCustomers);
65
+ } else if (selectedCustomers) {
66
+ // setting of rendered via @CreatePane
67
+ setCustomersArray(selectedCustomers);
68
+ }
69
+ }, [selectedCustomers, allCustomers]);
70
+
71
+ // populate modal state with current customer data
72
+ useEffect(() => {
73
+ const currentCustomer = customersArray.find(
74
+ (cust) => cust.id === customerID
75
+ );
76
+
77
+ if (currentCustomer && currentCustomer.details) {
78
+ setLocalCustomerDetailsData({
79
+ sex: currentCustomer.details?.sex || '',
80
+ race: currentCustomer.details?.race || '',
81
+ height: currentCustomer.details?.height || '',
82
+ weight: currentCustomer.details?.weight || '',
83
+ hair: currentCustomer.details?.hair || '',
84
+ eyes: currentCustomer.details?.eyes || '',
85
+ dateOfBirth: currentCustomer.details?.dateOfBirth || '',
86
+ streetAddress: currentCustomer.details?.streetAddress || '',
87
+ city: currentCustomer.details?.city || '',
88
+ state: currentCustomer.details?.state || '',
89
+ zipcode: currentCustomer.details?.zipcode || '',
90
+ });
91
+ } else {
92
+ setLocalCustomerDetailsData({
93
+ sex: '',
94
+ race: '',
95
+ height: '',
96
+ weight: '',
97
+ hair: '',
98
+ eyes: '',
99
+ dateOfBirth: '',
100
+ streetAddress: '',
101
+ city: '',
102
+ state: '',
103
+ zipcode: '',
104
+ });
105
+ };
106
+
107
+ if (currentCustomer && currentCustomer.description) {
108
+ setLocalCustomerDescription(currentCustomer.description || '');
109
+ } else {
110
+ setLocalCustomerDescription('');
111
+ };
112
+
113
+ if (currentCustomer && currentCustomer.registered === false) {
114
+ setIsCustomerRegistered(false) // setter for no require customer 'description'
115
+ // console.log("is NOT registered block ran")
116
+ setUseNames(true) //if not registered, use instance name values and not associated key name
117
+ setUnregisteredNames({
118
+ firstName: currentCustomer.firstName,
119
+ lastName: currentCustomer.lastName
120
+ });
121
+ } else {
122
+ // console.log("IS registered block ran")
123
+ setIsCustomerRegistered(true)
124
+ setUseNames(false); // hide the name fields if registered
125
+ setUnregisteredNames({
126
+ firstName: '',
127
+ lastName: '',
128
+ });
129
+ };
130
+ }, [isModalCustomerDetails, customerID, customersArray]);
131
+
132
+
133
+ const handleDetailsChange = (event) => {
134
+ const { name, value } = event.target;
135
+ setLocalCustomerDetailsData((prevData) => ({
136
+ ...prevData,
137
+ [name]: value,
138
+ }));
139
+ };
140
+
141
+ const handleDescriptionChange = (content) => {
142
+ const sanitizedContent = DOMPurify.sanitize(content);
143
+ setLocalCustomerDescription(sanitizedContent);
144
+ };
145
+
146
+ const isFormValid = () => {
147
+ const isDateOfBirthValid = localCustomerDetailsData.dateOfBirth === '' ||
148
+ isValidDateFormat(localCustomerDetailsData.dateOfBirth);
149
+
150
+ const isEditorValid = isCustomerRegistered || (localCustomerDescription && stripHTML(localCustomerDescription));
151
+
152
+ return isDateOfBirthValid && isEditorValid;
153
+ };
154
+
155
+ const handleUnregisteredNames = (event) => {
156
+ const { name, value } = event.target;
157
+ setUnregisteredNames((prevData) => ({
158
+ ...prevData,
159
+ [name]: value,
160
+ }))
161
+ };
162
+
163
+ const normalizeDescription = (html) => {
164
+ const raw = (html || '').trim();
165
+ return stripHTML(raw) ? raw : undefined;
166
+ };
167
+
168
+ const handleSave = () => {
169
+ const updatedCustomerArray = customersArray.map((cust) => {
170
+ if (cust.id === customerID) {
171
+ const cleanedDetails = Object.entries(localCustomerDetailsData).reduce((acc, [k, v]) => {
172
+ const trimmed = typeof v === 'string' ? v.trim() : v;
173
+ if (trimmed !== '' && trimmed !== null && trimmed !== undefined) {
174
+ acc[k] = trimmed;
175
+ }
176
+ return acc;
177
+ }, {});
178
+
179
+ // only include details if it has non-empty fields
180
+ const hasDetails = Object.keys(cleanedDetails).length > 0;
181
+
182
+ const updatedCustomer = {
183
+ ...cust,
184
+ firstName: useNames ? unregisteredNames.firstName : cust.firstName,
185
+ lastName: useNames ? unregisteredNames.lastName : cust.lastName,
186
+ ...(hasDetails ? { details: cleanedDetails } : {})
187
+ };
188
+
189
+ const normalizeDesc = normalizeDescription(localCustomerDescription);
190
+ if (normalizeDesc) {
191
+ updatedCustomer.description = normalizeDesc;
192
+ } else {
193
+ delete updatedCustomer.description
194
+ };
195
+
196
+ if (!hasDetails && 'details' in updatedCustomer) {
197
+ delete updatedCustomer.details;
198
+ };
199
+
200
+ return updatedCustomer;
201
+ };
202
+ return cust;
203
+ });
204
+
205
+ if (workWithEdit) {
206
+ // setting via EditPane
207
+ setAllCustomers(updatedCustomerArray);
208
+ } else {
209
+ // setting via CreatePane, using setSelectedCustomers
210
+ setSelectedCustomers(updatedCustomerArray);
211
+ };
212
+
213
+ resetModalState();
214
+ closeModalCustomerDetails();
215
+ };
216
+
217
+ const resetModalState = () => {
218
+ setLocalCustomerDescription('');
219
+ setLocalCustomerDetailsData({
220
+ sex: '',
221
+ race: '',
222
+ height: '',
223
+ weight: '',
224
+ hair: '',
225
+ eyes: '',
226
+ dateOfBirth: '',
227
+ streetAddress: '',
228
+ city: '',
229
+ state: '',
230
+ zipcode: '',
231
+ });
232
+
233
+ setUnregisteredNames({ firstName: '', lastName: '' });
234
+ setWorkWithEdit(false);
235
+ setUseNames(false);
236
+ setIsCustomerRegistered(true);
237
+ };
238
+
239
+ const handleCloseDismiss = () => {
240
+ setIsCustomerRegistered(true);
241
+ resetModalState();
242
+ closeModalCustomerDetails();
243
+ };
244
+
245
+ if (!isModalCustomerDetails) {
246
+ return null;
247
+ };
248
+
249
+ const editorModules = {
250
+ toolbar: [
251
+ [{ 'header': [1, 2, false] }],
252
+ ['bold', 'italic', 'underline'],
253
+ ],
254
+ };
255
+
256
+ const footer = (
257
+ <ModalFooter>
258
+ <Button
259
+ onClick={handleSave}
260
+ buttonStyle="primary"
261
+ marginBottom0
262
+ disabled={!isFormValid()}
263
+ >
264
+ <FormattedMessage id="close-continue-button" />
265
+ </Button>
266
+ <Button onClick={handleCloseDismiss}>
267
+ <FormattedMessage id="cancel-button" />
268
+ </Button>
269
+ </ModalFooter>
270
+ );
271
+
272
+ return (
273
+ <Modal
274
+ style={{
275
+ minHeight: '250px',
276
+ height: '80%', // allows modal to grow/shrink based on content
277
+ maxHeight: '300vh',
278
+ maxWidth: '300vw', // modal width responsive to viewport width
279
+ width: '70%' // modal width adjusts based on content and window size
280
+ }}
281
+ open
282
+ dismissible
283
+ onClose={handleCloseDismiss}
284
+ closeOnBackgroundClick
285
+ label={<FormattedMessage id="modal-customer-details-label" />}
286
+ size="large"
287
+ footer={footer}
288
+ contentClass={css.modalContent}
289
+ >
290
+ <Paneset>
291
+ <Pane defaultWidth="100%">
292
+ <AccordionSet>
293
+ <Accordion
294
+ label={
295
+ <FormattedMessage id="modal-customer-details.accordion-label-customer-description" />
296
+ }
297
+ >
298
+ {useNames && (
299
+ <>
300
+ <Row>
301
+ <Col xs={3}>
302
+ <TextField
303
+ onChange={handleUnregisteredNames}
304
+ value={unregisteredNames.firstName}
305
+ name="firstName"
306
+ label={
307
+ <FormattedMessage id="modal-customer-details.textField-first-name-label" />
308
+ }
309
+ />
310
+
311
+ </Col>
312
+ </Row>
313
+ <Row>
314
+ <Col xs={3}>
315
+ <TextField
316
+ onChange={handleUnregisteredNames}
317
+ value={unregisteredNames.lastName}
318
+ name="lastName"
319
+ label={
320
+ <FormattedMessage id="modal-customer-details.textField-last-name-label" />
321
+ }
322
+ />
323
+ </Col>
324
+ </Row>
325
+ </>
326
+ )}
327
+
328
+ <Row>
329
+ <Col xs={6}>
330
+ <Editor
331
+ required={!isCustomerRegistered}
332
+ label={
333
+ <FormattedMessage id="modal-customer-details.textArea-description-of-customer-label" />
334
+ }
335
+ value={localCustomerDescription}
336
+ onChange={handleDescriptionChange}
337
+ modules={editorModules}
338
+ />
339
+ </Col>
340
+ </Row>
341
+ </Accordion>
342
+ <Accordion
343
+ label={
344
+ <FormattedMessage id="modal-customer-details.accordion-identity-details"/>
345
+ }
346
+ >
347
+ <Row>
348
+ <Col xs={2}>
349
+ <TextField
350
+ label={<FormattedMessage id="modal-customer-details-sex" />}
351
+ name="sex"
352
+ value={localCustomerDetailsData.sex}
353
+ onChange={handleDetailsChange}
354
+ />
355
+ </Col>
356
+ <Col xs={2}>
357
+ <TextField
358
+ label={
359
+ <FormattedMessage id="modal-customer-details-race" />
360
+ }
361
+ name="race"
362
+ value={localCustomerDetailsData.race}
363
+ onChange={handleDetailsChange}
364
+ />
365
+ </Col>
366
+ <Col xs={2}>
367
+ <TextField
368
+ label={
369
+ <FormattedMessage id="modal-customer-details-height" />
370
+ }
371
+ name="height"
372
+ value={localCustomerDetailsData.height}
373
+ onChange={handleDetailsChange}
374
+ />
375
+ </Col>
376
+ </Row>
377
+
378
+ <Row>
379
+ <Col xs={2}>
380
+ <TextField
381
+ label={
382
+ <FormattedMessage id="modal-customer-details-weight" />
383
+ }
384
+ name="weight"
385
+ value={localCustomerDetailsData.weight}
386
+ onChange={handleDetailsChange}
387
+ />
388
+ </Col>
389
+ <Col xs={2}>
390
+ <TextField
391
+ label={
392
+ <FormattedMessage id="modal-customer-details-hair" />
393
+ }
394
+ name="hair"
395
+ value={localCustomerDetailsData.hair}
396
+ onChange={handleDetailsChange}
397
+ />
398
+ </Col>
399
+ <Col xs={2}>
400
+ <TextField
401
+ label={
402
+ <FormattedMessage id="modal-customer-details-eyes" />
403
+ }
404
+ name="eyes"
405
+ value={localCustomerDetailsData.eyes}
406
+ onChange={handleDetailsChange}
407
+ />
408
+ </Col>
409
+ </Row>
410
+ <Row>
411
+ <Col xs={3}>
412
+ <Datepicker
413
+ label={
414
+ <FormattedMessage id="modal-customer-details-date-of-birth" />
415
+ }
416
+ name="dateOfBirth"
417
+ value={localCustomerDetailsData.dateOfBirth}
418
+ onChange={handleDetailsChange}
419
+ />
420
+ </Col>
421
+ </Row>
422
+ <Row>
423
+ <Col xs={4}>
424
+ <TextField
425
+ label={
426
+ <FormattedMessage id="modal-customer-details-street-address" />
427
+ }
428
+ name="streetAddress"
429
+ value={localCustomerDetailsData.streetAddress}
430
+ onChange={handleDetailsChange}
431
+ />
432
+ </Col>
433
+ <Col xs={2}>
434
+ <TextField
435
+ label={
436
+ <FormattedMessage id="modal-customer-details-city" />
437
+ }
438
+ name="city"
439
+ value={localCustomerDetailsData.city}
440
+ onChange={handleDetailsChange}
441
+ />
442
+ </Col>
443
+ </Row>
444
+ <Row style={{ marginBottom: '100px' }}>
445
+ <Col xs={2}>
446
+ <TextField
447
+ label={
448
+ <FormattedMessage id="modal-customer-details-state" />
449
+ }
450
+ name="state"
451
+ value={localCustomerDetailsData.state}
452
+ onChange={handleDetailsChange}
453
+ />
454
+ </Col>
455
+ <Col xs={2}>
456
+ <TextField
457
+ label={
458
+ <FormattedMessage id="modal-customer-details-zipcode" />
459
+ }
460
+ name="zipcode"
461
+ value={localCustomerDetailsData.zipcode}
462
+ onChange={handleDetailsChange}
463
+ />
464
+ </Col>
465
+ </Row>
466
+ </Accordion>
467
+ </AccordionSet>
468
+ </Pane>
469
+ </Paneset>
470
+ </Modal>
471
+ );
472
+ };
473
+
474
+ ModalCustomerDetails.propTypes = {
475
+ customerID: PropTypes.string.isRequired,
476
+ customersList: PropTypes.arrayOf(PropTypes.object).isRequired,
477
+ setCustomersList: PropTypes.func.isRequired,
478
+ };
479
+
480
+ export default ModalCustomerDetails;
@@ -0,0 +1,116 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom';
3
+ import { act } from 'react-dom/test-utils';
4
+ import ModalCustomerDetails from './ModalCustomerDetails';
5
+
6
+ // --- Mocks for External Dependencies ---
7
+
8
+ // Mock react-intl: FormattedMessage renders the id.
9
+ jest.mock('react-intl', () => ({
10
+ FormattedMessage: (props) => <span>{props.id}</span>,
11
+ }));
12
+
13
+ // Mock @folio/stripes/components with minimal implementations.
14
+ jest.mock('@folio/stripes/components', () => {
15
+ const React = require('react');
16
+ return {
17
+ AccordionSet: (props) => <div {...props}>{props.children}</div>,
18
+ Accordion: (props) => <div {...props}>{props.children}</div>,
19
+ Button: (props) => <button {...props}>{props.children}</button>,
20
+ Col: (props) => <div {...props}>{props.children}</div>,
21
+ Datepicker: (props) => <input type="date" {...props} />,
22
+ Editor: (props) => <textarea {...props} />,
23
+ Modal: (props) => <div {...props}>{props.children}</div>,
24
+ ModalFooter: (props) => <div {...props}>{props.children}</div>,
25
+ Paneset: (props) => <div {...props}>{props.children}</div>,
26
+ Pane: (props) => <div {...props}>{props.children}</div>,
27
+ Row: (props) => <div {...props}>{props.children}</div>,
28
+ TextField: (props) => <input type="text" {...props} />,
29
+ };
30
+ });
31
+
32
+ // Mock helper functions.
33
+ jest.mock('./helpers/isValidDateFormat', () => jest.fn(() => true));
34
+ jest.mock('./helpers/stripHTML', () => jest.fn((str) => str));
35
+
36
+ // --- Mocks for the Incident Context ---
37
+ // Define a stable context object inside the module factory to avoid re-render loops.
38
+ jest.mock('../../contexts/IncidentContext', () => {
39
+ const stableContext = {
40
+ isModalCustomerDetails: true, // modal is open for our test
41
+ closeModalCustomerDetails: jest.fn(),
42
+ selectedCustomers: [
43
+ { id: 'cust-1', firstName: 'John', lastName: 'Doe', registered: false }
44
+ ],
45
+ setSelectedCustomers: jest.fn(),
46
+ };
47
+ return {
48
+ useIncidents: () => stableContext,
49
+ };
50
+ });
51
+
52
+ // --- Test Props ---
53
+ // Even though the PropTypes define customersList and setCustomersList,
54
+ // the component destructures props as { customerID, allCustomers, setAllCustomers }.
55
+ // Here we supply sample props for testing.
56
+ const sampleCustomer = {
57
+ id: 'cust-1',
58
+ firstName: 'John',
59
+ lastName: 'Doe',
60
+ registered: false,
61
+ details: {
62
+ sex: '',
63
+ race: '',
64
+ height: '',
65
+ weight: '',
66
+ hair: '',
67
+ eyes: '',
68
+ dateOfBirth: '',
69
+ streetAddress: '',
70
+ city: '',
71
+ state: '',
72
+ zipcode: '',
73
+ },
74
+ description: 'Test description',
75
+ };
76
+
77
+ const sampleProps = {
78
+ customerID: 'cust-1',
79
+ allCustomers: [sampleCustomer],
80
+ setAllCustomers: jest.fn(),
81
+ };
82
+
83
+ // --- Test Setup ---
84
+ let container = null;
85
+ beforeEach(() => {
86
+ container = document.createElement('div');
87
+ document.body.appendChild(container);
88
+ });
89
+ afterEach(() => {
90
+ document.body.removeChild(container);
91
+ container = null;
92
+ });
93
+
94
+ // --- Test Cases ---
95
+
96
+ // Test case 1: When isModalCustomerDetails is true, the modal renders.
97
+ test('ModalCustomerDetails renders correctly when open (snapshot test)', () => {
98
+ act(() => {
99
+ ReactDOM.render(<ModalCustomerDetails {...sampleProps} />, container);
100
+ });
101
+ expect(container.innerHTML).toMatchSnapshot();
102
+ });
103
+
104
+ // Test case 2: When isModalCustomerDetails is false, the component returns null.
105
+ test('ModalCustomerDetails returns null when closed', () => {
106
+ // Retrieve the stable context and set isModalCustomerDetails to false.
107
+ const { useIncidents } = require('../../contexts/IncidentContext');
108
+ const stableContext = useIncidents();
109
+ stableContext.isModalCustomerDetails = false; // simulate modal closed
110
+
111
+ act(() => {
112
+ ReactDOM.render(<ModalCustomerDetails {...sampleProps} />, container);
113
+ });
114
+ // If the modal is closed, the component should render nothing.
115
+ expect(container.innerHTML).toBe('');
116
+ });