@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,111 @@
1
+
2
+ import { isSameHtml } from './isSameHtml.js';
3
+ import deepNormalizeForComparison from './deepNormalizeForComparison.js';
4
+
5
+ // implemented to assert meaningful change for allowing save&close on EditPane
6
+ const hasTopLevelFormChanged = (
7
+ current,
8
+ initial,
9
+ selectedCustomers = [],
10
+ selectedWitnesses = [],
11
+ unsavedMediaArray = [],
12
+ customersToUpdateDeclaration = []
13
+ ) => {
14
+ const simpleKeys = [
15
+ 'customerNa',
16
+ 'incidentLocation',
17
+ 'subLocation',
18
+ 'dateTimeOfIncident',
19
+ 'timeOfIncident',
20
+ 'isApproximateTime'
21
+ ];
22
+
23
+ // return true if any changes to simple keys
24
+ for (const key of simpleKeys) {
25
+ if (current[key] !== initial[key]) {
26
+ return true;
27
+ }
28
+ };
29
+
30
+ // return true if any changes to top-level detailedDescriptionOfIncident
31
+ if (!isSameHtml(current.detailedDescriptionOfIncident, initial.detailedDescriptionOfIncident)) {
32
+ return true;
33
+ };
34
+
35
+ // return true if any new customers selected for saving
36
+ if (selectedCustomers.length > 0) {
37
+ return true;
38
+ };
39
+
40
+ // return true if any new witnesses selected for saving
41
+ if (selectedWitnesses.length > 0) {
42
+ return true;
43
+ };
44
+
45
+ // return true if any media is added for saving
46
+ if ((unsavedMediaArray || []).length > 0) {
47
+ return true;
48
+ };
49
+
50
+ if ((customersToUpdateDeclaration || []).length > 0) {
51
+ return true;
52
+ };
53
+
54
+ const getSortedIds = arr => (arr || []).map(i => i.id).sort();
55
+
56
+ // return true if relevant item is removed in UI
57
+ if (
58
+ JSON.stringify(getSortedIds(current.customers)) !== JSON.stringify(getSortedIds(initial.customers)) ||
59
+ JSON.stringify(getSortedIds(current.incidentTypes)) !== JSON.stringify(getSortedIds(initial.incidentTypes)) ||
60
+ JSON.stringify(getSortedIds(current.incidentWitnesses)) !== JSON.stringify(getSortedIds(initial.incidentWitnesses)) ||
61
+ JSON.stringify(getSortedIds(current.attachments)) !== JSON.stringify(getSortedIds(initial.attachments))
62
+ ) {
63
+ return true;
64
+ };
65
+
66
+ // handle if an attachment has been marked for removal
67
+ const hasToBeRemovedMedia = (current.attachments || []).some(att => att.toBeRemoved);
68
+ if (hasToBeRemovedMedia) return true;
69
+
70
+ const normalizedWitness = (wit) => {
71
+ const {
72
+ id,
73
+ isCustom,
74
+ firstName = '',
75
+ lastName = '',
76
+ role = '',
77
+ phone = '',
78
+ email = '',
79
+ } = wit;
80
+
81
+ return deepNormalizeForComparison({
82
+ id,
83
+ isCustom,
84
+ firstName: firstName.trim(),
85
+ lastName: lastName.trim(),
86
+ ...(role.trim() && { role: role.trim() }),
87
+ ...(phone.trim() && { phone: phone.trim() }),
88
+ ...(email.trim() && { email: email.trim() }),
89
+ });
90
+ };
91
+
92
+ const initialCustomWitnesses = (initial.incidentWitnesses || []).filter(w => w.isCustom);
93
+ const currentCustomWitnesses = (current.incidentWitnesses || []).filter(w => w.isCustom);
94
+
95
+ // check for any modified custom witness
96
+ for (const initWit of initialCustomWitnesses) {
97
+ const currWit = currentCustomWitnesses.find(w => w.id === initWit.id);
98
+ if (!currWit) return true;
99
+
100
+ const normInit = normalizedWitness(initWit);
101
+ const normCurr = normalizedWitness(currWit);
102
+
103
+ if (JSON.stringify(normInit) !== JSON.stringify(normCurr)) {
104
+ return true;
105
+ };
106
+ };
107
+
108
+ return false; // no changes
109
+ };
110
+
111
+ export default hasTopLevelFormChanged;
@@ -0,0 +1,109 @@
1
+
2
+ /*
3
+ Helper function to determine most recent trespass document for each customer.
4
+ Each document.id contains a customer name and date, and version as relevant (when report is updated)
5
+ (e.g.
6
+ Initial ->
7
+ "id": "smith-danielle-louise-trespass-03-26-2025"
8
+ Incremented on updated report ->
9
+ "id": "smith-danielle-louise-trespass-03-26-2025-1"
10
+ ).
11
+
12
+ Documents are grouped by customer based on the prefix before 'trespass-'. Within each customer group, the most recent document is identified based on date and increment.
13
+
14
+ - @param {Array} docs - array containing trespass document objects
15
+ - @param {string} startStr - string for where to start for indexOf ('trespass-')
16
+ - @returns {Array} array of most current document.id for each customer
17
+ */
18
+
19
+ // extract date and increment (version) from document.id
20
+ const parseDateAndIncrement = (id, startStr) => {
21
+ // get the part of 'id' that comes after 'trespass-'
22
+ const raw = id.slice(id.indexOf(startStr) + startStr.length); // e.g. '03-26-2025' or '03-26-2025-1'
23
+ const parts = raw.split('-'); // e.g. ['03', '26', '2025', '1']
24
+
25
+ // construct standard date string for Date comparison
26
+ const [month, day, year] = parts;
27
+ const date = `${month}/${day}/${year}`;
28
+
29
+ // if there is a fourth part, it is the increment (e.g. '1'), else it is the base document
30
+ let increment = 0;
31
+ if (parts.length > 3) {
32
+ const maybeInc = parseInt(parts[3], 10);
33
+ increment = isNaN(maybeInc) ? 0 : maybeInc;
34
+ };
35
+ return { date, increment };
36
+ };
37
+
38
+ // compare two documents to deterimine which is most recent
39
+ const compareDocDates = (doc1, doc2, startStr) => {
40
+ // parse out date and increment values for both docs
41
+ const { date: date1, increment: inc1 } = parseDateAndIncrement(doc1.id, startStr);
42
+ const { date: date2, increment: inc2 } = parseDateAndIncrement(doc2.id, startStr);
43
+
44
+ const d1 = new Date(date1);
45
+ const d2 = new Date(date2);
46
+
47
+ // compare dates first
48
+ if (d1 > d2) return 1;
49
+ if (d1 < d2) return -1;
50
+
51
+ // if the dates are equal, use incrment to find which is most recent
52
+ if (inc1 > inc2) return 1;
53
+ if (inc1 < inc2) return -1;
54
+ return 0; // both are equal
55
+ };
56
+
57
+
58
+ // extract unique customer key form document.id, from segment before 'trespass-'
59
+ const extractCustomerKey = (id, startStr) => {
60
+ const index = id.indexOf(startStr);
61
+ // slice everything up to the hyphen before 'trespass-'
62
+ return index !== -1 ? id.slice(0, index - 1) : null;
63
+ };
64
+
65
+ // reduce into object, grouped by key of customer name from document.id (e.g. key of 'halls-kerry-amanda' referencing arr of respective docuemtn objs)
66
+ const groupDocsByCustomer = (docs, startStr) => {
67
+ return docs.reduce((groups, doc) => {
68
+ const customerKey = extractCustomerKey(doc.id, startStr);
69
+ if (!customerKey) return groups; // skip if no key
70
+
71
+ // if the customer hasn't been seen before, init their group
72
+ if (!groups[customerKey]) {
73
+ groups[customerKey] = [];
74
+ }
75
+ // add document obj to customer's group
76
+ groups[customerKey].push(doc);
77
+ // console.log("groups", JSON.stringify(groups, null, 2));
78
+ return groups;
79
+ }, {});
80
+ };
81
+
82
+
83
+ // return array of document ids that are the most recent trespass for each customer
84
+ const identifyCurrentTrespassDocs = (docs, startStr) => {
85
+ // group documents by customer
86
+ const grouped = groupDocsByCustomer(docs, startStr);
87
+ const mostCurrentIds = [];
88
+
89
+ // for each group (each customer)
90
+ for (const customerKey in grouped) {
91
+ const customerDocs = grouped[customerKey];
92
+ // assume first document is most current initally
93
+ let mostCurrent = customerDocs[0];
94
+
95
+ // compare each document to the current 'most recent' and replace if newer
96
+ for (let i = 1; i < customerDocs.length; i++) {
97
+ if (compareDocDates(customerDocs[i], mostCurrent, startStr) > 0) {
98
+ mostCurrent = customerDocs[i];
99
+ }
100
+ }
101
+ // save id of the most current doc for this customer
102
+ mostCurrentIds.push(mostCurrent.id);
103
+ };
104
+ // console.log("mostCurrentIds", JSON.stringify(mostCurrentIds, null, 2));
105
+ return mostCurrentIds;
106
+ };
107
+
108
+ // console.log(identifyCurrentTrespassDocs(attachments, "trespass-"));
109
+ export default identifyCurrentTrespassDocs;
@@ -0,0 +1,13 @@
1
+ /*
2
+ compare to update only if content actually changed.
3
+ assists in preventing react-quill's cleanup <-> setState feedback loop
4
+ */
5
+
6
+ export const isSameHtml = (a, b) => {
7
+ const normalize = (html) => {
8
+ const doc = new DOMParser().parseFromString(html || '', 'text/html');
9
+ return doc.body.textContent.trim();
10
+ };
11
+
12
+ return normalize(a) === normalize(b);
13
+ };
@@ -0,0 +1,14 @@
1
+
2
+ /*
3
+ @param {string} dateString - validate for strict format of mm/dd/yyyy in DatePicker component
4
+ @returns: zero-padded single digits for months, days, four digit year
5
+ - not allowed: non-existent date number (ex: 02/33/2024), alphabet chars, extra spaces, or non-slash delimiters
6
+ - convert to ISO format is handled at relevant submit function
7
+ */
8
+ const isValidDateFormat = (dateString) => {
9
+ if(!dateString) return null;
10
+ const regex = /^(0[1-9]|1[0-2])\/(0[1-9]|[12][0-9]|3[01])\/(19|20)\d\d$/;
11
+ return regex.test(dateString)
12
+ };
13
+
14
+ export default isValidDateFormat;
@@ -0,0 +1,11 @@
1
+
2
+
3
+
4
+ const isValidTimeInput = (timeString) => {
5
+ if(!timeString) return null;
6
+ // space character required between final number char and first alphabet char
7
+ const timePattern = /^(0?[1-9]|1[0-2]):[0-5][0-9]\s(AM|PM)$/i;
8
+ return timePattern.test(timeString);
9
+ }
10
+
11
+ export default isValidTimeInput;
@@ -0,0 +1,14 @@
1
+
2
+
3
+
4
+
5
+ const isValidUTCTimeFormat = (timeString) => {
6
+ console.log("@isValidUTCTimeFormat - timeString:", timeString);
7
+ if (!timeString) return false;
8
+
9
+ // regex for "hh:mm:ss.sssZ" UTC ISO format
10
+ const timePattern = /^([01]\d|2[0-3]):([0-5]\d):([0-5]\d)\.\d{3}Z$/;
11
+ return timePattern.test(timeString);
12
+ };
13
+
14
+ export default isValidUTCTimeFormat;
@@ -0,0 +1,7 @@
1
+
2
+ const parseMMDDYYYY = (dateString) => {
3
+ const [month, day, year] = dateString.split('/').map((val) => parseInt(val, 10));
4
+ return new Date(year, month - 1, day);
5
+ };
6
+
7
+ export default parseMMDDYYYY;
@@ -0,0 +1,16 @@
1
+
2
+ /*
3
+ @param {string} queryString - parse query string from current URL
4
+ @returns: obj w/ key/values of query string
5
+ */
6
+ const parseQueryString = (queryString) => {
7
+ const query = {};
8
+ const pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&');
9
+ for (const pair of pairs) {
10
+ const [key, value] = pair.split('=');
11
+ query[decodeURIComponent(key)] = decodeURIComponent(value || '');
12
+ }
13
+ return query;
14
+ };
15
+
16
+ export default parseQueryString
@@ -0,0 +1,44 @@
1
+
2
+ // sort trespass documents for display in UI
3
+
4
+ /**
5
+ * parses a trespass document ID and extracts the date and increment.
6
+ * @param {string} id - document id in the format "name-trespass-MM-DD-YYYY-increment"
7
+ * @returns {{ date: Date, increment: number }}
8
+ */
9
+ const parseDocId = (id) => {
10
+ const raw = id.slice(id.indexOf('trespass-') + 'trespass-'.length);
11
+ const parts = raw.split('-');
12
+ const [month, day, year] = parts;
13
+ const date = new Date(`${month}/${day}/${year}`);
14
+ const increment = parts.length > 3 ? parseInt(parts[3], 10) || 0 : 0;
15
+ return { date, increment };
16
+ };
17
+
18
+ /**
19
+ * sorts trespass documents so that the most recent ones (based on date & increment) are first.
20
+ * documents identified as 'most current' are prioritized at the top.
21
+ *
22
+ * @param {Array} documents - Array of document objects with `id` field
23
+ * @param {Array} mostCurrentDocIds - Array of IDs that represent the most current trespass documents
24
+ * @returns {Array} - Sorted document array
25
+ */
26
+ export default function sortTrespassDocuments(documents, mostCurrentDocIds = []) {
27
+ return [...documents].sort((a, b) => {
28
+ const aIsCurrent = mostCurrentDocIds.includes(a.id);
29
+ const bIsCurrent = mostCurrentDocIds.includes(b.id);
30
+
31
+ // prioritize current documents
32
+ if (aIsCurrent && !bIsCurrent) return -1;
33
+ if (!aIsCurrent && bIsCurrent) return 1;
34
+
35
+ // then sort by date and increment
36
+ const { date: dateA, increment: incA } = parseDocId(a.id);
37
+ const { date: dateB, increment: incB } = parseDocId(b.id);
38
+
39
+ if (dateA > dateB) return -1;
40
+ if (dateA < dateB) return 1;
41
+
42
+ return incB - incA;
43
+ });
44
+ }
@@ -0,0 +1,11 @@
1
+
2
+
3
+ // helper for isFormValid, strip to validate for non-empty
4
+ const stripHTML = (html) => {
5
+ const editorDocument = new DOMParser().parseFromString(html, 'text/html');
6
+ const textContent = editorDocument.body.textContent || '';
7
+ // ensure empty tags or whitespace return an empty string
8
+ return textContent.trim() === '' ? '' : textContent.trim();
9
+ };
10
+
11
+ export default stripHTML;
@@ -0,0 +1,197 @@
1
+ import Handlebars from 'handlebars';
2
+ import { FormattedMessage } from 'react-intl';
3
+ import { registerHandlebarsHelpers } from './handlebarsHelpers.js';
4
+ import stripHTML from './stripHTML.js';
5
+ import convertUTCISOToPrettyDate from './convertUTCISOToPrettyDate.js';
6
+ import getTodayDate from './getTodayDate.js';
7
+ import makeId from '../../../settings/helpers/makeId.js';
8
+ import html2pdf from 'html2pdf.js';
9
+
10
+ export function createTrespassDocument(
11
+ template,
12
+ customer,
13
+ incidentData,
14
+ helperDeps
15
+ ) {
16
+ try {
17
+ registerHandlebarsHelpers(helperDeps);
18
+
19
+ const readyCustomer = {
20
+ ...customer,
21
+ description: stripHTML(customer.description),
22
+ fullName: `${customer.lastName}, ${customer.firstName}`,
23
+ trespass: {
24
+ ...customer.trespass,
25
+ descriptionOfOccurrence: customer.trespass.descriptionOfOccurrence, // sani at related token helper
26
+ witnessedBy: customer.trespass.witnessedBy.map(wit => ({
27
+ ...wit,
28
+ fullName: `${wit.lastName}, ${wit.firstName}`
29
+ })),
30
+ dateOfOccurrence: convertUTCISOToPrettyDate(customer.trespass.dateOfOccurrence),
31
+ endDateOfTrespass: convertUTCISOToPrettyDate(customer.trespass.endDateOfTrespass)
32
+ }
33
+ };
34
+
35
+ // console.log("readyCustomer --> ",JSON.stringify(readyCustomer, null, 2))
36
+ const compiled = Handlebars.compile(template);
37
+
38
+ return compiled({
39
+ customer: readyCustomer,
40
+ incident: incidentData
41
+ });
42
+ } catch (error) {
43
+ helperDeps.triggerDocumentError?.(
44
+ <FormattedMessage id="generate-trespass.error-doc-createTrespassDocument" values={{ error: error.message }} />
45
+ );
46
+ return '';
47
+ }
48
+ };
49
+
50
+ // Used at CreatePane
51
+ export function generateTrespassDocuments(
52
+ customers,
53
+ incidentData,
54
+ template,
55
+ helperDeps
56
+ ) {
57
+ try {
58
+ return customers
59
+ .filter(cust => cust.trespass?.declarationOfService)
60
+ .map(cust => {
61
+ const html = createTrespassDocument(template, cust, incidentData, helperDeps);
62
+ if (!html || html.trim() === '') return null;
63
+ return {
64
+ content: html,
65
+ customerName: `${cust.lastName || 'Unknown'}, ${cust.firstName}`
66
+ };
67
+ })
68
+ .filter(Boolean); // remove any null docs
69
+ } catch (error) {
70
+ helperDeps.triggerDocumentError?.(
71
+ <FormattedMessage id="generate-trespass.error-doc-generateTrespassDocuments" values={{ error: error.message }} />
72
+ );
73
+ return [];
74
+ }
75
+ };
76
+
77
+
78
+ // used at EditPane
79
+ export function generateTrespassDocumentsAtEdit(
80
+ customers,
81
+ customersToUpdateDeclaration, // array
82
+ selectedCustomerIds, // Set
83
+ originalDeclarationCustomerIds, // Set
84
+ topLevelAffectsDeclaration, // boolean
85
+ incidentData,
86
+ template,
87
+ helperDeps
88
+ ) {
89
+ try {
90
+ const allowSet = new Set(customersToUpdateDeclaration);
91
+
92
+ return customers
93
+ .filter(cust => {
94
+ const hasDoS = Boolean(cust?.trespass?.declarationOfService);
95
+ if (!hasDoS) return false;
96
+
97
+ const id = cust.id;
98
+ const originallyHad = originalDeclarationCustomerIds.has(id);
99
+ const newlyAdded = !originallyHad;
100
+
101
+ if (newlyAdded) return true; // always generate for newly added DoS
102
+ if (selectedCustomerIds.has(id)) return true; // new customer this session
103
+
104
+ if (topLevelAffectsDeclaration) {
105
+ // require opt-in for everyone who originally had DoS
106
+ return allowSet.has(id);
107
+ } else {
108
+ // per-customer-only mode: generate only for edited + opted-in
109
+ return allowSet.has(id);
110
+ }
111
+ })
112
+ .map(cust => {
113
+ const html = createTrespassDocument(template, cust, incidentData, helperDeps);
114
+ if (!html || html.trim() === '') return null;
115
+ return {
116
+ content: html,
117
+ customerName: `${cust.lastName || 'Unknown'}, ${cust.firstName}`
118
+ };
119
+ })
120
+ .filter(Boolean);
121
+ } catch (error) {
122
+ helperDeps.triggerDocumentError?.(
123
+ <FormattedMessage id="generate-trespass.error-doc-generateTrespassDocuments" values={{ error: error.message }} />
124
+ );
125
+ return [];
126
+ };
127
+ };
128
+
129
+ export async function generatePDFAttachments(documents, triggerDocumentError) {
130
+ try {
131
+ const attachments = await Promise.all(documents.map(async (doc) => {
132
+ const tempParent = document.createElement('div');
133
+ tempParent.style.visibility = 'hidden';
134
+ tempParent.style.position = 'absolute';
135
+ tempParent.style.left = '-10000px';
136
+
137
+ const tempDiv = document.createElement('div');
138
+ tempDiv.innerHTML = doc.content;
139
+ // force layout normalization to avoid phantom height
140
+ tempDiv.style.whiteSpace = 'normal';
141
+ tempDiv.style.padding = '0';
142
+ tempDiv.style.margin = '0';
143
+ tempDiv.style.position = 'relative';
144
+ tempDiv.style.top = '0';
145
+ tempDiv.style.lineHeight = '1.5';
146
+ tempDiv.style.fontSize = '12pt';
147
+
148
+ tempParent.appendChild(tempDiv);
149
+ document.body.appendChild(tempParent);
150
+
151
+ const style = document.createElement('style');
152
+ style.textContent = `
153
+ html, body {
154
+ margin: 0;
155
+ padding: 0;
156
+ }
157
+
158
+ p, h1, h2, h3, h4 {
159
+ orphans: 3;
160
+ widows: 3;
161
+ margin: 0 0 0.25in 0;
162
+ }
163
+ `;
164
+
165
+ tempDiv.prepend(style);
166
+
167
+ const options = {
168
+ margin: 0.5,
169
+ filename: `Trespass_Document_${doc.customerName}.pdf`,
170
+ html2canvas: { scale: 2 },
171
+ jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' },
172
+ pagebreak: {
173
+ mode: ['avoid-all', 'css', 'legacy']
174
+ }
175
+ };
176
+
177
+ const blob = await html2pdf().set(options).from(tempDiv).output('blob');
178
+ // console.log('PDF HTML snapshot:\n', tempDiv.innerHTML);
179
+
180
+ document.body.removeChild(tempParent);
181
+
182
+ return {
183
+ contentType: 'application/pdf',
184
+ description: `${doc.customerName} trespass ${getTodayDate()}`,
185
+ id: makeId(`${doc.customerName} trespass ${getTodayDate()}`),
186
+ file: blob
187
+ };
188
+ }));
189
+
190
+ return attachments;
191
+ } catch (error) {
192
+ triggerDocumentError?.(
193
+ <FormattedMessage id="generate-trespass.error-doc-generatePDFAttachments" values={{ error: error.message }} />
194
+ );
195
+ return [];
196
+ }
197
+ };
@@ -0,0 +1,37 @@
1
+ import isValidDateFormat from './isValidDateFormat';
2
+
3
+ export const isDeclarationOfServiceEmpty = (declaration = {}) =>
4
+ Object.values(declaration).every(value => value === '' || value === false);
5
+
6
+ export const validateDeclarationOfService = (declaration) => {
7
+ if (!declaration) return true;
8
+ const { date, placeSigned, title, signature } = declaration;
9
+ const fields = [date, placeSigned, title, signature];
10
+ const allFilled = fields.every(Boolean);
11
+ const allEmpty = fields.every(value => !value);
12
+ return allEmpty || (allFilled && isValidDateFormat(date));
13
+ };
14
+
15
+ export const validateDateFields = (data = {}) => {
16
+ const { endDateOfTrespass, declarationOfService } = data;
17
+ const dateValues = [endDateOfTrespass, declarationOfService?.date];
18
+ return dateValues.every(val => val === '' || isValidDateFormat(val));
19
+ };
20
+
21
+ export const hasTrespassReason = (data = {}) => {
22
+ const arr = data.exclusionOrTrespassBasedOn;
23
+ if (!Array.isArray(arr)) return false;
24
+ return arr.some(item => {
25
+ if (!item) return false;
26
+ if (typeof item === 'string') return item.trim() !== '';
27
+ if (typeof item === 'object') return typeof item.id === 'string' && item.id.trim() !== '';
28
+ return false;
29
+ });
30
+ };
31
+
32
+ export const validateTrespass = (data) => {
33
+ const dateFieldsValid = validateDateFields(data);
34
+ const declarationValid = validateDeclarationOfService(data.declarationOfService);
35
+ const reasonsValid = hasTrespassReason(data);
36
+ return dateFieldsValid && declarationValid && reasonsValid;
37
+ };
@@ -0,0 +1,70 @@
1
+ import React, { useState, useCallback, useEffect } from 'react';
2
+
3
+ export const STORAGE_KEY = 'ui-secinci-modal-link-visible-columns';
4
+
5
+ export default function usePersistedColModalLink(possibleColumns) {
6
+ // walk through the canonical list and retain only the columns that are currently visible, in their canonical order
7
+ const reorderToCanonical = (columns, canonicalOrder) =>
8
+ canonicalOrder.filter(col => columns.includes(col));
9
+
10
+ const [visibleColumns, setVisibleColumns] = useState(() => {
11
+ // load from sessionStorage
12
+ const stored = sessionStorage.getItem(STORAGE_KEY);
13
+
14
+ try {
15
+ const parsed = JSON.parse(stored);
16
+ // keep only valid column IDs
17
+ const filtered = Array.isArray(parsed) && parsed.length
18
+ ? parsed.filter(col => possibleColumns.includes(col))
19
+ : possibleColumns;
20
+
21
+ // initialize with canon
22
+ return reorderToCanonical(filtered, possibleColumns);
23
+ } catch {
24
+ return possibleColumns;
25
+ }
26
+ });
27
+
28
+ // sync visibleColumns from sessionStorage on each mount/render
29
+ useEffect(() => {
30
+ const stored = sessionStorage.getItem(STORAGE_KEY);
31
+
32
+ try {
33
+ const parsed = JSON.parse(stored);
34
+ const filtered = Array.isArray(parsed) && parsed.length
35
+ ? parsed.filter(col => possibleColumns.includes(col))
36
+ : possibleColumns;
37
+
38
+ const canonical = reorderToCanonical(filtered, possibleColumns);
39
+ setVisibleColumns((current) => {
40
+ const joined = (arr) => arr.join('|');
41
+ return joined(current) === joined(canonical) ? current : canonical;
42
+ });
43
+ } catch {
44
+ setVisibleColumns(possibleColumns);
45
+ }
46
+ }, [possibleColumns]);
47
+
48
+ const toggleColumn = useCallback((colId) => {
49
+ setVisibleColumns(prev => {
50
+ const isRemoving = prev.includes(colId);
51
+ // const isLastOne = prev.length === 1;
52
+
53
+ // if (isRemoving && isLastOne) {
54
+ // return prev;
55
+ // }
56
+
57
+ let next = isRemoving
58
+ ? prev.filter(c => c !== colId)
59
+ : [...prev, colId];
60
+
61
+ // return enforced canonical order after every toggle, regardless of toggle sequence
62
+ next = reorderToCanonical(next, possibleColumns);
63
+
64
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(next));
65
+ return next;
66
+ });
67
+ }, [possibleColumns]);
68
+
69
+ return [visibleColumns, toggleColumn];
70
+ }