@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,2334 @@
1
+ import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
2
+ import { useIntl, FormattedMessage } from 'react-intl';
3
+ import { useParams, useHistory, useLocation } from 'react-router-dom';
4
+ import DOMPurify from 'dompurify';
5
+ import {
6
+ Accordion,
7
+ AccordionSet,
8
+ Button,
9
+ Checkbox,
10
+ Col,
11
+ Datepicker,
12
+ Editor,
13
+ ExpandAllButton,
14
+ Icon,
15
+ KeyValue,
16
+ Label,
17
+ List,
18
+ LoadingPane,
19
+ MessageBanner,
20
+ MetaSection,
21
+ Pane,
22
+ PaneHeader,
23
+ PaneFooter,
24
+ Row,
25
+ Select,
26
+ Timepicker,
27
+ } from '@folio/stripes/components';
28
+ import GetLocationsInService from '../../settings/GetLocationsInService';
29
+ import GetIncidentTypesDetails from '../../settings/GetIncidentTypesDetails';
30
+ import ModalSelectKnownCustomer from './ModalSelectKnownCustomer';
31
+ import ModalDescribeCustomer from './ModalDescribeCustomer';
32
+ import ModalSelectIncidentTypes from './ModalSelectIncidentTypes';
33
+ import ModalSelectWitness from './ModalSelectWitness';
34
+ import ModalTrespass from './ModalTrespass';
35
+ import ModalCustomerDetails from './ModalCustomerDetails';
36
+ import ModalAddMedia from './ModalAddMedia';
37
+ import CreateMedia from './CreateMedia';
38
+ import parseMMDDYYYY from './helpers/parseMMDDYYYY';
39
+ import convertUTCISOToPrettyDate from './helpers/convertUTCISOToPrettyDate';
40
+ import convertUTCISOToLocalePrettyTime from './helpers/convertUTCISOToLocalePrettyTime';
41
+ // formats to local date at midnight and one second in UTC ISO:
42
+ import formatDateToUTCISO from './helpers/formatDateToUTCISO';
43
+ import formatDateAndTimeToUTCISO from './helpers/formatDateAndTimeToUTCISO';
44
+ import isValidDateFormat from './helpers/isValidDateFormat';
45
+ import isValidTimeInput from './helpers/isValidTimeInput';
46
+ import stripHTML from './helpers/stripHTML';
47
+ import getTodayDate from './helpers/getTodayDate';
48
+ import { isSameHtml } from './helpers/isSameHtml.js';
49
+ import GetDetails from './GetDetails';
50
+ import GetSelf from './GetSelf';
51
+ import GetMedia from './GetMedia';
52
+ import GetName from './GetName';
53
+ import GetNameCreatedBy from './GetNameCreatedBy';
54
+ import identifyCurrentTrespassDocs from './helpers/identifyCurrentTrespassDocs';
55
+ import Thumbnail from './Thumbnail';
56
+ import ThumbnailSkeleton from './ThumbnailSkeleton';
57
+ import ThumbnailMarkRemoval from './ThumbnailMarkRemoval';
58
+ import ThumbnailTempPreSave from './ThumbnailTempPreSave';
59
+ import UpdateReport from './UpdateReport';
60
+ import makeId from '../../settings/helpers/makeId';
61
+ import ModalCustomWitness from './ModalCustomWitness';
62
+ import GetTrespassTemplates from '../../settings/GetTrespassTemplates';
63
+ import GetTrespassReasons from '../../settings/GetTrespassReasons';
64
+ import hasTopLevelFormChanged from './helpers/hasTopLevelFormChanged.js';
65
+ import hasTopLevelChangeAffectedDeclaration from './helpers/hasTopLevelChangeAffectedDeclaration.js';
66
+ import computeEditedCustomers from './helpers/computeEditedCustomers.js';
67
+ import sortTrespassDocuments from './helpers/sortTrespassDocuments.js';
68
+ import ModalDirtyFormWarn from './ModalDirtyFormWarn.js';
69
+ import ModalAttentionDecOfService from './ModalAttentionDecOfService.js';
70
+ import ModalLinkIncident from './ModalLinkIncident.js';
71
+ import GetSummary from './GetSummary.js';
72
+ import LinkedIncident from './LinkedIncident.js';
73
+ import { useIncidents } from '../../contexts/IncidentContext';
74
+ import {
75
+ generateTrespassDocumentsAtEdit,
76
+ generatePDFAttachments
77
+ } from './helpers/trespassDocUtils.js';
78
+
79
+ const EditPane = () => {
80
+ const history = useHistory();
81
+ const intl = useIntl();
82
+ const location = useLocation();
83
+ const searchRef = useRef(location.search);
84
+ const draftRef = useRef(''); // local unsanitized buffer - doesn't trigger React re-renders
85
+ const {
86
+ singleIncident,
87
+ closeEditPane,
88
+ openModalSelectTypes,
89
+ openModalUnknownCust,
90
+ openModalSelectKnownCust,
91
+ selectedCustomers, // customers selected for saving
92
+ setSelectedCustomers,
93
+ selectedWitnesses,
94
+ setSelectedWitnesses,
95
+ openModalSelectWitness,
96
+ self,
97
+ openModalTrespass,
98
+ isLoadingDetails,
99
+ openLoadingDetails,
100
+ isUpdatingReport,
101
+ setIsUpdatingReport,
102
+ openModalMedia,
103
+ setAttachmentsData, // UpdateReport passes attachmentsData to CreateMedia
104
+ idForMediaCreate,
105
+ setIdForMediaCreate,
106
+ formDataArrayForMediaCreate,
107
+ setFormDataArrayForMediaCreate,
108
+ openModalCustomerDetails,
109
+ locationsInService,
110
+ incidentTypesList,
111
+ isImageArrayLoading,
112
+ openImageSkeleton, // set isImageArrayLoading to true
113
+ // closeImageSkeleton, // set isImageArrayLoading to false
114
+ openModalCustomWitness,
115
+ trespassTemplates,
116
+ triggerDocumentError,
117
+ trespassReasons
118
+ } = useIncidents();
119
+
120
+ const { id } = useParams();
121
+ useEffect(() => {
122
+ if (id) {
123
+ openLoadingDetails();
124
+ }
125
+ }, [id]);
126
+
127
+ const allowedReasonsById = useMemo(() => {
128
+ const list = (trespassReasons ?? []).filter(r => !r.isSuppressed);
129
+ return new Map(list.map(r => [r.id, r]));
130
+ }, [trespassReasons])
131
+
132
+ const defaultReason = useMemo(() => {
133
+ const r = (trespassReasons ?? []).find(x => x.isDefault && !x.isSuppressed);
134
+ return r ? { id: r.id, reason: r.reason } : null;
135
+ }, [trespassReasons]);
136
+
137
+ /**
138
+ * - sanitizeReasons()
139
+ * - leveraged on any customer that is getting 'update declaration'
140
+ * - removes suppressed/deleted reasons (not in allowedReasonsById)
141
+ * - normalizes label to the current configs reason text
142
+ * - dedupes by id
143
+ * - optionally inserts a default reason if we stripped everything (shouldn't happen via UI process, but covering dark corner case...)
144
+ * AND the customer used to have at least one reason.
145
+ */
146
+ const sanitizeReasons = useCallback((arr, allowedMap) => {
147
+ const seen = new Set();
148
+ const out = [];
149
+ for (const item of (arr || [])) {
150
+ const id = typeof item === 'string' ? item : item?.id;
151
+ if (!id) continue;
152
+
153
+ const allowed = allowedMap.get(id); // undefined => suppressed or deleted
154
+ if (!allowed) continue;
155
+
156
+ if (seen.has(id)) continue;
157
+ seen.add(id);
158
+
159
+ // normalize to current wording in settings
160
+ out.push({ id, reason: allowed.reason });
161
+ };
162
+
163
+ return { sanitized: out, hadAnyBefore: (arr?.length ?? 0) > 0 };
164
+ }, []);
165
+
166
+ const [showModalLinkIncident, setShowModalLinkIncident] = useState(false);
167
+ const [linkedToSummaries, setLinkedToSummaries] = useState([]); // summaries built from 'allLinkedTo' via 'idsArray' memo
168
+ const [allLinkedTo, setAllLinkedTo] = useState(() => new Set()); // merging of record/selected
169
+ const [trespassCustomerID, setTrespassCustomerID] = useState(null);
170
+ const [detailsCustomerID, setDetailsCustomerID] = useState(null);
171
+ const [putData, setPutData] = useState({});
172
+ const [subLocationsDataOptions, setSubLocationsDataOptions] = useState([]);
173
+ const [unsavedMediaArray, setUnsavedMediaArray] = useState([]);
174
+ const [mediaArray, setMediaArray] = useState([]);
175
+ const [documents, setDocuments] = useState([]);
176
+ const [mostCurrentTrespassDocIds, setMostCurrentTrespassDocIds] = useState([]);
177
+ const [mediaSrc, setMediaSrc] = useState({});
178
+ const [loadingStatus, setLoadingStatus] = useState({});
179
+ const [removedCustomerIds, setRemovedCustomerIds] = useState([]);
180
+ const [removedWitnessIds, setRemovedWitnessIds] = useState([]);
181
+ const [associatedKeyCustArray, setAssociatedKeyCustArray] = useState([]);
182
+ const [associatedKeyWitArray, setAssociatedKeyWitArray] = useState([]);
183
+ const [allWitnesses, setAllWitnesses] = useState([]);
184
+ const [custWitEditObj, setCustWitEditObj] = useState({});
185
+ const [custWitEditID, setCustWitEditID] = useState('');
186
+ const [allCustomers, setAllCustomers] = useState([]);
187
+ const [isNoCustomer, setIsNoCustomer] = useState(false);
188
+ const [trespassTemplate, setTrespassTemplate] = useState('');
189
+ const [createdById, setCreatedById] = useState('');
190
+ const [updatedById, setUpdatedById] = useState('');
191
+ const [missingUsers, setMissingUsers] = useState([]);
192
+ const [stagedTrespassMap, setStagedTrespassMap] = useState(new Map()); // trespass objects set to be updated
193
+ const [showDirtyFormModal, setShowDirtyFormModal] = useState(false);
194
+ const [customersToUpdateDeclaration, setCustomersToUpdateDeclaration] = useState([]); // customers that have UI 'Update declaration' checked
195
+ const [isModalAttentionDecOfService, setIsModalAttentionDecOfService] = useState(false);
196
+ const [customersWithoutDeclaration, setCustomersWithoutDeclaration] = useState(new Set()); // persisted data customers with no declaration of service
197
+ const [originalDeclarationCustomerIds, setOriginalDeclarationCustomerIds] = useState(new Set()); // persisted data customers that have a declaration of service
198
+ const [isHydrated, setIsHydrated] = useState(false);
199
+ const [createdByForRender, setCreatedByForRender] = useState({
200
+ id: '',
201
+ barcode: '',
202
+ firstName: '',
203
+ lastName: ''
204
+ });
205
+ const [updatedByForRender, setUpdatedByForRender] = useState({
206
+ id: '',
207
+ barcode: '',
208
+ firstName: '',
209
+ lastName: ''
210
+ });
211
+ const [formData, setFormData] = useState({
212
+ customerNa: false,
213
+ customers: [],
214
+ incidentLocation: '',
215
+ subLocation: '',
216
+ dateTimeOfIncident: '', // feeds timeOfIncident key
217
+ timeOfIncident: '', // ui only key, does not persist in db
218
+ isApproximateTime: false,
219
+ detailedDescriptionOfIncident: '',
220
+ incidentWitnesses: [],
221
+ incidentTypes: [],
222
+ attachments: [],
223
+ id: '',
224
+ metadata: {},
225
+ staffSuppressed: undefined,
226
+ linkedTo: [],
227
+ });
228
+
229
+ const defaultFormData = {
230
+ customerNa: false,
231
+ customers: [],
232
+ incidentLocation: '',
233
+ subLocation: '',
234
+ dateTimeOfIncident: '',
235
+ timeOfIncident: '',
236
+ isApproximateTime: false,
237
+ detailedDescriptionOfIncident: '',
238
+ incidentWitnesses: [],
239
+ incidentTypes: [],
240
+ attachments: [],
241
+ id: '',
242
+ metadata: {},
243
+ staffSuppressed: false,
244
+ linkedTo: [],
245
+ };
246
+
247
+ const initialFormData = useMemo(() => {
248
+ if (!singleIncident || Object.keys(singleIncident).length === 0) {
249
+ return defaultFormData;
250
+ };
251
+ return {
252
+ customerNa: singleIncident?.customerNa,
253
+ customers: singleIncident?.customers || [],
254
+ incidentLocation: singleIncident.incidentLocation || '',
255
+ subLocation: singleIncident.subLocation || '',
256
+ dateTimeOfIncident: convertUTCISOToPrettyDate(singleIncident.dateTimeOfIncident) || '',
257
+ timeOfIncident: convertUTCISOToLocalePrettyTime(singleIncident.dateTimeOfIncident) || '',
258
+ isApproximateTime: singleIncident.isApproximateTime || false,
259
+ detailedDescriptionOfIncident: singleIncident.detailedDescriptionOfIncident || '',
260
+ incidentWitnesses: singleIncident?.incidentWitnesses || [],
261
+ incidentTypes: [...(singleIncident.incidentTypes || [])],
262
+ attachments: singleIncident.attachments || [],
263
+ id: singleIncident.id || '',
264
+ metadata: singleIncident.metadata,
265
+ staffSuppressed: singleIncident?.staffSuppressed || false,
266
+ linkedTo: singleIncident?.linkedTo || [],
267
+ };
268
+ }, [singleIncident]);
269
+
270
+ // keep track of which incident has been hydrated.
271
+ // prevents resetting formData every time initialFormData changes identity
272
+ // (e.g. modal openings). we only hydrate when incident ID
273
+ // changes, not when props produce a new object reference for the useEffect
274
+ const currentId = singleIncident?.id || '';
275
+ const hydratedIdRef = useRef(null);
276
+ useEffect(() => {
277
+ if (hydratedIdRef.current !== currentId) {
278
+ setFormData(initialFormData);
279
+ setAllLinkedTo(new Set((initialFormData.linkedTo || []).map(String)));
280
+ setIsHydrated(true);
281
+ hydratedIdRef.current = currentId;
282
+ }
283
+ }, [currentId]);
284
+
285
+ const idsArray = useMemo(
286
+ () => Array.from(allLinkedTo).sort(),
287
+ [allLinkedTo]
288
+ );
289
+
290
+ useEffect(() => {
291
+ if (singleIncident?.customers?.length) {
292
+ const ids = singleIncident.customers
293
+ .filter(cust => !cust.trespass?.declarationOfService)
294
+ .map(cust => cust.id);
295
+
296
+ setCustomersWithoutDeclaration(new Set(ids));
297
+ }
298
+ }, [singleIncident]);
299
+
300
+ useEffect(() => {
301
+ if (singleIncident?.customers?.length) {
302
+ const ids = singleIncident.customers
303
+ .filter(cust => cust.trespass?.declarationOfService)
304
+ .map(cust => cust.id);
305
+
306
+ setOriginalDeclarationCustomerIds(new Set(ids));
307
+ }
308
+ }, [singleIncident]);
309
+
310
+ const staffSuppressedIsDirty = useMemo(
311
+ () => isHydrated && (formData.staffSuppressed !== initialFormData.staffSuppressed),
312
+ [isHydrated, formData.staffSuppressed, initialFormData.staffSuppressed]
313
+ );
314
+
315
+ const topLevelIsDirty = useMemo(() => {
316
+ if (!isHydrated) return;
317
+ return hasTopLevelFormChanged(
318
+ formData,
319
+ initialFormData,
320
+ selectedCustomers,
321
+ selectedWitnesses,
322
+ unsavedMediaArray,
323
+ customersToUpdateDeclaration
324
+ );
325
+ }, [
326
+ isHydrated,
327
+ formData,
328
+ initialFormData,
329
+ selectedCustomers,
330
+ selectedWitnesses,
331
+ unsavedMediaArray,
332
+ customersToUpdateDeclaration
333
+ ]);
334
+
335
+ const linkedDirty = useMemo(() => {
336
+ const initial = new Set((initialFormData.linkedTo || []).map(String));
337
+ if (initial.size !== allLinkedTo.size) return true;
338
+ for (const id of allLinkedTo) {
339
+ if (!initial.has(String(id))) return true;
340
+ }
341
+ return false;
342
+ }, [initialFormData.linkedTo, allLinkedTo]);
343
+
344
+
345
+ // which customers changed at the per-customer level
346
+ const editedCustomerIDs = useMemo(() => {
347
+ if (!isHydrated || !allCustomers || allCustomers.length === 0) return new Set();
348
+
349
+ return computeEditedCustomers(initialFormData, allCustomers);
350
+ }, [isHydrated, initialFormData.customers, allCustomers]);
351
+
352
+ const formIsDirty = useMemo(
353
+ () => isHydrated && (topLevelIsDirty ||
354
+ editedCustomerIDs.size > 0 ||
355
+ linkedDirty),
356
+ [isHydrated, topLevelIsDirty, editedCustomerIDs, linkedDirty]
357
+ );
358
+
359
+ const _hasCurrentDeclaration = useCallback(
360
+ (cust) => Boolean(cust?.trespass?.declarationOfService),
361
+ []
362
+ );
363
+
364
+ const _isNewlyAddedDeclaration = useCallback(
365
+ (cust) => _hasCurrentDeclaration(cust) && !originalDeclarationCustomerIds.has(cust.id),
366
+ [originalDeclarationCustomerIds, _hasCurrentDeclaration]
367
+ );
368
+
369
+ const topLevelAffectsDeclaration = useMemo(() => {
370
+ if (!isHydrated) return false;
371
+ return hasTopLevelChangeAffectedDeclaration(
372
+ initialFormData,
373
+ formData,
374
+ selectedWitnesses,
375
+ unsavedMediaArray
376
+ );
377
+ }, [
378
+ isHydrated,
379
+ initialFormData,
380
+ formData,
381
+ selectedWitnesses,
382
+ unsavedMediaArray
383
+ ]);
384
+
385
+ // determine who must opt-in via 'Update declaration' if wanting persist DoS and new document generated
386
+ const requiredIds = useMemo(() => {
387
+ if (topLevelAffectsDeclaration) {
388
+ // global 'Update declaration' only when fields taht affect docs changed
389
+ return new Set(originalDeclarationCustomerIds);
390
+ };
391
+ // per-customer path
392
+ // only edited customers who originally had DoS
393
+ const set = new Set();
394
+ for (const id of editedCustomerIDs) {
395
+ if (originalDeclarationCustomerIds.has(id)) set.add(id);
396
+ }
397
+ return set;
398
+ }, [
399
+ originalDeclarationCustomerIds,
400
+ editedCustomerIDs,
401
+ topLevelAffectsDeclaration
402
+ ]);
403
+
404
+ // who is required but not checked in list via 'update declaration'
405
+ const missingIds = useMemo(() => {
406
+ const allow = new Set(customersToUpdateDeclaration);
407
+ return [...requiredIds].filter(id => !allow.has(id))
408
+ }, [requiredIds, customersToUpdateDeclaration]);
409
+
410
+ const handleStagedTrespassUpdate = useCallback((custId, trespassData) => {
411
+ setStagedTrespassMap(prev => {
412
+ const next = new Map(prev);
413
+ next.set(custId, trespassData);
414
+ return next;
415
+ });
416
+ }, []);
417
+
418
+ const handleUpdateDeclaration = useCallback((custId) => {
419
+ setCustomersToUpdateDeclaration((prevArray) => {
420
+ if (prevArray.includes(custId)) {
421
+ return prevArray.filter(c => c !== custId);
422
+ } else {
423
+ return [...prevArray, custId];
424
+ }
425
+ });
426
+ }, []);
427
+
428
+ const handleClickNo = () => {
429
+ handleSubmit();
430
+ };
431
+
432
+ const handlClickYes = () => {
433
+ setIsModalAttentionDecOfService(false);
434
+ };
435
+
436
+ useEffect(() => {
437
+ if (trespassTemplates) {
438
+ const defaultTemplate = trespassTemplates.find((template) => {
439
+ return template.isDefault === true;
440
+ });
441
+ if (defaultTemplate) {
442
+ const templateValue = defaultTemplate.templateValue
443
+ setTrespassTemplate(templateValue)
444
+ };
445
+ };
446
+ }, [trespassTemplates]);
447
+
448
+ useEffect(() => {
449
+ setIsNoCustomer(formData.customerNa)
450
+ }, [formData.customerNa]);
451
+
452
+ useEffect(() => {
453
+ if(formData.metadata && 'createdByUserId' in formData.metadata && formData.metadata.createdByUserId !== '') {
454
+ setCreatedById(formData.metadata.createdByUserId)
455
+ }
456
+ }, [formData, formData.metadata]);
457
+
458
+ useEffect(() => {
459
+ if(formData.metadata && 'updatedByUserId' in formData.metadata && formData.metadata.updatedByUserId !== '') {
460
+ setUpdatedById(formData.metadata.updatedByUserId)
461
+ }
462
+ }, [formData, formData.metadata]);
463
+
464
+ useEffect(() => {
465
+ if(formData.attachments && formData.attachments.length > 0) {
466
+ const docs = formData.attachments.filter((att) => att.contentType.startsWith('application'));
467
+ setDocuments(docs);
468
+ const medias = formData.attachments.filter((att) => att.contentType.startsWith('image') || att.contentType.startsWith('video'));
469
+ setMediaArray(medias);
470
+ const newLoadingStatus = medias.reduce((acc, att) => ({
471
+ ...acc,
472
+ [att.id]: loadingStatus[att.id] !== false // retain prev load status
473
+ ? true // start as loading if not already set
474
+ : false
475
+ }), {});
476
+ setLoadingStatus(newLoadingStatus);
477
+ openImageSkeleton();
478
+ }
479
+ }, [formData.attachments])
480
+
481
+ const sortedDocuments = useMemo(() => sortTrespassDocuments(documents, mostCurrentTrespassDocIds), [documents, mostCurrentTrespassDocIds]);
482
+
483
+ const handleAddMedia = (mediaObj) => {
484
+ const readyMediaObj = {
485
+ ...mediaObj,
486
+ id: makeId(mediaObj.description),
487
+ description: mediaObj.description.trim()
488
+
489
+ };
490
+ setUnsavedMediaArray((prev) => [
491
+ ...prev,
492
+ readyMediaObj
493
+ ]);
494
+ };
495
+
496
+ const handleRemoveUnsavedMedia = (unsavedId) => {
497
+ const updatedUnsavedMediaArray = unsavedMediaArray.filter((obj) => obj.id !== unsavedId)
498
+ setUnsavedMediaArray(updatedUnsavedMediaArray);
499
+ };
500
+
501
+ const handleMediaUrl = (mediaUrl, attachmentId) => {
502
+ setMediaSrc(prev => ({ ...prev, [attachmentId]: mediaUrl }));
503
+ setLoadingStatus(prev => ({ ...prev, [attachmentId]: false }));
504
+ };
505
+
506
+ useEffect(() => {
507
+ if (documents && documents.length > 0) {
508
+ const startStr = 'trespass-';
509
+ const current = identifyCurrentTrespassDocs(documents, startStr);
510
+ setMostCurrentTrespassDocIds(current);
511
+ }
512
+ }, [documents]);
513
+
514
+ const thumbnailStyle = { width: '100px', height: 'auto', objectFit: 'cover'};
515
+
516
+ const handleMissingUsers = (userId) => {
517
+ setMissingUsers((prev) => {
518
+ return [ ...prev, userId]
519
+ })
520
+ };
521
+
522
+ const handleCloseEdit = () => {
523
+ setCreatedByForRender({
524
+ id: '',
525
+ barcode: '',
526
+ firstName: '',
527
+ lastName: ''
528
+ });
529
+ setUpdatedByForRender({
530
+ id: '',
531
+ barcode: '',
532
+ firstName: '',
533
+ lastName: ''
534
+ });
535
+ setCreatedById('');
536
+ setUpdatedById('');
537
+ setSelectedCustomers([]);
538
+ setSelectedWitnesses([]);
539
+ setMissingUsers([]);
540
+ setIdForMediaCreate(null);
541
+ setFormDataArrayForMediaCreate(null);
542
+ setCustomersToUpdateDeclaration([]);
543
+ closeEditPane();
544
+ history.replace(`/incidents/${id}${searchRef.current}`);
545
+ };
546
+
547
+ const anyDirty = formIsDirty || staffSuppressedIsDirty;
548
+ const handleClickDismissCancel = () => {
549
+ if (anyDirty) {
550
+ setShowDirtyFormModal(true);
551
+ } else {
552
+ handleCloseEdit();
553
+ };
554
+ };
555
+
556
+ const handleKeepEditing = () => {
557
+ setShowDirtyFormModal(false);
558
+ };
559
+
560
+ const handleDismissOnDirty = () => {
561
+ handleCloseEdit();
562
+ };
563
+
564
+ const clearSubLocation = () => {
565
+ setFormData({
566
+ ...formData,
567
+ subLocation: '',
568
+ });
569
+ };
570
+
571
+ const handleOpenModalLinkIncident = () => {
572
+ setShowModalLinkIncident(true)
573
+ };
574
+
575
+ const handleCloseModalLinkIncident = () => {
576
+ setShowModalLinkIncident(false)
577
+ };
578
+
579
+ const toggleRowChecked = useCallback((id) => {
580
+ setAllLinkedTo(prev => {
581
+ const nextSet = new Set(prev);
582
+ nextSet.has(id) ? nextSet.delete(id) : nextSet.add(id);
583
+ return nextSet;
584
+ })
585
+ }, []);
586
+
587
+ const handleTrashLinkedIncident = (toDeleteId) => {
588
+ toggleRowChecked(toDeleteId)
589
+ };
590
+
591
+ const locationDataOptions = useMemo(() => {
592
+ const defaultValueLabel = [{
593
+ value: '',
594
+ label: <FormattedMessage
595
+ id="create-pane.locationDataOptions-label-select-location"/>
596
+ }];
597
+ const formattedLocations = locationsInService
598
+ ? locationsInService.map((loc) => ({
599
+ // <Select /> to match current inst loc.id to pretty name
600
+ value: loc.id,
601
+ label: loc.location,
602
+ subLocations: loc.subLocations ? loc.subLocations : []
603
+ }))
604
+ : [{
605
+ value: '',
606
+ label: <FormattedMessage
607
+ id="create-pane.locationDataOptions-label-no-loaded"/>
608
+ }];
609
+ return [
610
+ ...defaultValueLabel,
611
+ ...formattedLocations,
612
+ ];
613
+ }, [locationsInService]);
614
+
615
+ const runSubLocationsSelect = useCallback((value) => {
616
+ let subLocs;
617
+ let options;
618
+ const noSubLocationOption = [{
619
+ value: 'No sub-location',
620
+ label: <FormattedMessage
621
+ id="create-pane.subLocations-label-default-no-sub-location"/>
622
+ }];
623
+ const noValueLabel = [{
624
+ value: '',
625
+ label: <FormattedMessage
626
+ id="create-pane.subLocations-label-no-sub-location-available"
627
+ />
628
+ }];
629
+
630
+ // ensure initialization of locationDataOptions
631
+ const currentValue = Array.isArray(locationDataOptions)
632
+ ? locationDataOptions.find((loc) => loc.value === value)
633
+ : undefined;
634
+
635
+ // ensure initialization of currentValue.subLocations
636
+ if (currentValue && Array.isArray(currentValue.subLocations) && currentValue.subLocations.length > 0) {
637
+ subLocs = currentValue.subLocations.map((sub) => {
638
+ return { value: sub.name, label: `${sub.name} - ${sub.description}` };
639
+ });
640
+ options = [...noSubLocationOption, ...subLocs];
641
+ } else {
642
+ options = [...noValueLabel];
643
+ };
644
+ setSubLocationsDataOptions(options);
645
+ }, [locationDataOptions]);
646
+
647
+ useEffect(() => {
648
+ // for initial rendering the current instance subLocation value in Select
649
+ // dependency of once locationDataOptions has been set
650
+ runSubLocationsSelect(formData.incidentLocation);
651
+ }, [locationDataOptions, formData.incidentLocation, runSubLocationsSelect]);
652
+
653
+ const handleChange = (eventOrValue) => {
654
+ let name;
655
+ let value;
656
+ setFormData((prev) => ({
657
+ ...prev,
658
+ [name]: value,
659
+ }));
660
+ if (eventOrValue && eventOrValue.target) {
661
+ ({ name, value } = eventOrValue.target);
662
+
663
+ if (eventOrValue.target.type === 'checkbox') {
664
+ value = eventOrValue.target.checked;
665
+ }
666
+ } else {
667
+ // if no 'target' property in custom component such as
668
+ // ( has array of selected options, not event object)
669
+ name = 'incidentWitnesses';
670
+ value = eventOrValue;
671
+ }
672
+ if (
673
+ name === 'customerDetails.firstName' ||
674
+ name === 'customerDetails.lastName'
675
+ ) {
676
+ const key = name.split('.')[1];
677
+
678
+ setFormData((prev) => ({
679
+ ...prev,
680
+ customerDetails: {
681
+ ...prev.customerDetails,
682
+ [key]: value,
683
+ },
684
+ }));
685
+ }
686
+ if (name === 'incidentWitnesses') {
687
+ const selectedRoles = value.map((item) => item.value);
688
+ const updatedWitnesses = formData.incidentWitnesses.map((witness) => ({
689
+ ...witness,
690
+ selected: selectedRoles.includes(witness.role),
691
+ }));
692
+ setFormData((prev) => ({
693
+ ...prev,
694
+ incidentWitnesses: updatedWitnesses,
695
+ }));
696
+ }
697
+ if (name === 'incidentLocation') {
698
+ clearSubLocation();
699
+ runSubLocationsSelect(value);
700
+ setFormData((prev) => ({
701
+ ...prev,
702
+ [name]: value,
703
+ }));
704
+ }
705
+ if (name === 'dateTimeOfIncident') {
706
+ setFormData((prev) => ({
707
+ ...prev,
708
+ [name]: value,
709
+ }));
710
+ } else {
711
+ setFormData((prev) => ({
712
+ ...prev,
713
+ [name]: value,
714
+ }));
715
+ }
716
+ };
717
+
718
+ useEffect(() => {
719
+ draftRef.current = formData.detailedDescriptionOfIncident || '';
720
+ }, [formData.detailedDescriptionOfIncident])
721
+
722
+ // fires on every key-press but only mutates the ref
723
+ const handleDescriptionChange = (content) => {
724
+ draftRef.current = content;
725
+ };
726
+
727
+ // commits once, with sanitized HTML
728
+ const handleEditorBlur = () => {
729
+ const sanitizedContent = DOMPurify.sanitize(draftRef.current);
730
+ setFormData(prev =>
731
+ isSameHtml(prev.detailedDescriptionOfIncident, sanitizedContent)
732
+ ? prev
733
+ : { ...prev, detailedDescriptionOfIncident: sanitizedContent }
734
+ );
735
+ };
736
+
737
+ const handleEditorKeyDown = (e) => {
738
+ if (e.key === 'Tab') {
739
+ handleEditorBlur(); // force commit when user tabs out
740
+ }
741
+ };
742
+
743
+ const handleGetWitnessName = (witObj) => {
744
+ setAssociatedKeyWitArray((prevState) => {
745
+ return [...prevState, witObj]
746
+ })
747
+ };
748
+
749
+ useEffect(() => {
750
+ if(associatedKeyWitArray && associatedKeyWitArray.length > 0) {
751
+ const readyDataWitnesses = formData.incidentWitnesses.map((dataObj) => {
752
+ const matchingWit = associatedKeyWitArray.find(
753
+ (wit) => wit.id === dataObj.id
754
+ );
755
+ if(matchingWit) {
756
+ return {
757
+ ...dataObj,
758
+ associatedFirstName: matchingWit.firstName,
759
+ associatedLastName: matchingWit.lastName
760
+ };
761
+ }
762
+ return dataObj;
763
+ });
764
+ setFormData(prev => ({
765
+ ...prev,
766
+ incidentWitnesses: readyDataWitnesses
767
+ }))
768
+ }
769
+ }, [associatedKeyWitArray]);
770
+
771
+ useEffect(() => {
772
+ const witnessesSet = new Set(
773
+ [...formData.incidentWitnesses, ...selectedWitnesses].map((wit) =>
774
+ JSON.stringify(wit)
775
+ )
776
+ );
777
+ const mergedNoDuplicateWitnesses = Array.from(witnessesSet).map((wit) =>
778
+ JSON.parse(wit)
779
+ );
780
+ setAllWitnesses(mergedNoDuplicateWitnesses)
781
+ }, [formData.incidentWitnesses, selectedWitnesses]);
782
+
783
+ const handleGetCustName = (userObj) => {
784
+ setAssociatedKeyCustArray((prevState) => {
785
+ return [...prevState, userObj];
786
+ })
787
+ };
788
+
789
+ useEffect(() => {
790
+ // handle non registered customers to bypass setting associated key names
791
+ singleIncident.customers?.forEach((cust) => {
792
+ if (cust.registered === false) {
793
+ handleGetCustName(cust);
794
+ }
795
+ });
796
+ }, [singleIncident.customers]);
797
+
798
+ useEffect(() => {
799
+ // handle registered customers to add in temporary key/values for rendering name based on associated keys
800
+ if(associatedKeyCustArray && associatedKeyCustArray.length > 0) {
801
+ const readyDataCustomers = formData.customers.map((dataObj) => {
802
+ const matchingCust = associatedKeyCustArray.find(
803
+ (cust) => cust.id === dataObj.id
804
+ );
805
+ if (matchingCust) {
806
+ return {
807
+ ...dataObj,
808
+ associatedFirstName: matchingCust.firstName,
809
+ associatedLastName: matchingCust.lastName
810
+ };
811
+ };
812
+ return dataObj;
813
+ });
814
+ setFormData(prev => ({
815
+ ...prev,
816
+ customers: readyDataCustomers
817
+ }))
818
+ };
819
+ }, [associatedKeyCustArray]);
820
+
821
+
822
+ /*
823
+ effect ensures that allCustomers always refelcts the most recently
824
+ edited customer data.
825
+ it merges customer info from prevAllCustomers, formData.customers, and selectedCustomers,
826
+ always preferring latest version for each customer (formData > selected > prev).
827
+ only the trespass field is conditionally updated from stagedTrespassMap.
828
+ this prevents stale data from overwriting recent edits,
829
+ especially after ModalTrespass.js save/submit (fix: which previously fired downstream
830
+ events to unexpectedly wipe ModalCustomerDetails.js edits).
831
+ */
832
+ useEffect(() => {
833
+ if (!isHydrated) return;
834
+ setAllCustomers(prevAllCustomers => {
835
+ // previous state, keyed by id
836
+ const prevMap = new Map(prevAllCustomers.map(c => [c.id, c]));
837
+ // build the new list de-duped by id, while merging fields
838
+ const byId = new Map();
839
+
840
+ // helper to merge staged trespass, but preserve all other fields from prevAllCustomers
841
+ const mergeOne = (cust) => {
842
+ if (!cust || !cust.id) return;
843
+ const existing = prevMap.get(cust.id) || cust;
844
+ const stagedTrespass = stagedTrespassMap.get(cust.id);
845
+
846
+ // only update trespass if staged otherwise keep everything else as last edited
847
+ const merged = {
848
+ ...existing,
849
+ trespass: stagedTrespass !== undefined ? stagedTrespass : existing.trespass,
850
+ };
851
+
852
+ byId.set(cust.id, merged);
853
+ };
854
+
855
+ // always use the union of all customer IDs from previous state, formData, and selectedCustomers
856
+ const allIds = new Set([
857
+ ...prevAllCustomers.map(c => c.id),
858
+ ...formData.customers.map(c => c.id),
859
+ ...selectedCustomers.map(c => c.id),
860
+ ]);
861
+
862
+ // for each customer, merge as above
863
+ allIds.forEach(id => {
864
+ // prefer prevAllCustomers as the base
865
+ const prev = prevMap.get(id);
866
+ // find the latest in formData or selectedCustomers if present
867
+ const formCust = formData.customers.find(c => c.id === id);
868
+ const selectedCust = selectedCustomers.find(c => c.id === id);
869
+ // use the most recently edited customer object, falling back to prev
870
+ const baseCust = formCust || selectedCust || prev;
871
+ mergeOne(baseCust);
872
+ });
873
+
874
+ return Array.from(byId.values());
875
+ });
876
+ }, [isHydrated, formData.customers, selectedCustomers, stagedTrespassMap]);
877
+
878
+ const handleRemoveCustomer = (customerId) => {
879
+ setFormData(prevFormData => ({
880
+ ...prevFormData,
881
+ customers: prevFormData.customers.filter(cust => cust.id !== customerId)
882
+ }))
883
+ setSelectedCustomers(prevSelectedCustomers =>
884
+ prevSelectedCustomers.filter(cust => cust.id !== customerId)
885
+ );
886
+ setRemovedCustomerIds(prev => [...prev, customerId]);
887
+ };
888
+
889
+ // persisted (original) customers on the incident
890
+ const initialCustomerIds = useMemo(
891
+ () => new Set((initialFormData.customers ?? []).map(c => c.id)),
892
+ [initialFormData.customers]
893
+ );
894
+
895
+ // truly new in THIS edit session (not present in initial incident)
896
+ const newlyAddedCustomerIds = useMemo(() => {
897
+ const out = new Set();
898
+ for (const c of allCustomers) {
899
+ if (c?.id && !initialCustomerIds.has(c.id)) out.add(c.id);
900
+ }
901
+ return out;
902
+ }, [allCustomers, initialCustomerIds]);
903
+
904
+ const optedSet = useMemo(
905
+ () => new Set(customersToUpdateDeclaration),
906
+ [customersToUpdateDeclaration]
907
+ );
908
+
909
+ const isNewlyAddedDoS = (cust) =>
910
+ Boolean(cust.trespass?.declarationOfService) &&
911
+ !originalDeclarationCustomerIds.has(cust.id);
912
+
913
+ const willReceiveNewDeclaration = (cust) => {
914
+ const hasDoSNow = Boolean(cust.trespass?.declarationOfService);
915
+ return (
916
+ hasDoSNow &&
917
+ !formData.staffSuppressed &&
918
+ (
919
+ optedSet.has(cust.id) || // user opted-in
920
+ newlyAddedCustomerIds.has(cust.id) || // truly new & with DoS
921
+ isNewlyAddedDoS(cust) // DoS added this edit
922
+ )
923
+ );
924
+ };
925
+
926
+ const finalizeSubmission = async (finalCustomersArray) => {
927
+ /*
928
+ For 'detailedDescriptionOfIncident' build that value from draftRef,
929
+ not formData, so we always send the latest text even if user clicks save
930
+ while the Editor still has focus and onBlur hasn't committed the field to state yet.
931
+ Note that 'detailedDescriptionOfIncident' feeds a trespass object's 'descriptionOfOccurrence' value, which is only seen in the context of raw record or an automated trespass document if that token is chosen for the doc template.
932
+ */
933
+ const cleanedDetailedDescription = DOMPurify.sanitize(draftRef.current);
934
+ // console.log("cleanedDetailedDescription --> ", JSON.stringify(cleanedDetailedDescription, null, 2))
935
+
936
+ try {
937
+ // remove temp associated name keys
938
+ const formattedWitnesses = allWitnesses.map(({ associatedFirstName, associatedLastName, ...rest }) => {
939
+ if (rest.isCustom) {
940
+ const { role, phone, email, ...others } = rest;
941
+
942
+ const trimmedRole = rest.role?.trim();
943
+ const trimmedPhone = rest.phone?.trim();
944
+ const trimmedEmail = rest.email?.trim();
945
+ return {
946
+ ...others,
947
+ ...(trimmedRole ? { role: trimmedRole } : {}),
948
+ ...(trimmedPhone ? { phone: trimmedPhone } : {}),
949
+ ...(trimmedEmail ? { email: trimmedEmail } : {})
950
+ };
951
+ }
952
+ return rest;
953
+ });
954
+
955
+ // let for conditional assignment
956
+ let formattedCustomers = finalCustomersArray.map((cust) => {
957
+ let updatedCustomer = { ...cust };
958
+ delete updatedCustomer.associatedFirstName;
959
+ delete updatedCustomer.associatedLastName;
960
+
961
+ if (cust.trespass) {
962
+ let reasonPatch = {};
963
+ if (willReceiveNewDeclaration(cust)) {
964
+ const currentReasons = cust.trespass.exclusionOrTrespassBasedOn || [];
965
+ const { sanitized: cleaned, hadAnyBefore } =
966
+ sanitizeReasons(currentReasons, allowedReasonsById);
967
+
968
+ const finalReasons =
969
+ cleaned.length > 0
970
+ ? cleaned
971
+ : (hadAnyBefore && defaultReason ? [defaultReason] : []);
972
+
973
+ reasonPatch = { exclusionOrTrespassBasedOn: finalReasons };
974
+ };
975
+
976
+ let trespassDesc = cust.trespass.description?.trim();
977
+ updatedCustomer = {
978
+ ...updatedCustomer,
979
+ trespass: {
980
+ ...cust.trespass,
981
+ ...reasonPatch, // only applied when gated in, otherise untouched
982
+ dateOfOccurrence: formatDateToUTCISO(formData.dateTimeOfIncident),
983
+ ...(cust.trespass.endDateOfTrespass
984
+ ? {
985
+ endDateOfTrespass: formatDateToUTCISO(
986
+ cust.trespass.endDateOfTrespass
987
+ ),
988
+ }
989
+ : {}),
990
+ ...(cust.trespass.declarationOfService
991
+ ? {
992
+ declarationOfService: {
993
+ ...cust.trespass.declarationOfService,
994
+ date: formatDateToUTCISO(
995
+ cust.trespass.declarationOfService.date
996
+ ),
997
+ },
998
+ }
999
+ : {}),
1000
+
1001
+ // descriptionOfOccurrence: cleanedDetailedDescription.trim(),
1002
+ // if custom trespass description exists, use it for both fields
1003
+ ...(trespassDesc
1004
+ ? {
1005
+ description: DOMPurify.sanitize(trespassDesc),
1006
+ descriptionOfOccurrence: DOMPurify.sanitize(trespassDesc),
1007
+ }
1008
+ : {
1009
+ // default
1010
+ descriptionOfOccurrence: cleanedDetailedDescription.trim(),
1011
+ }),
1012
+ witnessedBy: formattedWitnesses,
1013
+ },
1014
+ };
1015
+ };
1016
+ if (cust.details) {
1017
+ const { dateOfBirth, ...restDetails } = cust.details;
1018
+ updatedCustomer = {
1019
+ ...updatedCustomer,
1020
+ details: {
1021
+ ...restDetails,
1022
+ ...(dateOfBirth ?
1023
+ { dateOfBirth: formatDateToUTCISO(cust.details.dateOfBirth) }
1024
+ : {}),
1025
+ },
1026
+ };
1027
+ }
1028
+ return updatedCustomer;
1029
+ });
1030
+
1031
+ const updatedMetadata = {
1032
+ createdByUserId: singleIncident.metadata.createdByUserId,
1033
+ createdDate: singleIncident.metadata.createdDate,
1034
+ updatedByUserId: self.id
1035
+ };
1036
+ if (formData.customerNa && formattedCustomers.length > 0) {
1037
+ formattedCustomers = []
1038
+ };
1039
+ const data = {
1040
+ ...formData,
1041
+ detailedDescriptionOfIncident: cleanedDetailedDescription,
1042
+ customerNa: formattedCustomers.length > 0 ? false : formData.customerNa,
1043
+ customers: formData.customerNa ? [] : formattedCustomers,
1044
+ dateTimeOfIncident: formatDateAndTimeToUTCISO(formData.dateTimeOfIncident, formData.timeOfIncident),
1045
+ incidentWitnesses: formattedWitnesses, // allWitnesses witnessesList
1046
+ id: singleIncident.id,
1047
+ createdBy: singleIncident.createdBy,
1048
+ metadata: updatedMetadata,
1049
+ linkedTo: Array.from(allLinkedTo)
1050
+ };
1051
+ delete data.timeOfIncident; // is temp UI render key and its value derives from dateTimeOfIncident
1052
+
1053
+ const onlyLinkedChanged = linkedDirty &&
1054
+ !topLevelAffectsDeclaration &&
1055
+ editedCustomerIDs.size === 0;
1056
+
1057
+ let readyTrespassDocuments = [];
1058
+ let trespassDocumentPDFs = [];
1059
+ // only generate trespass PDFs if record is not staffSuppressed and not only did linkedTo change
1060
+ if (!formData.staffSuppressed && !onlyLinkedChanged) {
1061
+ // generate trespass documents
1062
+ try {
1063
+ const helperDeps = { locationDataOptions, trespassReasons, self, triggerDocumentError};
1064
+ const selectedCustomerIds = new Set(selectedCustomers.map(c => c.id));
1065
+
1066
+ readyTrespassDocuments = generateTrespassDocumentsAtEdit(
1067
+ formattedCustomers,
1068
+ customersToUpdateDeclaration, // array
1069
+ selectedCustomerIds, // Set
1070
+ originalDeclarationCustomerIds, // Set
1071
+ // editedCustomerIDs, // Set
1072
+ topLevelAffectsDeclaration, // boolean
1073
+ data,
1074
+ trespassTemplate,
1075
+ helperDeps
1076
+ );
1077
+ } catch (error) {
1078
+ console.error(`Error at readyTrespassDocuments: ${error}`);
1079
+ // triggerDocumentError(`Error in generateTrespassDocuments: ${error}`)
1080
+ const errorMsg = error.message;
1081
+ triggerDocumentError(<FormattedMessage
1082
+ id="generate-trespass.error-doc-readyTrespassDocuments"
1083
+ values={{ error: errorMsg }}
1084
+ />)
1085
+ readyTrespassDocuments = [];
1086
+ };
1087
+
1088
+ try {
1089
+ // trespassDocumentPDFs = await generatePDFAttachments();
1090
+ trespassDocumentPDFs = await generatePDFAttachments(
1091
+ readyTrespassDocuments,
1092
+ triggerDocumentError
1093
+ );
1094
+ } catch (error) {
1095
+ console.error(`Unexpected error in PDF generation: ${error}`);
1096
+ // triggerDocumentError(`Unexpected error in PDF generation: ${error}`)
1097
+ const errorMsg = error.message;
1098
+ triggerDocumentError(<FormattedMessage
1099
+ id="generate-trespass.error-doc-unexpected-error"
1100
+ values={{ error: errorMsg }}
1101
+ />)
1102
+ trespassDocumentPDFs = [];
1103
+ };
1104
+ };
1105
+
1106
+ const readyToBeSaved = unsavedMediaArray.map((mediaObj) => {
1107
+ const {id, file, description, contentType} = mediaObj
1108
+ return {
1109
+ contentType: contentType,
1110
+ description: description,
1111
+ id: id,
1112
+ file: file,
1113
+ }
1114
+ });
1115
+ const mergedAttachments = [...readyToBeSaved, ...trespassDocumentPDFs];
1116
+ // UpdateReport will pass attachmentsData to CreateMedia on PUT success
1117
+ setAttachmentsData(mergedAttachments);
1118
+ // console.log('@Edit - the PUT data: ', JSON.stringify(data, null, 2));
1119
+ setPutData(data);
1120
+ setSelectedCustomers([]);
1121
+ setSelectedWitnesses([]);
1122
+ } catch (error) {
1123
+ console.error('error in edit submit - error: ', error)
1124
+ };
1125
+ };
1126
+
1127
+ const cleanupCustomerDeclarations = () => {
1128
+ const allowSet = new Set(customersToUpdateDeclaration);
1129
+ const selectedCustomerIds = new Set(selectedCustomers.map(c => c.id));
1130
+ // const wiped = [];
1131
+
1132
+ const updated = allCustomers.map((cust) => {
1133
+ const hasDoS = _hasCurrentDeclaration(cust);
1134
+ if (!hasDoS) return cust;
1135
+
1136
+ // const originallyHad = originalDeclarationCustomerIds.has(cust.id);
1137
+ const newlyAdded = _isNewlyAddedDeclaration(cust); // at CURRENT time
1138
+ const edited = editedCustomerIDs.has(cust.id);
1139
+
1140
+ const keep =
1141
+ allowSet.has(cust.id) || // explicitly opted-in
1142
+ selectedCustomerIds.has(cust.id) || // new customer this session
1143
+ newlyAdded || // newly added DoS
1144
+ (!topLevelAffectsDeclaration && !edited); // only document-affecting globals
1145
+
1146
+ if (keep) return cust;
1147
+
1148
+ const clone = { ...cust, trespass: { ...cust.trespass } };
1149
+ delete clone.trespass.declarationOfService;
1150
+
1151
+ return clone;
1152
+ });
1153
+ return updated;
1154
+ };
1155
+
1156
+ const handleSaveAndCloseClick = () => {
1157
+ // staffSuppressed only path
1158
+ if (staffSuppressedIsDirty && !formIsDirty) {
1159
+ handleSubmit();
1160
+ return;
1161
+ };
1162
+ if (isNoCustomer) {
1163
+ handleSubmit();
1164
+ return;
1165
+ };
1166
+ if (missingIds.length > 0) {
1167
+ setIsModalAttentionDecOfService(true);
1168
+ return;
1169
+ };
1170
+ // good to go, no exceptions/warns happy path
1171
+ handleSubmit();
1172
+ };
1173
+
1174
+ const handleSubmit = () => {
1175
+ setIsUpdatingReport(true);
1176
+ const readyCustomers = formData.staffSuppressed
1177
+ ? allCustomers
1178
+ : cleanupCustomerDeclarations();
1179
+ finalizeSubmission(readyCustomers);
1180
+ };
1181
+
1182
+ const dateIsNotInFuture = (dateString) => {
1183
+ const todayString = getTodayDate();
1184
+ const todayDate = parseMMDDYYYY(todayString);
1185
+ todayDate.setHours(0,0,0,0);
1186
+ const formDataDate = parseMMDDYYYY(dateString);
1187
+ if (formDataDate <= todayDate) {
1188
+ return true
1189
+ } else if (formDataDate > todayDate){
1190
+ return false
1191
+ };
1192
+ };
1193
+
1194
+ const isFormDataValid = () => {
1195
+ const isCustomersValid =
1196
+ (allCustomers.length > 0 && !formData.customerNa) || formData.customerNa;
1197
+ const isIncidentDetailsValid =
1198
+ formData.incidentLocation !== '' &&
1199
+ isValidDateFormat(formData.dateTimeOfIncident) &&
1200
+ dateIsNotInFuture(formData.dateTimeOfIncident) &&
1201
+ stripHTML(formData.detailedDescriptionOfIncident) !== '' &&
1202
+ isValidTimeInput(formData.timeOfIncident);
1203
+ const isIncidentTypeValid = formData.incidentTypes.length > 0;
1204
+ const isWitnessValid = allWitnesses && allWitnesses.length > 0;
1205
+ return (
1206
+ isCustomersValid &&
1207
+ isIncidentDetailsValid &&
1208
+ isIncidentTypeValid &&
1209
+ isWitnessValid
1210
+ );
1211
+ };
1212
+
1213
+ const handleGetCreatedByName = (createdByNameObj) => {
1214
+ if (createdByNameObj) {
1215
+ // console.log("@handleGetCreatedByName - createdByNameObj: ", JSON.stringify(createdByNameObj, null, 2))
1216
+ setCreatedByForRender({
1217
+ id: createdByNameObj.id,
1218
+ barcode: createdByNameObj.barcode,
1219
+ firstName: createdByNameObj.firstName,
1220
+ lastName: createdByNameObj.lastName
1221
+ })
1222
+ } else {
1223
+ setCreatedByForRender({
1224
+ id: '',
1225
+ barcode: '',
1226
+ firstName: '',
1227
+ lastName: ''
1228
+ })
1229
+ }
1230
+ };
1231
+
1232
+ const handleGetUpdatedByName = (updatedByNameObj) => {
1233
+ if (updatedByNameObj) {
1234
+ setUpdatedByForRender({
1235
+ id: updatedByNameObj.id,
1236
+ barcode: updatedByNameObj.barcode,
1237
+ firstName: updatedByNameObj.firstName,
1238
+ lastName: updatedByNameObj.lastName
1239
+ })
1240
+ } else {
1241
+ setUpdatedByForRender({
1242
+ id: '',
1243
+ barcode: '',
1244
+ firstName: '',
1245
+ lastName: ''
1246
+ })
1247
+ }
1248
+ };
1249
+
1250
+ // is passed into Modal
1251
+ const handleIncidentTypeToggle = (type) => {
1252
+ // console.log("@EDIT - handleIncidentTypeToggle, type: ", type)
1253
+ setFormData((prevFormData) => {
1254
+ const currentTypes = new Set(prevFormData.incidentTypes.map(t => t.id));
1255
+ if(currentTypes.has(type.id)) {
1256
+ return {
1257
+ ...prevFormData,
1258
+ incidentTypes: prevFormData.incidentTypes.filter(t => t.id !== type.id)
1259
+ };
1260
+ } else {
1261
+ return {
1262
+ ...prevFormData,
1263
+ incidentTypes: [...prevFormData.incidentTypes, type]
1264
+ }
1265
+ }
1266
+ })
1267
+ };
1268
+
1269
+ const handleRemoveType = (typeId) => {
1270
+ setFormData((prevFormData) => ({
1271
+ ...prevFormData,
1272
+ incidentTypes: prevFormData.incidentTypes.filter(type => type.id !== typeId)
1273
+ }));
1274
+ };
1275
+
1276
+ const handleRemoveWitness = (witnessId) => {
1277
+ setFormData(prevFormData => ({
1278
+ ...prevFormData,
1279
+ incidentWitnesses: prevFormData.incidentWitnesses.filter(wit => wit.id !== witnessId)
1280
+ }));
1281
+ setSelectedWitnesses(prevSelectedWitnesses => (
1282
+ prevSelectedWitnesses.filter(wit => wit.id !== witnessId)
1283
+ ));
1284
+ };
1285
+
1286
+ const handleMarkForRemoval = (mediaId) => {
1287
+ const updatedAttachments = formData.attachments.map((attachment) =>
1288
+ attachment.id === mediaId
1289
+ ? { ...attachment, toBeRemoved: true }
1290
+ : attachment
1291
+ );
1292
+ setFormData((prevState) => ({
1293
+ ...prevState,
1294
+ attachments: updatedAttachments,
1295
+ }));
1296
+ };
1297
+
1298
+ const handleUndo = (mediaId) => {
1299
+ const updatedAttachments = formData.attachments.map((attachment) => {
1300
+ if(attachment.id === mediaId) {
1301
+ const { toBeRemoved, ...rest } = attachment;
1302
+ return { ...rest };
1303
+ }
1304
+ return attachment
1305
+ });
1306
+ setFormData((prevState) => ({
1307
+ ...prevState,
1308
+ attachments: updatedAttachments,
1309
+ }));
1310
+ };
1311
+
1312
+ const handleAddSelfAsWitness = () => {
1313
+ setFormData(prevFormData => {
1314
+ const isSelfAlreadyWitness = prevFormData.incidentWitnesses.some(
1315
+ wit => wit.id === self.id
1316
+ );
1317
+ if(isSelfAlreadyWitness) {
1318
+ return prevFormData
1319
+ };
1320
+
1321
+ return {
1322
+ ...prevFormData,
1323
+ incidentWitnesses: [
1324
+ ...prevFormData.incidentWitnesses,
1325
+ self
1326
+ ]
1327
+ };
1328
+ });
1329
+ };
1330
+
1331
+ const handleOpenAddCustomWitness = () => {
1332
+ openModalCustomWitness()
1333
+ };
1334
+
1335
+ const handleShowTrespassFormModal = (trespassCustomerId) => {
1336
+ setTrespassCustomerID(trespassCustomerId);
1337
+ openModalTrespass();
1338
+ };
1339
+
1340
+ const handleShowCustomerDetailsFormModal = (custId) => {
1341
+ setDetailsCustomerID(custId);
1342
+ openModalCustomerDetails();
1343
+ };
1344
+
1345
+ const handleShowCustomWitModalAsEdit = (witObj) => {
1346
+ setCustWitEditObj(witObj)
1347
+ openModalCustomWitness()
1348
+ };
1349
+
1350
+ const handleShowCustomWitModalAsEditForUnsaved = (witId) => {
1351
+ setCustWitEditID(witId)
1352
+ openModalCustomWitness()
1353
+ };
1354
+
1355
+ // handle rendering inc type 'title' via associated key of 'id'
1356
+ // instead of the instance's inc type 'title'
1357
+ const preparedIncidentTypes = useMemo(() => {
1358
+ return formData.incidentTypes.map(incidentType => {
1359
+ const foundType = incidentTypesList.find(type => type.id === incidentType.id);
1360
+ const notFoundId = incidentType.id;
1361
+ return {
1362
+ id: incidentType.id,
1363
+ title: foundType ? foundType.title :
1364
+ <FormattedMessage
1365
+ id="incident-type-not-found-fallback"
1366
+ values={{ id: notFoundId }}
1367
+ />
1368
+ };
1369
+ });
1370
+ }, [formData.incidentTypes, incidentTypesList]);
1371
+
1372
+ const itemFormatterIncidentTypes = (item, index) => {
1373
+ if (!item) {
1374
+ // console.log("@itemFormatterIncidentTypes - no item ran ")
1375
+ return null;
1376
+ };
1377
+ return (
1378
+ <li key={item.id ?? index}>
1379
+ {item.title}
1380
+ <button
1381
+ style={{ paddingLeft: '8px' }}
1382
+ onClick={() => handleRemoveType(item.id)}
1383
+ type="button"
1384
+ aria-label={`Remove ${item.title}`}
1385
+ >
1386
+ <Icon icon="trash" size="medium" />
1387
+ </button>
1388
+ </li>
1389
+ );
1390
+ };
1391
+
1392
+ const handleCustomerNa = (event) => {
1393
+ setFormData((prev) => ({
1394
+ ...prev,
1395
+ customerNa: event.target.checked,
1396
+ }));
1397
+ setIsNoCustomer(prev => !prev );
1398
+ };
1399
+
1400
+ const handleStaffSuppressed = (event) => {
1401
+ setFormData((prev) => ({
1402
+ ...prev,
1403
+ staffSuppressed: event.target.checked,
1404
+ }));
1405
+ };
1406
+
1407
+ const extractSnippet = (description) => {
1408
+ // regex remove HTML tags
1409
+ const text = description.replace(/<[^>]*>/g, '');
1410
+ return text.length > 60 ? text.slice(0, 57) + '...' : text;
1411
+ };
1412
+
1413
+ // useEffect(() => {
1414
+ // console.log('[EditPane] allCustomers -> ', JSON.stringify(allCustomers, null, 2))
1415
+ // }, [allCustomers]);
1416
+
1417
+ const itemFormatterCustomers = (cust) => {
1418
+ const notAvailable = intl.formatMessage({ id: "unknown-name-placeholder" });
1419
+
1420
+ const firstName = cust.registered === false ?
1421
+ cust.firstName || notAvailable : cust.associatedFirstName;
1422
+
1423
+ const lastName = cust.registered === false ?
1424
+ cust.lastName || notAvailable : cust.associatedLastName;
1425
+
1426
+ const snippetOfDescription = cust.description ? extractSnippet(cust.description) : '';
1427
+
1428
+ const trespassServed = cust.trespass && cust.trespass.declarationOfService;
1429
+
1430
+ const name = `${lastName}, ${firstName}`;
1431
+
1432
+ return (
1433
+ <li key={cust.id} style={{ marginTop: '12px' }}>
1434
+ {cust.firstName === '' && cust.lastName === '' ? (
1435
+ snippetOfDescription
1436
+ ) : cust.registered ? (
1437
+ <a
1438
+ href={`/users/preview/${cust.id}`}
1439
+ target="_blank"
1440
+ aria-label="Link to customer in users application"
1441
+ style={{
1442
+ textDecoration: 'none',
1443
+ color: 'rgb(0,0,238)',
1444
+ fontWeight: 'bold',
1445
+ }}
1446
+ rel="noreferrer"
1447
+ >
1448
+ {/* render associated key name if instance customer, else if newly added customer show name */}
1449
+ {cust.associatedFirstName ? name : `${cust.lastName}, ${cust.firstName}`}
1450
+ </a>
1451
+ ) : (
1452
+ name // unregistered customer show instance name
1453
+ )}
1454
+
1455
+ {/* customer has been served their trespass */}
1456
+ {trespassServed ? (
1457
+ <span style={{ marginLeft: '10px', color: 'green' }}>
1458
+ <Icon icon="check-circle" />
1459
+ </span>
1460
+ ) : null}
1461
+
1462
+ <button
1463
+ style={{ paddingLeft: '8px' }}
1464
+ onClick={() => handleRemoveCustomer(cust.id)}
1465
+ type="button"
1466
+ aria-label={`Remove ${name} as customer`}
1467
+ >
1468
+ <Icon icon="trash" size="medium" />
1469
+ </button>
1470
+
1471
+ {cust.details || cust.description ? (
1472
+ <button
1473
+ style={{ paddingLeft: '15px' }}
1474
+ onClick={() => handleShowCustomerDetailsFormModal(cust.id)}
1475
+ type="button"
1476
+ >
1477
+ <Icon icon="report" size="medium" />
1478
+ <FormattedMessage id="edit-details-button"/>
1479
+ </button>
1480
+ ) : (
1481
+ <button
1482
+ style={{ paddingLeft: '15px' }}
1483
+ onClick={() => handleShowCustomerDetailsFormModal(cust.id)}
1484
+ type="button"
1485
+ >
1486
+ <Icon icon="plus-sign" size="medium" />
1487
+ <FormattedMessage id="add-details-button"/>
1488
+ </button>
1489
+ )}
1490
+
1491
+ {cust.trespass ? (
1492
+ <button
1493
+ style={{ paddingLeft: '15px' }}
1494
+ onClick={() => handleShowTrespassFormModal(cust.id)}
1495
+ type="button"
1496
+ >
1497
+ <Icon icon="report" size="medium" />
1498
+ <FormattedMessage id="edit-trespass-button"/>
1499
+ </button>
1500
+ ) : (
1501
+ <button
1502
+ style={{ paddingLeft: '15px' }}
1503
+ onClick={() => handleShowTrespassFormModal(cust.id)}
1504
+ type="button"
1505
+ >
1506
+ <Icon icon="plus-sign" size="medium" />
1507
+ <FormattedMessage id="add-trespass-button"/>
1508
+ </button>
1509
+ )}
1510
+ </li>
1511
+ );
1512
+ };
1513
+
1514
+ const witnessItemFormatter = (wit) => {
1515
+ const isCustomNotSaved = formData.incidentWitnesses.filter(incWit => incWit.id === wit.id).length === 0;
1516
+ return (
1517
+ <li key={wit.id}>
1518
+ {wit.isCustom === true ? (
1519
+ // custom witness
1520
+ <>
1521
+ {wit.lastName}, {wit.firstName}
1522
+ {isCustomNotSaved ? (
1523
+ // show handler for edit unsaved custom witness
1524
+ <button
1525
+ style={{ paddingLeft: '15px' }}
1526
+ onClick={() => handleShowCustomWitModalAsEditForUnsaved(wit.id)}
1527
+ type="button"
1528
+ >
1529
+ <Icon icon="report" size="medium" /> Edit
1530
+ </button>)
1531
+ : (
1532
+ <button
1533
+ style={{ paddingLeft: '15px' }}
1534
+ onClick={() => handleShowCustomWitModalAsEdit(wit)}
1535
+ type="button"
1536
+ >
1537
+ <Icon icon="report" size="medium" /> Edit
1538
+ </button>
1539
+ )}
1540
+ </>
1541
+ ) : wit.associatedFirstName ? (
1542
+ // Users witness
1543
+ <a
1544
+ href={`/users/preview/${wit.id}`}
1545
+ target="_blank"
1546
+ aria-label="Link to customer in users application"
1547
+ style={{
1548
+ textDecoration: 'none',
1549
+ color: 'rgb(0,0,238)',
1550
+ fontWeight: 'bold',
1551
+ }}
1552
+ rel="noreferrer"
1553
+ >
1554
+ {`${wit.associatedLastName}, ${wit.associatedFirstName}`}
1555
+ </a>
1556
+ ) : wit.id ? (
1557
+ // newly selected Users witness (not yet saved)
1558
+ <a
1559
+ href={`/users/preview/${wit.id}`}
1560
+ target="_blank"
1561
+ aria-label="Link to customer in users application"
1562
+ style={{
1563
+ textDecoration: 'none',
1564
+ color: 'rgb(0,0,238)',
1565
+ fontWeight: 'bold',
1566
+ }}
1567
+ rel="noreferrer"
1568
+ >
1569
+ {`${wit.lastName}, ${wit.firstName}`}
1570
+ </a>) : null}
1571
+
1572
+ <button
1573
+ style={{ paddingLeft: '8px' }}
1574
+ onClick={() => handleRemoveWitness(wit.id)}
1575
+ type="button"
1576
+ aria-label={`Remove ${wit.lastName}, ${wit.firstName} as witness`}
1577
+ >
1578
+ <Icon icon="trash" size="medium" />
1579
+ </button>
1580
+ </li>
1581
+ );
1582
+ };
1583
+
1584
+ const handleOpenModalAddMedia = () => {
1585
+ openModalMedia();
1586
+ };
1587
+
1588
+ const customersListLabel = intl.formatMessage(
1589
+ { id: `customers-list-label` },
1590
+ { count: formData.customers.length }
1591
+ );
1592
+
1593
+ const incidentTypesListLabel = intl.formatMessage(
1594
+ { id: `incident-types-list-label` },
1595
+ { count: preparedIncidentTypes.length }
1596
+ );
1597
+
1598
+ const witnessesListLabel = intl.formatMessage(
1599
+ { id: `witnesses-list-label` },
1600
+ { count: allWitnesses.length,
1601
+ bold: (chunks) => (
1602
+ <strong style={{ color: '#A12A2A' }}>{chunks}</strong>
1603
+ )
1604
+ }
1605
+ );
1606
+
1607
+ const editorModules = {
1608
+ toolbar: [
1609
+ [{ 'header': [1, 2, false] }],
1610
+ ['bold', 'italic', 'underline'],
1611
+ ],
1612
+ };
1613
+
1614
+ const canSave = isHydrated && isFormDataValid() && (formIsDirty ||staffSuppressedIsDirty);
1615
+
1616
+ const footer = (
1617
+ <PaneFooter
1618
+ renderStart={
1619
+ <Button onClick={handleClickDismissCancel}>
1620
+ <FormattedMessage id="cancel-button" />
1621
+ </Button>
1622
+ }
1623
+ renderEnd={
1624
+ <Button
1625
+ buttonStyle="primary"
1626
+ onClick={handleSaveAndCloseClick}
1627
+ disabled={!canSave}
1628
+ >
1629
+ <FormattedMessage id="save-and-close-button" />
1630
+ </Button>
1631
+ }
1632
+ />
1633
+ );
1634
+
1635
+ return (
1636
+ <>
1637
+ {id && <GetDetails id={id} />}
1638
+ {isLoadingDetails ? (
1639
+ <LoadingPane
1640
+ defaultWidth="fill"
1641
+ paneTitle={<FormattedMessage id="edit-pane.loading-pane-paneTitle" />}
1642
+ />
1643
+ ) : (
1644
+ <>
1645
+ {id && putData && (
1646
+ <UpdateReport
1647
+ id={id}
1648
+ data={putData}
1649
+ handleCloseEdit={handleCloseEdit}
1650
+ />
1651
+ )}
1652
+
1653
+ {idForMediaCreate && formDataArrayForMediaCreate && (
1654
+ <CreateMedia
1655
+ id={idForMediaCreate}
1656
+ formDataArray={formDataArrayForMediaCreate}
1657
+ handleCloseEdit={handleCloseEdit}
1658
+ context="edit"
1659
+ />
1660
+ )}
1661
+
1662
+ {isUpdatingReport ? (
1663
+ <LoadingPane
1664
+ defaultWidth="fill"
1665
+ paneTitle={<FormattedMessage id="edit-pane.loading-pane-submit-paneTitle" />}
1666
+ />
1667
+ ) : (
1668
+ <Pane
1669
+ paneTitle={<FormattedMessage id="edit-pane.paneTitle" />}
1670
+ defaultWidth="fill"
1671
+ renderHeader={(renderProps) => (
1672
+ <PaneHeader
1673
+ {...renderProps}
1674
+ dismissible
1675
+ onClose={handleClickDismissCancel}
1676
+ />
1677
+ )}
1678
+ footer={footer}
1679
+ >
1680
+ <GetTrespassTemplates />
1681
+ <GetTrespassReasons />
1682
+
1683
+ {showDirtyFormModal && (
1684
+ <ModalDirtyFormWarn
1685
+ handleKeepEditing={handleKeepEditing}
1686
+ handleDismissOnDirty={handleDismissOnDirty}
1687
+ />
1688
+ )}
1689
+
1690
+ <GetSummary
1691
+ ids={idsArray}
1692
+ onResult={setLinkedToSummaries}
1693
+ />
1694
+
1695
+ {showModalLinkIncident && (
1696
+ <ModalLinkIncident
1697
+ toggleRowChecked={toggleRowChecked}
1698
+ ids={allLinkedTo}
1699
+ setIds={setAllLinkedTo}
1700
+ handleCloseModalLinkIncident={handleCloseModalLinkIncident}
1701
+ />
1702
+ )}
1703
+
1704
+ {isModalAttentionDecOfService && (
1705
+ <ModalAttentionDecOfService
1706
+ onNo={handleClickNo}
1707
+ onYes={handlClickYes}
1708
+ missingIds={missingIds}
1709
+ allCustomers={allCustomers}
1710
+ />
1711
+ )}
1712
+
1713
+ {trespassCustomerID && (
1714
+ <ModalTrespass
1715
+ customerID={trespassCustomerID}
1716
+ setAllCustomers={setAllCustomers}
1717
+ allCustomers={allCustomers}
1718
+ onStagedTrespassUpdate={handleStagedTrespassUpdate}
1719
+ updateDeclarationArray={customersToUpdateDeclaration}
1720
+ onUpdateDeclaration={handleUpdateDeclaration}
1721
+ customersWithoutDeclaration={customersWithoutDeclaration}
1722
+ isNewlySelected={newlyAddedCustomerIds.has(trespassCustomerID)}
1723
+ />
1724
+ )}
1725
+
1726
+ {detailsCustomerID && (
1727
+ <ModalCustomerDetails
1728
+ customerID={detailsCustomerID}
1729
+ setAllCustomers={setAllCustomers}
1730
+ allCustomers={allCustomers}
1731
+ />
1732
+ )}
1733
+ {custWitEditObj && Object.keys(custWitEditObj).length > 0 ? (
1734
+ // Edit saved custom witness
1735
+ <ModalCustomWitness
1736
+ context='editSavedCustomWitness'
1737
+ formData={formData}
1738
+ setFormData={setFormData}
1739
+ setCustWitEditObj={setCustWitEditObj}
1740
+ custWitEditObj={custWitEditObj}
1741
+ />
1742
+ ) : custWitEditID ? (
1743
+ // Edit un-saved custom witness
1744
+ <ModalCustomWitness
1745
+ // utilizes selectedWitnesses React context at Modal
1746
+ custWitEditID={custWitEditID}
1747
+ />
1748
+ ) : (
1749
+ // Add a new custom witness
1750
+ <ModalCustomWitness
1751
+ // utilizes selectedWitnesses React context at Modal
1752
+ context='addCustomWitAtEdit'
1753
+ />
1754
+ )}
1755
+
1756
+ {singleIncident.customers?.map((cust) => {
1757
+ if (cust.registered === false) {
1758
+ return null
1759
+ }
1760
+ return (
1761
+ <GetName
1762
+ key={cust.id}
1763
+ uuid={cust.id}
1764
+ handleGetCustName={handleGetCustName}
1765
+ handleMissingUsers={handleMissingUsers}
1766
+ context='customer'
1767
+ />
1768
+ )
1769
+ })}
1770
+
1771
+ {formData.incidentWitnesses?.map((wit) => (
1772
+ <GetName
1773
+ isCustomWitness={wit.isCustom === true ? wit : null}
1774
+ key={wit.id}
1775
+ uuid={wit.id}
1776
+ handleGetWitnessName={handleGetWitnessName}
1777
+ handleMissingUsers={handleMissingUsers}
1778
+ context='witness'/>
1779
+ ))
1780
+ }
1781
+
1782
+ {updatedById && updatedById !== '' && (
1783
+ <GetName
1784
+ uuid={updatedById}
1785
+ handleGetUpdatedByName={handleGetUpdatedByName}
1786
+ handleMissingUsers={handleMissingUsers}
1787
+ context="updatedBy"
1788
+ />
1789
+ )}
1790
+
1791
+ {createdById && createdById !== '' && (
1792
+ <GetNameCreatedBy
1793
+ uuid={createdById}
1794
+ handleGetCreatedByName={handleGetCreatedByName}
1795
+ handleMissingUsers={handleMissingUsers}
1796
+ />
1797
+ )}
1798
+
1799
+ <GetLocationsInService />
1800
+ <GetIncidentTypesDetails context='incidents'/>
1801
+ <ModalAddMedia
1802
+ context='edit'
1803
+ handleAddMedia={handleAddMedia}
1804
+ />
1805
+ <ModalDescribeCustomer />
1806
+ <ModalSelectKnownCustomer
1807
+ context='edit'
1808
+ setRemovedCustomerIds={setRemovedCustomerIds}
1809
+ removedCustomerIds={removedCustomerIds}
1810
+ setFormData={setFormData}
1811
+ formData={formData}
1812
+ />
1813
+ <ModalSelectWitness
1814
+ context='edit'
1815
+ setFormData={setFormData}
1816
+ formData={formData}
1817
+ setRemovedWitnessIds={setRemovedWitnessIds}
1818
+ removedWitnessIds={removedWitnessIds}
1819
+ />
1820
+ <ModalSelectIncidentTypes
1821
+ handleIncidentTypeToggle={handleIncidentTypeToggle}
1822
+ formDataIncidentTypes={formData.incidentTypes}
1823
+ />
1824
+
1825
+ <GetSelf />
1826
+
1827
+ {/* if 404 for any /Users request get associated key for name, render MessageBanner with message and those unfound uuid(s) */}
1828
+ <Row>
1829
+ <Col xs={12}>
1830
+ <div>
1831
+ <MessageBanner
1832
+ dismissible
1833
+ type="error"
1834
+ show={missingUsers.length > 0}
1835
+ >
1836
+ {<FormattedMessage
1837
+ id="message-banner.error-missing-users-404"
1838
+ values={{ ids: missingUsers.join(', ') }}
1839
+ />}
1840
+ </MessageBanner>
1841
+ </div>
1842
+ </Col>
1843
+ </Row>
1844
+
1845
+ <AccordionSet>
1846
+ <ExpandAllButton />
1847
+ <Accordion
1848
+ closedByDefault={true}
1849
+ label={<FormattedMessage
1850
+ id="edit-pane.accordion-administrative-data-label"/>}>
1851
+ <MetaSection
1852
+ headingLevel={4}
1853
+ useAccordion
1854
+ showUserLink
1855
+ createdDate={formData?.metadata?.createdDate || null}
1856
+ lastUpdatedDate={formData?.metadata?.updatedDate || null}
1857
+ createdBy={{
1858
+ id: createdByForRender.id,
1859
+ personal: {
1860
+ firstName: createdByForRender.firstName,
1861
+ lastName: createdByForRender.lastName
1862
+ }
1863
+ }}
1864
+ lastUpdatedBy={{
1865
+ id: updatedByForRender.id,
1866
+ personal: {
1867
+ firstName: updatedByForRender.firstName,
1868
+ lastName: updatedByForRender.lastName,
1869
+ }
1870
+ }}
1871
+ />
1872
+ <Row>
1873
+ <Col xs={6}>
1874
+ <Checkbox
1875
+ label={<FormattedMessage id="edit-pane.checkbox-staff-suppress"/>}
1876
+ checked={formData.staffSuppressed}
1877
+ name='staffSuppressed'
1878
+ onChange={handleStaffSuppressed}
1879
+ />
1880
+ </Col>
1881
+ </Row>
1882
+ </Accordion>
1883
+ <Accordion
1884
+ label={<FormattedMessage id="accordion-label-customers" />}
1885
+ >
1886
+ <Row>
1887
+ <Col xs={2} style={{ marginTop: '10px', marginLeft: '10px', marginBottom: '10px'}}>
1888
+ <Checkbox
1889
+ label={<FormattedMessage id="customer-not-available"/>}
1890
+ checked={formData.customerNa}
1891
+ name='customerNa'
1892
+ onChange={handleCustomerNa}
1893
+ />
1894
+ </Col>
1895
+ </Row>
1896
+ {!isNoCustomer && (
1897
+ <>
1898
+ <Row>
1899
+ <Col xs={3}>
1900
+ <Button
1901
+ style={{ marginTop: '25px' }}
1902
+ onClick={openModalSelectKnownCust}
1903
+ >
1904
+ <FormattedMessage id="select-add-known-customer-button" />
1905
+ </Button>
1906
+ </Col>
1907
+ </Row>
1908
+ <Row>
1909
+ <Col xs={3}>
1910
+ <Button
1911
+ style={{ marginTop: '25px' }}
1912
+ onClick={openModalUnknownCust}
1913
+ >
1914
+ <FormattedMessage id="describe-add-unknown-customer-button" />
1915
+ </Button>
1916
+ </Col>
1917
+ </Row>
1918
+ <Row>
1919
+ <Col xs={6}>
1920
+ <Label style={{ marginTop: '5px' }} size="medium" tag="h2" id='customer-list-label'>
1921
+ <b>{customersListLabel}</b>
1922
+ </Label>
1923
+ </Col>
1924
+ </Row>
1925
+ <Row>
1926
+ <Col xs={12}>
1927
+ <List
1928
+ aria-labelledby='customer-list-label'
1929
+ listStyle="bullets"
1930
+ label="Customers"
1931
+ items={allCustomers}
1932
+ isEmptyMessage={
1933
+ <FormattedMessage
1934
+ id="customers-list-is-empty-message"
1935
+ values={{
1936
+ bold: (chunks) => (
1937
+ <strong style={{ color: '#A12A2A' }}>
1938
+ {chunks}
1939
+ </strong>
1940
+ )
1941
+ }}
1942
+ />
1943
+ }
1944
+ itemFormatter={itemFormatterCustomers}
1945
+ />
1946
+ </Col>
1947
+ </Row>
1948
+ </>
1949
+
1950
+ )}
1951
+ </Accordion>
1952
+
1953
+ <Accordion
1954
+ label={<FormattedMessage id="accordion-label-incident" />}
1955
+ >
1956
+ <Row>
1957
+ <Col xs={3}>
1958
+ <Select
1959
+ required
1960
+ label={
1961
+ <FormattedMessage id="edit-pane.incident-location-select-label" />
1962
+ }
1963
+ name="incidentLocation"
1964
+ value={formData.incidentLocation}
1965
+ dataOptions={locationDataOptions}
1966
+ onChange={handleChange}
1967
+ />
1968
+ </Col>
1969
+
1970
+ <Col xs={3}>
1971
+ <Select
1972
+ label={
1973
+ <FormattedMessage id="edit-pane.sub-location-select-label" />
1974
+ }
1975
+ name="subLocation"
1976
+ value={formData.subLocation}
1977
+ dataOptions={subLocationsDataOptions}
1978
+ onChange={handleChange}
1979
+ />
1980
+ </Col>
1981
+ </Row>
1982
+
1983
+ <Row>
1984
+ <Col xs={3}>
1985
+ <Datepicker
1986
+ required
1987
+ name="dateTimeOfIncident"
1988
+ value={formData.dateTimeOfIncident}
1989
+ label={
1990
+ <FormattedMessage id="edit-pane.date-of-incident-date-picker-label" />
1991
+ }
1992
+ onChange={handleChange}
1993
+ />
1994
+ </Col>
1995
+
1996
+ <Col xs={2}>
1997
+ <Timepicker
1998
+ required
1999
+ name="timeOfIncident"
2000
+ value={formData.timeOfIncident}
2001
+ label={
2002
+ <FormattedMessage id="edit-pane.time-of-incident-date-picker-label" />
2003
+ }
2004
+ onChange={handleChange}
2005
+ />
2006
+ </Col>
2007
+
2008
+ <Col xs={2} style={{ marginTop: '25px' }}>
2009
+ <Checkbox
2010
+ label='Approximate time'
2011
+ name='isApproximateTime'
2012
+ checked={formData.isApproximateTime}
2013
+ onChange={handleChange}
2014
+ />
2015
+ </Col>
2016
+ </Row>
2017
+
2018
+ <Row>
2019
+ <Col xs={4}>
2020
+ <Button
2021
+ onClick={openModalSelectTypes}
2022
+ style={{ marginTop: '15px' }}
2023
+ >
2024
+ <FormattedMessage id="edit-pane.select-add-incident-type-button" />
2025
+ </Button>
2026
+ </Col>
2027
+ </Row>
2028
+
2029
+ <Row>
2030
+ <Col xs={8} style={{ paddingLeft: '20px' }}>
2031
+ <Label style={{ marginTop: '5px' }} size="medium" tag="h2">
2032
+ <b>{incidentTypesListLabel}</b>
2033
+ </Label>
2034
+ <List
2035
+ listStyle="bullets"
2036
+ label={incidentTypesListLabel}
2037
+ items={preparedIncidentTypes}
2038
+ isEmptyMessage={
2039
+ <FormattedMessage
2040
+ id="incident-types-list-is-empty-message"
2041
+ values={{
2042
+ bold: (chunks) => (
2043
+ <strong style={{ color: '#A12A2A' }}>
2044
+ {chunks}
2045
+ </strong>
2046
+ ),
2047
+ }}
2048
+ />
2049
+ }
2050
+ itemFormatter={itemFormatterIncidentTypes}
2051
+ />
2052
+ </Col>
2053
+ </Row>
2054
+
2055
+ <Row style={{ marginTop: '25px' }}>
2056
+ <Col xs={6}>
2057
+ <Editor
2058
+ required
2059
+ label={
2060
+ <FormattedMessage id="edit-pane.incident-description" />
2061
+ }
2062
+ defaultValue={formData.detailedDescriptionOfIncident}
2063
+ onChange={handleDescriptionChange}
2064
+ onBlur={handleEditorBlur}
2065
+ onKeyDown={handleEditorKeyDown}
2066
+ modules={editorModules}
2067
+ />
2068
+ </Col>
2069
+ </Row>
2070
+
2071
+ <Row>
2072
+ <Col xs={2} style={{ paddingTop: '25px' }}>
2073
+ <Button onClick={openModalSelectWitness}>
2074
+ <FormattedMessage id="select-add-witness-button" />
2075
+ </Button>
2076
+ </Col>
2077
+ </Row>
2078
+
2079
+ <Row>
2080
+ <Col xs={2} style={{ paddingTop: '25px' }}>
2081
+ <Button onClick={handleAddSelfAsWitness}>
2082
+ <FormattedMessage id="add-self-witness-button" />
2083
+ </Button>
2084
+ </Col>
2085
+ </Row>
2086
+
2087
+ <Row>
2088
+ <Col xs={2} style={{ paddingTop: '25px' }}>
2089
+ <Button
2090
+ onClick={handleOpenAddCustomWitness}
2091
+ >
2092
+ <FormattedMessage id="add-custom-witness-button" />
2093
+ </Button>
2094
+ </Col>
2095
+ </Row>
2096
+
2097
+ <Row>
2098
+ <Col xs={2} style={{ paddingTop: '25px' }}>
2099
+ <Button onClick={handleOpenModalLinkIncident}>
2100
+ <FormattedMessage id="link-to-button" />
2101
+ </Button>
2102
+ </Col>
2103
+ </Row>
2104
+
2105
+ <Row>
2106
+ <Col xs={8}>
2107
+ <Label style={{ marginTop: '5px' }} size="medium" tag="h2">
2108
+ <FormattedMessage
2109
+ id="witnesses-list-label"
2110
+ defaultMessage="{count, plural, one {{bold}Witness} other {{bold}Witnesses}}"
2111
+ values={{
2112
+ count: allWitnesses.length,
2113
+ bold: (chunks) => <strong>{chunks}</strong>,
2114
+ }}
2115
+ />
2116
+ </Label>
2117
+ <List
2118
+ listStyle="bullets"
2119
+ label={witnessesListLabel}
2120
+ items={allWitnesses}
2121
+ isEmptyMessage={
2122
+ <FormattedMessage
2123
+ id="witnesses-list-is-empty-message"
2124
+ values={{
2125
+ bold: (chunks) => (
2126
+ <strong style={{ color: '#A12A2A' }}>
2127
+ {chunks}
2128
+ </strong>
2129
+ ),
2130
+ }}
2131
+ />
2132
+ }
2133
+ itemFormatter={witnessItemFormatter}
2134
+ />
2135
+ </Col>
2136
+ </Row>
2137
+
2138
+ {linkedToSummaries.length > 0 ? (
2139
+ <Row style={{ marginTop: '25px' }}>
2140
+ <Col xs={2}>
2141
+ <KeyValue
2142
+ label={<FormattedMessage id="linked-incidents-label"/>}
2143
+ value={
2144
+ <div style={{ display: 'grid', rowGap: '10px' }}>
2145
+ {linkedToSummaries.map((ltS) => (
2146
+ <LinkedIncident
2147
+ key={ltS.id}
2148
+ summaryObj={ltS}
2149
+ onDelete={handleTrashLinkedIncident}
2150
+ renderContext='create-edit'
2151
+ />
2152
+ ))}
2153
+ </div>
2154
+ }
2155
+ />
2156
+ </Col>
2157
+ </Row>
2158
+ ) : null}
2159
+ </Accordion>
2160
+
2161
+ <Accordion label={<FormattedMessage id="accordion-label-media" />}>
2162
+ <div>
2163
+ {mediaArray.map((attachment) => (
2164
+ <GetMedia
2165
+ context='thumbnail'
2166
+ contentType={attachment.contentType}
2167
+ key={attachment.id}
2168
+ id={id}
2169
+ imageId={attachment.id}
2170
+ mediaHandler={(mediaUrl) => handleMediaUrl(mediaUrl, attachment.id)}
2171
+ />
2172
+ ))}
2173
+ </div>
2174
+
2175
+ <Row style={{ margin: '25px' }}>
2176
+ <Col xs={1} style={{ visibility: 'hidden' }}></Col>
2177
+ {mediaArray.slice(0, 5).map((attachment) => (
2178
+ <Col xs={2} key={attachment.id} style={{ minHeight: '200px' }}>
2179
+ {loadingStatus[attachment.id] && isImageArrayLoading ? <ThumbnailSkeleton />
2180
+ : attachment.toBeRemoved ? (
2181
+ <ThumbnailMarkRemoval
2182
+ undoId={attachment.id}
2183
+ handleUndo={handleUndo}
2184
+ />
2185
+ ) : <Thumbnail
2186
+ key={attachment.id}
2187
+ handleMarkForRemoval={handleMarkForRemoval}
2188
+ mediaId={attachment.id}
2189
+ src={mediaSrc[attachment.id]}
2190
+ alt={attachment.description}
2191
+ imageDescription={attachment.description}
2192
+ contentType={attachment.contentType}
2193
+ style={thumbnailStyle}
2194
+ isMarkedForRemoval={attachment.toBeRemoved}
2195
+ />
2196
+ }
2197
+ </Col>
2198
+ ))}
2199
+ </Row>
2200
+ <Row style={{ margin: '25px' }}>
2201
+ <Col xs={1} style={{ visibility: 'hidden' }}></Col>
2202
+ {mediaArray.slice(5, 10).map((attachment) => (
2203
+ <Col xs={2} key={attachment.id}>
2204
+ {loadingStatus[attachment.id] && isImageArrayLoading ? <ThumbnailSkeleton />
2205
+ : attachment.toBeRemoved ? (
2206
+ <ThumbnailMarkRemoval
2207
+ undoId={attachment.id}
2208
+ handleUndo={handleUndo}
2209
+ />
2210
+ ) : <Thumbnail
2211
+ key={attachment.id}
2212
+ handleMarkForRemoval={handleMarkForRemoval}
2213
+ mediaId={attachment.id}
2214
+ src={mediaSrc[attachment.id]}
2215
+ alt={attachment.description}
2216
+ imageDescription={attachment.description}
2217
+ contentType={attachment.contentType}
2218
+ style={thumbnailStyle}
2219
+ />
2220
+ }
2221
+ </Col>
2222
+ ))}
2223
+ </Row>
2224
+
2225
+ <Row style={{ margin: '25px' }}>
2226
+ <Col xs={1} style={{ visibility: 'hidden' }}></Col>
2227
+ {unsavedMediaArray.map((attachment) => (
2228
+ <Col xs={2} key={attachment.id}>
2229
+ <ThumbnailTempPreSave
2230
+ contentType={attachment.contentType}
2231
+ handleRemoveUnsavedMedia={handleRemoveUnsavedMedia}
2232
+ key={attachment.id}
2233
+ mediaId={attachment.id}
2234
+ src={attachment.filePreviewUrl}
2235
+ alt={attachment.description}
2236
+ imageDescription={attachment.description}
2237
+ style={thumbnailStyle}
2238
+ />
2239
+ </Col>
2240
+ ))}
2241
+ </Row>
2242
+
2243
+ <Row style={{ marginTop: '25px' }}>
2244
+ <Col xs={2}>
2245
+ <Button onClick={handleOpenModalAddMedia}>
2246
+ <FormattedMessage id="add-media-button" />
2247
+ </Button>
2248
+ </Col>
2249
+ </Row>
2250
+ </Accordion>
2251
+ <Accordion label={<FormattedMessage id="accordion-label-documents"/>}>
2252
+ <div>
2253
+ {documents.map((doc) => (
2254
+ <GetMedia
2255
+ context='document'
2256
+ key={doc.id}
2257
+ id={id}
2258
+ imageId={doc.id}
2259
+ mediaHandler={(mediaUrl) => handleMediaUrl(mediaUrl, doc.id)}
2260
+ />
2261
+ ))}
2262
+ </div>
2263
+
2264
+ {/* This sorts and maps document buttons. The most recent documents are placed at the visual top of list with a clock icon to identify the most current trespass document(s). */}
2265
+ <div>
2266
+ <Col xs={1} style={{ visibility: 'hidden' }}></Col>
2267
+ {sortedDocuments.map((doc) => (
2268
+ <Row xs={2} style={{ marginLeft: '15px' }} key={doc.id}>
2269
+ {doc.toBeRemoved ? (
2270
+ <div
2271
+ style={{
2272
+ display: 'flex',
2273
+ alignItems: 'center',
2274
+ marginTop: '10px',
2275
+ }}
2276
+ >
2277
+ <Button
2278
+ onClick={() => handleUndo(doc.id)}
2279
+ style={{ marginTop: '25px' }}
2280
+ buttonStyle='default'
2281
+ >
2282
+ <FormattedMessage id='undo-button' />
2283
+ </Button>
2284
+ </div>
2285
+ ) : (
2286
+ <div
2287
+ style={{
2288
+ display: 'flex',
2289
+ alignItems: 'center',
2290
+ marginLeft: '10px',
2291
+ }}
2292
+ >
2293
+ <Button
2294
+ allowAnchorClick={true}
2295
+ href={mediaSrc[doc.id]}
2296
+ target='_blank'
2297
+ style={{ marginTop: '15px' }}
2298
+ >
2299
+ {doc.description}
2300
+ </Button>
2301
+
2302
+ {/* if most current document, render clock icon */}
2303
+ {mostCurrentTrespassDocIds.includes(doc.id) && (
2304
+ <span style={{ marginLeft: '8px' }}>
2305
+ <Icon icon='clock' size='small' />
2306
+ </span>
2307
+ )}
2308
+
2309
+ {/*
2310
+ Track application may or may not utilize delete trespass documents functionality.
2311
+ Possibly will be a configuration or specific perm in future release.
2312
+ Commenting out for now, rest of logic is present.
2313
+ */}
2314
+ {/*
2315
+ <div style={{ marginLeft: '5px' }}>
2316
+ <button onClick={() => handleMarkForRemoval(doc.id)}>
2317
+ <Icon icon='trash'></Icon>
2318
+ </button>
2319
+ </div>
2320
+ */}
2321
+ </div>
2322
+ )}
2323
+ </Row>
2324
+ ))}
2325
+ </div>
2326
+ </Accordion>
2327
+ </AccordionSet>
2328
+ </Pane>)}
2329
+ </>
2330
+ )})
2331
+ </>
2332
+ );
2333
+ };
2334
+ export default EditPane;