@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.
- package/.eslintrc +32 -0
- package/.github/workflows/CODEOWNERS +8 -0
- package/.github/workflows/pr-validation.yml +44 -0
- package/.github/workflows/release.yml +64 -0
- package/.prettierrc +6 -0
- package/.stripesclirc +4 -0
- package/CHANGELOG.md +8 -0
- package/CONTRIBUTING.md +4 -0
- package/LICENSE +201 -0
- package/README.md +16 -0
- package/administrator-documentation/roles-and-permissions.md +65 -0
- package/administrator-documentation/track-settings-admin-guide-sketch.md +192 -0
- package/administrator-documentation/using-the-application.md +192 -0
- package/icons/app.png +0 -0
- package/icons/app.svg +1 -0
- package/icons/playButton.png +0 -0
- package/icons/profilePicThumbnail.png +0 -0
- package/jest.config.js +10 -0
- package/module-descriptor.json +75 -0
- package/output/service-worker.js +0 -0
- package/package.json +146 -0
- package/src/components/incidents/ColumnChooser.js +37 -0
- package/src/components/incidents/CreateMedia.js +132 -0
- package/src/components/incidents/CreatePane.js +1215 -0
- package/src/components/incidents/CreatePane.test.js +138 -0
- package/src/components/incidents/CreateReport.js +102 -0
- package/src/components/incidents/DetailsPane.js +1267 -0
- package/src/components/incidents/DetailsPane.test.js +150 -0
- package/src/components/incidents/EditPane.js +2334 -0
- package/src/components/incidents/EditPane.test.js +187 -0
- package/src/components/incidents/GetDetails.js +55 -0
- package/src/components/incidents/GetListDQLinkIncident.js +81 -0
- package/src/components/incidents/GetListDynamicQuery.js +66 -0
- package/src/components/incidents/GetLocations.js +57 -0
- package/src/components/incidents/GetMedia.js +98 -0
- package/src/components/incidents/GetName.js +111 -0
- package/src/components/incidents/GetNameCreatedBy.js +94 -0
- package/src/components/incidents/GetOrgLocaleSettings.js +61 -0
- package/src/components/incidents/GetPatronGroups.js +52 -0
- package/src/components/incidents/GetSelf.js +65 -0
- package/src/components/incidents/GetSummary.js +110 -0
- package/src/components/incidents/IncidentTypeCard.js +53 -0
- package/src/components/incidents/IncidentTypeCard.test.js +133 -0
- package/src/components/incidents/IncidentsPaneset.js +810 -0
- package/src/components/incidents/IncidentsPaneset.test.js +128 -0
- package/src/components/incidents/LinkedIncident.js +86 -0
- package/src/components/incidents/ModalAddMedia.js +262 -0
- package/src/components/incidents/ModalAddMedia.test.js +97 -0
- package/src/components/incidents/ModalAttentionDecOfService.js +111 -0
- package/src/components/incidents/ModalCustomWitness.js +469 -0
- package/src/components/incidents/ModalCustomWitness.test.js +147 -0
- package/src/components/incidents/ModalCustomerDetails.js +480 -0
- package/src/components/incidents/ModalCustomerDetails.test.js +116 -0
- package/src/components/incidents/ModalDescribeCustomer.js +361 -0
- package/src/components/incidents/ModalDescribeCustomer.test.js +156 -0
- package/src/components/incidents/ModalDirtyFormWarn.js +62 -0
- package/src/components/incidents/ModalLinkIncident.js +1213 -0
- package/src/components/incidents/ModalLinkIncidentStyle.css +32 -0
- package/src/components/incidents/ModalSelectIncidentTypes.js +178 -0
- package/src/components/incidents/ModalSelectIncidentTypes.test.js +273 -0
- package/src/components/incidents/ModalSelectKnownCustomer.js +395 -0
- package/src/components/incidents/ModalSelectWitness.js +406 -0
- package/src/components/incidents/ModalSelectWitness.test.js +308 -0
- package/src/components/incidents/ModalStyle.css +44 -0
- package/src/components/incidents/ModalTrespass.js +741 -0
- package/src/components/incidents/ModalViewCustomerDetails.js +241 -0
- package/src/components/incidents/ModalViewMedia.js +86 -0
- package/src/components/incidents/ModalViewTrespass.js +210 -0
- package/src/components/incidents/ResultsPane.js +437 -0
- package/src/components/incidents/ResultsPane.test.js +120 -0
- package/src/components/incidents/SearchCustomerOrWitness.js +108 -0
- package/src/components/incidents/Thumbnail.js +72 -0
- package/src/components/incidents/ThumbnailMarkRemoval.js +38 -0
- package/src/components/incidents/ThumbnailSkeleton.js +30 -0
- package/src/components/incidents/ThumbnailStyles.js +49 -0
- package/src/components/incidents/ThumbnailTempPreSave.js +71 -0
- package/src/components/incidents/UpdateReport.js +84 -0
- package/src/components/incidents/__snapshots__/CreatePane.test.js.snap +3 -0
- package/src/components/incidents/__snapshots__/DetailsPane.test.js.snap +3 -0
- package/src/components/incidents/__snapshots__/EditPane.test.js.snap +3 -0
- package/src/components/incidents/__snapshots__/IncidentTypeCard.test.js.snap +3 -0
- package/src/components/incidents/__snapshots__/IncidentsPaneset.test.js.snap +3 -0
- package/src/components/incidents/__snapshots__/ModalAddMedia.test.js.snap +3 -0
- package/src/components/incidents/__snapshots__/ModalCustomerDetails.test.js.snap +3 -0
- package/src/components/incidents/__snapshots__/ModalSelectWitness.test.js.snap +3 -0
- package/src/components/incidents/__snapshots__/ResultsPane.test.js.snap +3 -0
- package/src/components/incidents/helpers/ProfilePicture/ProfilePicture.css +5 -0
- package/src/components/incidents/helpers/ProfilePicture/ProfilePicture.js +51 -0
- package/src/components/incidents/helpers/ProfilePicture/isAValidURL.js +3 -0
- package/src/components/incidents/helpers/ProfilePicture/useProfilePicture.js +127 -0
- package/src/components/incidents/helpers/buildQueryString.js +28 -0
- package/src/components/incidents/helpers/cleanFormValues.js +53 -0
- package/src/components/incidents/helpers/computeEditedCustomers.js +124 -0
- package/src/components/incidents/helpers/convertDateIgnoringTZ.js +8 -0
- package/src/components/incidents/helpers/convertUTCISOToLocalePrettyTime.js +15 -0
- package/src/components/incidents/helpers/convertUTCISOToPrettyDate.js +19 -0
- package/src/components/incidents/helpers/decodeParamsToForm.js +20 -0
- package/src/components/incidents/helpers/deepNormalizeForComparison.js +39 -0
- package/src/components/incidents/helpers/extractFilterString.js +12 -0
- package/src/components/incidents/helpers/formatDateAndTimeToUTCISO.js +14 -0
- package/src/components/incidents/helpers/formatDateToUTCISO.js +14 -0
- package/src/components/incidents/helpers/formatTimeToUTCISO.js +28 -0
- package/src/components/incidents/helpers/getCurrentTime.js +20 -0
- package/src/components/incidents/helpers/getTodayDate.js +12 -0
- package/src/components/incidents/helpers/handlebarsHelpers.js +148 -0
- package/src/components/incidents/helpers/hasFormChangedAtCreate.js +50 -0
- package/src/components/incidents/helpers/hasTopLevelChangeAffectedDeclaration.js +90 -0
- package/src/components/incidents/helpers/hasTopLevelFormChanged.js +111 -0
- package/src/components/incidents/helpers/identifyCurrentTrespassDocs.js +109 -0
- package/src/components/incidents/helpers/isSameHtml.js +13 -0
- package/src/components/incidents/helpers/isValidDateFormat.js +14 -0
- package/src/components/incidents/helpers/isValidTimeInput.js +11 -0
- package/src/components/incidents/helpers/isValidUTCTimeFormat.js +14 -0
- package/src/components/incidents/helpers/parseMMDDYYYY.js +7 -0
- package/src/components/incidents/helpers/parseQueryString.js +16 -0
- package/src/components/incidents/helpers/sortTrespassDocuments.js +44 -0
- package/src/components/incidents/helpers/stripHTML.js +11 -0
- package/src/components/incidents/helpers/trespassDocUtils.js +197 -0
- package/src/components/incidents/helpers/validateTrespassDetails.js +37 -0
- package/src/components/incidents/usePersistedColModalLink.js +70 -0
- package/src/components/incidents/usePersistedColumns.js +70 -0
- package/src/components/incidents/usePersistedSort.js +23 -0
- package/src/components/incidents/usePersistedSortModalLink.js +23 -0
- package/src/contexts/IncidentContext.js +433 -0
- package/src/index.js +61 -0
- package/src/routes/Application.js +13 -0
- package/src/settings/GetIncidentCategories.js +56 -0
- package/src/settings/GetIncidentTypesDetails.js +88 -0
- package/src/settings/GetIncidentTypesIds.js +74 -0
- package/src/settings/GetLocationsInService.js +54 -0
- package/src/settings/GetSingleCustomLocationDetails.js +60 -0
- package/src/settings/GetSingleIncidentTypeDetails.js +60 -0
- package/src/settings/GetTrespassReasons.js +67 -0
- package/src/settings/GetTrespassTemplates.js +51 -0
- package/src/settings/IncidentCategoriesPane.js +285 -0
- package/src/settings/IncidentCategoriesPane.test.js +229 -0
- package/src/settings/IncidentTypeDetailsPane.js +215 -0
- package/src/settings/IncidentTypeDetailsPane.test.js +220 -0
- package/src/settings/IncidentTypeEditPane.js +211 -0
- package/src/settings/IncidentTypeEditPane.test.js +170 -0
- package/src/settings/IncidentTypesPaneset.js +167 -0
- package/src/settings/IncidentTypesPaneset.test.js +124 -0
- package/src/settings/LocationInServiceEditPane.js +320 -0
- package/src/settings/LocationsPaneset.js +415 -0
- package/src/settings/LocationsPaneset.test.js +106 -0
- package/src/settings/ModalDeleteCategory.js +47 -0
- package/src/settings/ModalDeleteIncidentType.js +49 -0
- package/src/settings/ModalDeleteLocationInService.js +49 -0
- package/src/settings/ModalDeleteTrespassReason.js +49 -0
- package/src/settings/ModalPreviewTrespassDoc.js +65 -0
- package/src/settings/ModalTrespassDocTokens.js +83 -0
- package/src/settings/NewIncidentTypePane.js +182 -0
- package/src/settings/PutIncidentType.js +60 -0
- package/src/settings/PutLocationsInService.js +52 -0
- package/src/settings/PutTrespassReasons.js +61 -0
- package/src/settings/PutTrespassTemplate.js +50 -0
- package/src/settings/TrespassDoc.css +17 -0
- package/src/settings/TrespassDocDetailsPane.js +215 -0
- package/src/settings/TrespassDocEditPane.js +538 -0
- package/src/settings/TrespassDocPaneset.js +581 -0
- package/src/settings/TrespassReasonDetailsPane.js +171 -0
- package/src/settings/TrespassReasonEditPane.js +221 -0
- package/src/settings/TrespassReasonsPaneset.js +282 -0
- package/src/settings/__snapshots__/IncidentCategoriesPane.test.js.snap +3 -0
- package/src/settings/__snapshots__/IncidentTypeDetailsPane.test.js.snap +3 -0
- package/src/settings/__snapshots__/IncidentTypeEditPane.test.js.snap +3 -0
- package/src/settings/__snapshots__/IncidentTypesPaneset.test.js.snap +3 -0
- package/src/settings/__snapshots__/LocationsPaneset.test.js.snap +3 -0
- package/src/settings/data/exampleJSON.json +92 -0
- package/src/settings/data/templateTokens.js +396 -0
- package/src/settings/helpers/alphabetize.js +18 -0
- package/src/settings/helpers/getCategoryTitleById.js +13 -0
- package/src/settings/helpers/makeId.js +15 -0
- package/src/settings/index.js +48 -0
- package/stripes.config.js +10 -0
- package/test/jest/__mock__/index.js +8 -0
- package/test/jest/__mock__/intl.mock.js +27 -0
- package/test/jest/__mock__/stripes.mock.js +26 -0
- package/test/jest/__mock__/stripesComponents.mock.js +151 -0
- package/test/jest/__mock__/stripesConfig.mock.js +1 -0
- package/test/jest/__mock__/stripesCore.mock.js +9 -0
- package/test/jest/__mock__/stripesIcon.mock.js +5 -0
- package/test/jest/__mock__/stripesSmartComponents.mock.js +7 -0
- package/test/jest/__mock__/stripesUtils.mock.js +3 -0
- package/test/jest/eslintrc.js +12 -0
- package/test/jest/setupFiles.js +5 -0
- package/translations/ui-security-incident/en_US.json +542 -0
- 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,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
|
+
}
|