@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,741 @@
1
+ import React, { useState, useEffect, useMemo, useRef } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import DOMPurify from 'dompurify';
4
+ import { decode } from 'html-entities';
5
+ import { FormattedMessage } from 'react-intl';
6
+ import {
7
+ AccordionSet,
8
+ Accordion,
9
+ Button,
10
+ Checkbox,
11
+ Col,
12
+ Datepicker,
13
+ Editor,
14
+ InfoPopover,
15
+ Modal,
16
+ ModalFooter,
17
+ Pane,
18
+ Paneset,
19
+ Row,
20
+ Select,
21
+ TextField,
22
+ AutoSuggest,
23
+ } from '@folio/stripes/components';
24
+ import { useIncidents } from '../../contexts/IncidentContext';
25
+ import GetLocationsInService from '../../settings/GetLocationsInService';
26
+ import GetTrespassReasons from '../../settings/GetTrespassReasons';
27
+ import convertUTCISOToPrettyDate from './helpers/convertUTCISOToPrettyDate';
28
+ import {
29
+ isDeclarationOfServiceEmpty,
30
+ validateTrespass,
31
+ hasTrespassReason
32
+ } from './helpers/validateTrespassDetails';
33
+ import { isSameHtml } from './helpers/isSameHtml.js';
34
+ import css from './ModalStyle.css';
35
+
36
+ const ModalTrespass = ({
37
+ customerID,
38
+ allCustomers, // list of customers in EditPane context use of modal
39
+ setAllCustomers,
40
+ onStagedTrespassUpdate,
41
+ onUpdateDeclaration = () => {}, // Only EditPane passes this prop
42
+ updateDeclarationArray = [], // Only EditPane passes this prop
43
+ modalContext = '', // Only CreatePane passes this prop
44
+ customersWithoutDeclaration = new Set(), // Only EditPane passes this prop
45
+ isNewlySelected
46
+ }) => {
47
+ const {
48
+ isModalTrespass,
49
+ closeModalTrespass,
50
+ selectedCustomers, // list of customers in CreatePane context use of modal
51
+ setSelectedCustomers,
52
+ locationsInService,
53
+ trespassReasons
54
+ } = useIncidents();
55
+
56
+ const allowedReasonIds = useMemo(() => {
57
+ const list = (trespassReasons ?? []).filter(r => !r.isSuppressed);
58
+ return new Set(list.map(r => r.id));
59
+ }, [trespassReasons]);
60
+
61
+ const [localUpdateDeclaration, setLocalUpdateDeclaration] = useState(false);
62
+ const reasonsSectionRef = useRef(null);
63
+ const [trespassData, setTrespassData] = useState({
64
+ dateOfOccurrence: '',
65
+ exclusionOrTrespassBasedOn: [],
66
+ witnessedBy: { witnesses: [] },
67
+ endDateOfTrespass: '',
68
+ declarationOfService: {
69
+ date: '',
70
+ placeSigned: '',
71
+ title: '',
72
+ signature: false,
73
+ },
74
+ description: ''
75
+ });
76
+ const [customersArray, setCustomersArray] = useState([]);
77
+ const [workWithEdit, setWorkWithEdit] = useState(false);
78
+ const [submitOk, setSubmitOk] = useState(false);
79
+ const [useIncidentDesc, setUseIncidentDesc] = useState(true);
80
+ const [formTouched, setFormTouched] = useState(false);
81
+ // const [isNewlySelected, setIsNewlySelected] = useState(false);
82
+ const shouldHideUpdateDeclaration = customersWithoutDeclaration?.has(customerID);
83
+ const initializedByCustomerRef = useRef(new Set()); // remember who we've init
84
+ const [trInputValue, setTrInputValue] = useState(''); // trespass reason input value
85
+ const [trVisibleCount, setTrVisibleCount] = useState(5); // trespass reason vis count
86
+ const [trsHasExpanded, setTrsHasExpanded] = useState(false); // track tresp-reason exp
87
+ const trItems = useMemo(() => {
88
+ const formattedTRitems = trespassReasons
89
+ ? trespassReasons
90
+ .filter(tr => !tr.isSuppressed) // exclude suppressed
91
+ .map((tr) => ({
92
+ value: tr.id,
93
+ label: tr.reason,
94
+ isDefault: !!tr.isDefault
95
+ }))
96
+ : [{
97
+ value: '',
98
+ label: <FormattedMessage
99
+ id="search-pane.trespass-reasons-label-no-loaded"
100
+ />
101
+ }];
102
+ return formattedTRitems;
103
+ }, [trespassReasons]);
104
+
105
+ const defaultReasonObj = useMemo(() => {
106
+ const defaultTR = trespassReasons?.find(r => r.isDefault);
107
+ return defaultTR ?
108
+ {
109
+ id: defaultTR.id,
110
+ reason: defaultTR.reason
111
+ }
112
+ : null;
113
+ }, [trespassReasons]);
114
+
115
+ useEffect(() => {
116
+ if (modalContext !== 'create-mode') return;
117
+ if (!defaultReasonObj) return ;
118
+ if (initializedByCustomerRef.current.has(customerID)) return; // bail if already initialized for this customer
119
+
120
+ const hasSelection = (trespassData.exclusionOrTrespassBasedOn?.length ?? 0) > 0;
121
+
122
+ if (!hasSelection) {
123
+ setTrespassData(prev => ({
124
+ ...prev,
125
+ exclusionOrTrespassBasedOn: [defaultReasonObj]
126
+ }));
127
+ }
128
+ }, [
129
+ modalContext,
130
+ defaultReasonObj,
131
+ customerID,
132
+ setTrespassData,
133
+ trespassData.exclusionOrTrespassBasedOn
134
+ ]);
135
+
136
+ /*
137
+ handle if not in create-mode and customer does NOT have a
138
+ trespass.exclusionOrTrespassBasedOn value, we still set a defaultReasonObj
139
+ */
140
+ useEffect(() => {
141
+ if (!defaultReasonObj) return ;
142
+ if (initializedByCustomerRef.current.has(customerID)) return;
143
+ const hasSelection = (trespassData.exclusionOrTrespassBasedOn?.length ?? 0) > 0;
144
+
145
+ if (!hasSelection) {
146
+ setTrespassData(prev => ({
147
+ ...prev,
148
+ exclusionOrTrespassBasedOn: [defaultReasonObj]
149
+ }));
150
+ }
151
+ }, [
152
+ defaultReasonObj,
153
+ customerID,
154
+ setTrespassData,
155
+ trespassData.exclusionOrTrespassBasedOn
156
+ ]);
157
+
158
+ const allTrespassReasons = useMemo(() => {
159
+ const inVal = (trInputValue || '').toLowerCase();
160
+ return trItems.filter(item => (item.label || '').toLowerCase().includes(inVal));
161
+ }, [trItems, trInputValue]);
162
+
163
+ const sortedTrespassReasons = useMemo(() => {
164
+ // default first, then alpha by label
165
+ const arr = [...allTrespassReasons];
166
+ arr.sort((a, b) => (
167
+ Number(b.isDefault) - Number(a.isDefault)
168
+ ) || a.label.localeCompare(b.label));
169
+ return arr;
170
+ }, [allTrespassReasons]);
171
+
172
+ const filteredTrespassReasons = useMemo(() => {
173
+ return sortedTrespassReasons.slice(0, trVisibleCount)
174
+ }, [sortedTrespassReasons, trVisibleCount]);
175
+
176
+ const handleMoreTRsClick = () => {
177
+ setTrVisibleCount((prevCount) => {
178
+ return Math.min(prevCount + 5, sortedTrespassReasons.length)
179
+ });
180
+ };
181
+
182
+ const loadMoreTRs = () => {
183
+ handleMoreTRsClick();
184
+ if (!trsHasExpanded) {
185
+ setTrsHasExpanded(true);
186
+ };
187
+ };
188
+
189
+ // A customer can only (re)declare if they have at least one current (non-suppressed) reason selected
190
+ const isEligibleToRedeclare = useMemo(() => {
191
+ const arr = trespassData.exclusionOrTrespassBasedOn || [];
192
+ for (const item of arr) {
193
+ const id = typeof item === 'string' ? item : item?.id;
194
+ if (id && allowedReasonIds.has(id)) return true;
195
+ }
196
+ return false;
197
+ }, [trespassData.exclusionOrTrespassBasedOn, allowedReasonIds]);
198
+
199
+ const showUpdateCheckbox = modalContext !== 'create-mode' &&
200
+ !isNewlySelected && (!shouldHideUpdateDeclaration || localUpdateDeclaration);
201
+
202
+ const selectedObjs = trespassData.exclusionOrTrespassBasedOn || [];
203
+ // const reasonsOk = hasTrespassReason(trespassData);
204
+ // const showReasonError = !isEligibleToRedeclare && formTouched;
205
+
206
+ const handleTRtoggle = (id, reason, checked) => {
207
+ setTrespassData(prev => {
208
+ const prevArr = prev.exclusionOrTrespassBasedOn || [];
209
+ const next = checked
210
+ // add
211
+ ? (prevArr.some(x => x?.id === id) ? prevArr : [...prevArr, { id, reason }])
212
+ // remove by id
213
+ : prevArr.filter(x => x?.id !== id);
214
+
215
+ return { ...prev, exclusionOrTrespassBasedOn: next };
216
+ });
217
+ setFormTouched(true);
218
+ };
219
+
220
+ const handleTRFilterChange = (value) => {
221
+ // console.log("@handleTRFilterChange - value: ", value)
222
+ setTrInputValue(value);
223
+ };
224
+
225
+ const trContainerStyle = {
226
+ maxHeight: trsHasExpanded ? '175px' : '125px',
227
+ overflowX: 'clip',
228
+ overflowY: 'auto',
229
+ marginTop: '8px'
230
+ };
231
+
232
+ // setting customersArray with either allCustomers data OR selectedCustomers data
233
+ useEffect(() => {
234
+ if (allCustomers) {
235
+ setWorkWithEdit(true);
236
+ setCustomersArray(allCustomers); // setting if rendered via @EditPane
237
+ } else if (selectedCustomers) {
238
+ setCustomersArray(selectedCustomers); // setting if rendered via @CreatePane
239
+ }
240
+ }, [selectedCustomers, allCustomers]);
241
+
242
+ useEffect(() => {
243
+ const valid = validateTrespass(trespassData);
244
+ setSubmitOk(valid);
245
+ }, [trespassData]);
246
+
247
+ const rebuildFromParent = () => {
248
+ const currentCustomer = (allCustomers || selectedCustomers || []).find(c => c.id === customerID);
249
+ if (currentCustomer && currentCustomer.trespass) {
250
+ const next = {
251
+ dateOfOccurrence: convertUTCISOToPrettyDate(currentCustomer.trespass.dateOfOccurrence) || '',
252
+ exclusionOrTrespassBasedOn: currentCustomer.trespass.exclusionOrTrespassBasedOn || [],
253
+ endDateOfTrespass: convertUTCISOToPrettyDate(currentCustomer.trespass.endDateOfTrespass) || '',
254
+ declarationOfService: {
255
+ date: convertUTCISOToPrettyDate(currentCustomer.trespass.declarationOfService?.date) || '',
256
+ placeSigned: currentCustomer.trespass.declarationOfService?.placeSigned || '',
257
+ title: currentCustomer.trespass.declarationOfService?.title || '',
258
+ signature: currentCustomer.trespass.declarationOfService?.signature || false,
259
+ },
260
+ description: currentCustomer.trespass.description || ''
261
+ };
262
+
263
+ setTrespassData(next);
264
+ draftRef.current = next.description || '';
265
+ const useDefault = (next.description || '').trim() === '';
266
+ setUseIncidentDesc(useDefault);
267
+
268
+ // recompute validity gating
269
+ const hasAnyCurrentValidReason = (next.exclusionOrTrespassBasedOn || []).some(x => {
270
+ const id = typeof x === 'string' ? x : x?.id;
271
+ return id && allowedReasonIds.has(id);
272
+ });
273
+ setSubmitOk(validateTrespass(next));
274
+ setFormTouched(!hasAnyCurrentValidReason);
275
+ setLocalUpdateDeclaration(updateDeclarationArray.includes(customerID));
276
+ } else {
277
+ const empty = {
278
+ dateOfOccurrence: '',
279
+ exclusionOrTrespassBasedOn: [],
280
+ witnessedBy: { witnesses: [] },
281
+ endDateOfTrespass: '',
282
+ declarationOfService: { date: '', placeSigned: '', title: '', signature: false },
283
+ };
284
+ setTrespassData(empty);
285
+ draftRef.current = '';
286
+ setUseIncidentDesc(true);
287
+ setFormTouched(false);
288
+ setSubmitOk(false);
289
+ setLocalUpdateDeclaration(false);
290
+ }
291
+ };
292
+
293
+ useEffect(() => {
294
+ if (isModalTrespass) rebuildFromParent();
295
+ }, [isModalTrespass]);
296
+
297
+ useEffect(() => {
298
+ if (isModalTrespass) {
299
+ setLocalUpdateDeclaration(updateDeclarationArray.includes(customerID));
300
+ }
301
+ }, [isModalTrespass, updateDeclarationArray, customerID]);
302
+
303
+ const handleDismiss = () => {
304
+ rebuildFromParent();
305
+ setWorkWithEdit(false);
306
+ closeModalTrespass();
307
+ };
308
+
309
+ useEffect(() => {
310
+ if (formTouched) {
311
+ setSubmitOk(validateTrespass(trespassData))
312
+ }
313
+ }, [formTouched, trespassData]);
314
+
315
+ const locationDataOptions = useMemo(() => {
316
+ const defaultValueLabel = [{
317
+ value: '',
318
+ label: <FormattedMessage
319
+ id="create-pane.locationDataOptions-label-select-location"/>
320
+ }];
321
+ const formattedLocations = locationsInService
322
+ ? locationsInService.map((loc) => ({
323
+ value: loc.id,
324
+ label: loc.location
325
+ }))
326
+ : [{
327
+ value: '',
328
+ label: <FormattedMessage
329
+ id="create-pane.locationDataOptions-label-no-loaded"/>
330
+ }];
331
+
332
+ return [
333
+ ...defaultValueLabel,
334
+ ...formattedLocations,
335
+ ];
336
+ }, [locationsInService]);
337
+
338
+ const handleChange = (event) => {
339
+ const { name, value, checked } = event.target;
340
+ if (name === 'exclusionOrTrespassBasedOn') {
341
+ handleExclusionCheckboxChange(event);
342
+ } else if (name === 'declarationOfService.signature') {
343
+ setTrespassData((prevData) => ({
344
+ ...prevData,
345
+ declarationOfService: {
346
+ ...prevData.declarationOfService,
347
+ signature: checked,
348
+ },
349
+ }));
350
+ } else if (name === 'declarationOfService.placeSigned') {
351
+ setTrespassData((prevData) => ({
352
+ ...prevData,
353
+ declarationOfService: {
354
+ ...prevData.declarationOfService,
355
+ placeSigned: value,
356
+ },
357
+ }));
358
+ } else if (name === 'declarationOfService.date') {
359
+ setTrespassData((prevData) => ({
360
+ ...prevData,
361
+ declarationOfService: {
362
+ ...prevData.declarationOfService,
363
+ date: value,
364
+ },
365
+ }));
366
+ } else if (name === 'declarationOfService.title') {
367
+ setTrespassData((prevData) => ({
368
+ ...prevData,
369
+ declarationOfService: {
370
+ ...prevData.declarationOfService,
371
+ title: value,
372
+ },
373
+ }));
374
+ } else {
375
+ setTrespassData((prevData) => ({
376
+ ...prevData,
377
+ [name]: value,
378
+ }));
379
+ }
380
+ setFormTouched(true);
381
+ };
382
+
383
+ /*
384
+ Keep current HTML in a ref so keystrokes update w/out setting state.
385
+ This prevents react-quill's cleanup <-> setState feedback loop that otherwise
386
+ causes maximum update depth exceeded error when links are present in editor.
387
+ */
388
+ // local unsanitized buffer - doesn't trigger React re-renders
389
+ const draftRef = useRef('');
390
+
391
+ // fire on every key-press but only mutautes the ref
392
+ const handleEditorChange = (content) => {
393
+ draftRef.current = content;
394
+ setFormTouched(true);
395
+ };
396
+
397
+ // commits once, with sanitized HTML
398
+ const handleEditorBlur = () => {
399
+ const sanitizedContent = DOMPurify.sanitize(draftRef.current);
400
+ setTrespassData(prev =>
401
+ isSameHtml(prev.description, sanitizedContent)
402
+ ? prev
403
+ : { ...prev, description: sanitizedContent}
404
+ );
405
+ };
406
+
407
+ const handleSave = () => {
408
+ const updatedTrespassData = { ...trespassData };
409
+ const updatedCustomerArray = customersArray.map((cust) => {
410
+ if (cust.id === customerID) {
411
+ // if no end date of trespass delete it
412
+ if (trespassData.endDateOfTrespass === '') {
413
+ delete updatedTrespassData.endDateOfTrespass;
414
+ };
415
+ // if no declaration of service delete it
416
+ if (
417
+ isDeclarationOfServiceEmpty(updatedTrespassData.declarationOfService)
418
+ ) {
419
+ delete updatedTrespassData.declarationOfService;
420
+ };
421
+ if (useIncidentDesc) {
422
+ // using default, always delete custom override
423
+ delete updatedTrespassData.description;
424
+ } else {
425
+ const sanitizedContent = DOMPurify.sanitize(draftRef.current);
426
+ const rawText = decode(sanitizedContent)
427
+ .replace(/<\/?[^>]+>/g, '') // strip tags
428
+ .trim();
429
+
430
+ if (rawText === '') {
431
+ // Editor visually empty - treat like using default
432
+ delete updatedTrespassData.description;
433
+ } else {
434
+ // Editor has meaningful input - persist
435
+ updatedTrespassData.description = sanitizedContent;
436
+ };
437
+ }
438
+ // const object = {...cust, trespass: updatedTrespassData};
439
+
440
+ return {
441
+ ...cust,
442
+ trespass: updatedTrespassData,
443
+ };
444
+ }
445
+ return cust;
446
+ });
447
+
448
+ onStagedTrespassUpdate?.(customerID, updatedTrespassData);
449
+ const parentHas = updateDeclarationArray.includes(customerID);
450
+ if (localUpdateDeclaration !== parentHas) {
451
+ onUpdateDeclaration(customerID)
452
+ };
453
+
454
+ if (workWithEdit) {
455
+ // setting via @EditPane
456
+ setAllCustomers(updatedCustomerArray);
457
+ } else {
458
+ // setting via @CreatePane
459
+ setSelectedCustomers(updatedCustomerArray);
460
+ };
461
+ setTrespassData({
462
+ dateOfOccurrence: '',
463
+ exclusionOrTrespassBasedOn: [],
464
+ witnessedBy: { witnesses: [] },
465
+ endDateOfTrespass: '',
466
+ declarationOfService: {
467
+ date: '',
468
+ placeSigned: '',
469
+ title: '',
470
+ signature: false,
471
+ },
472
+ description: ''
473
+ })
474
+ draftRef.current = '';
475
+ setWorkWithEdit(false);
476
+ closeModalTrespass();
477
+ };
478
+
479
+ if (!isModalTrespass) {
480
+ return null;
481
+ };
482
+
483
+ const footer = (
484
+ <ModalFooter>
485
+ <Button
486
+ onClick={handleSave}
487
+ buttonStyle="primary"
488
+ marginBottom0
489
+ // must pass validation and have more than 1 current reason selected
490
+ disabled={!(submitOk && isEligibleToRedeclare)}
491
+ >
492
+ <FormattedMessage id="close-continue-button" />
493
+ </Button>
494
+ <Button onClick={handleDismiss}>
495
+ <FormattedMessage id="cancel-button" />
496
+ </Button>
497
+ </ModalFooter>
498
+ );
499
+
500
+ return (
501
+ <Modal
502
+ style={{
503
+ display: 'flex',
504
+ flexDirection: 'column',
505
+ maxHeight: '90vh',
506
+ minHeight: '500px'
507
+ }}
508
+ open
509
+ dismissible
510
+ closeOnBackgroundClick
511
+ label={<FormattedMessage id="modal-trespass-label" />}
512
+ size="large"
513
+ onClose={handleDismiss}
514
+ footer={footer}
515
+ contentClass={css.modalContent}
516
+ >
517
+ <GetLocationsInService />
518
+ <GetTrespassReasons />
519
+
520
+ <Paneset style={{ height: '100%', flexGrow: 1 }}>
521
+ <Pane
522
+ defaultWidth="100%"
523
+ style={{ overflowY: 'auto', flexGrow: 1 }}
524
+ >
525
+ <AccordionSet>
526
+ <Accordion
527
+ label={
528
+ <FormattedMessage id="modal-trespass.accordion-trespass-details-label" />
529
+ }
530
+ >
531
+ <Row>
532
+ <Col xs={4}>
533
+ <AutoSuggest
534
+ value={trInputValue}
535
+ items={[]} // using as filter input
536
+ onChange={handleTRFilterChange}
537
+ menuStyle={{ display: 'none' }}
538
+ renderValue={(val) => val || ''} // render item in input field
539
+ />
540
+ </Col>
541
+ </Row>
542
+
543
+ <Row xs={12} style={{ marginLeft: '10px' }}>
544
+ <Col>
545
+ <div style={trContainerStyle} ref={reasonsSectionRef}>
546
+ {filteredTrespassReasons.map((item) => (
547
+ <Checkbox
548
+ key={item.value}
549
+ label={item.label}
550
+ value={item.value}
551
+ checked={selectedObjs.some(sel => sel?.id === item.value)}
552
+ onChange={(e) => handleTRtoggle(item.value, item.label, e.target.checked)}
553
+ />
554
+ ))}
555
+ </div>
556
+
557
+ <div style={{ marginTop: '2px' }}>
558
+ {trVisibleCount < allTrespassReasons.length && (
559
+ <Button
560
+ onClick={loadMoreTRs}
561
+ >
562
+ <FormattedMessage id="more-button" />
563
+ </Button>
564
+ )}
565
+ </div>
566
+ </Col>
567
+ </Row>
568
+
569
+ <Row style={{ marginTop: '25px' }}>
570
+ <Col xs={3}>
571
+ <Checkbox
572
+ name="useIncidentDescription"
573
+ label={
574
+ <FormattedMessage id="modal-trespass-checkbox-use-incident-description" />
575
+ }
576
+ checked={useIncidentDesc}
577
+ onChange={() => setUseIncidentDesc(prev => !prev)}
578
+ />
579
+ </Col>
580
+ <Col xs={1}>
581
+ <InfoPopover
582
+ content="Uncheck this box to provide a Trespass description different than the incident description on the trespass document."
583
+ iconSize='medium'
584
+ />
585
+ </Col>
586
+ </Row>
587
+
588
+ {!useIncidentDesc && (
589
+ <Row style={{ marginTop: '25px' }}>
590
+ <Col xs={8}>
591
+ <Editor
592
+ label='Trespass description'
593
+ value={draftRef.current}
594
+ onChange={handleEditorChange}
595
+ onBlur={handleEditorBlur}
596
+ />
597
+ </Col>
598
+ </Row>
599
+ )}
600
+
601
+ <Row style={{ marginTop: '25px' }}>
602
+ <Col xs={3}>
603
+ <Datepicker
604
+ label={
605
+ <FormattedMessage id="modal-trespass.datepicker-end-date-trespass" />
606
+ }
607
+ name="endDateOfTrespass"
608
+ value={trespassData.endDateOfTrespass}
609
+ onChange={handleChange}
610
+ />
611
+ </Col>
612
+ </Row>
613
+ </Accordion>
614
+
615
+ <Accordion
616
+ label={
617
+ <FormattedMessage id="modal-trespass.accordion-declaration-of-service-label" />
618
+ }
619
+ >
620
+ <Row style={{ marginTop: '25px' }}>
621
+ <Col xs={3}>
622
+ <Datepicker
623
+ label={
624
+ <FormattedMessage id="modal-trespass.datepicker-date-served" />
625
+ }
626
+ name="declarationOfService.date"
627
+ value={
628
+ trespassData.declarationOfService
629
+ ? trespassData.declarationOfService.date
630
+ : ''
631
+ }
632
+ onChange={handleChange}
633
+ />
634
+ </Col>
635
+ <Col xs={3}>
636
+ <Select
637
+ label={
638
+ <FormattedMessage id="modal-trespass.select-place-signed" />
639
+ }
640
+ dataOptions={locationDataOptions}
641
+ name="declarationOfService.placeSigned"
642
+ value={
643
+ trespassData.declarationOfService
644
+ ? trespassData.declarationOfService.placeSigned
645
+ : ''
646
+ }
647
+ onChange={handleChange}
648
+ />
649
+ </Col>
650
+ <Col xs={3}>
651
+ <TextField
652
+ label={
653
+ <FormattedMessage id="modal-trespass.textField-title" />
654
+ }
655
+ name="declarationOfService.title"
656
+ value={
657
+ trespassData.declarationOfService
658
+ ? trespassData.declarationOfService.title
659
+ : ''
660
+ }
661
+ onChange={handleChange}
662
+ />
663
+ </Col>
664
+ </Row>
665
+
666
+ <Row style={{
667
+ marginTop: '25px',
668
+ marginBottom: modalContext === 'create-mode' || isNewlySelected || shouldHideUpdateDeclaration ? '120px' : '0'
669
+ }}>
670
+ <Col xs={3}>
671
+ <Checkbox
672
+ label={
673
+ <FormattedMessage id="modal-trespass.checkbox-signed" />
674
+ }
675
+ name="declarationOfService.signature"
676
+ checked={
677
+ trespassData.declarationOfService
678
+ ? trespassData.declarationOfService.signature
679
+ : false
680
+ }
681
+ value={
682
+ trespassData.declarationOfService
683
+ ? trespassData.declarationOfService.signature
684
+ : false
685
+ }
686
+ onChange={handleChange}
687
+ />
688
+ </Col>
689
+ </Row>
690
+
691
+ {showUpdateCheckbox && (
692
+ <Row style={{ marginTop: '5px', marginBottom: '120px' }}>
693
+ <Col xs={3}>
694
+ <Checkbox
695
+ label={
696
+ <FormattedMessage id="modal-trespass.checkbox-update-declaration" />
697
+ }
698
+ // must have a valid, current (non-suppressed) reason AND modal is otherwise valid
699
+ disabled={!submitOk || !isEligibleToRedeclare}
700
+ name="updateDeclaration"
701
+ // checked={updateDeclarationArray.includes(customerID)}
702
+ checked={localUpdateDeclaration}
703
+ // onChange={() => onUpdateDeclaration(customerID)}
704
+ onChange={() => setLocalUpdateDeclaration(prev => !prev)}
705
+ />
706
+ </Col>
707
+ <Col>
708
+ {!isEligibleToRedeclare && (
709
+ <div style={{ marginTop: 6 }}>
710
+ <InfoPopover
711
+ iconSize="medium"
712
+ content={
713
+ <FormattedMessage
714
+ id="edit-pane.update-declaration-requires-valid-reason"
715
+ />
716
+ }
717
+ />
718
+ </div>
719
+ )}
720
+ </Col>
721
+ </Row>
722
+ )}
723
+ </Accordion>
724
+ </AccordionSet>
725
+ </Pane>
726
+ </Paneset>
727
+ </Modal>
728
+ );
729
+ };
730
+
731
+ ModalTrespass.propTypes = {
732
+ customerID: PropTypes.string.isRequired,
733
+ allCustomers: PropTypes.array,
734
+ setAllCustomers: PropTypes.func,
735
+ onStagedTrespassUpdate: PropTypes.func,
736
+ onUpdateDeclaration: PropTypes.func,
737
+ updateDeclarationArray: PropTypes.array,
738
+ modalContext: PropTypes.string,
739
+ customersWithoutDeclaration: PropTypes.instanceOf(Set)
740
+ };
741
+ export default ModalTrespass;