@choiceapps/communityservices-sdk 0.1.0
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/AGENTS.md +43 -0
- package/API.md +86 -0
- package/CHANGELOG.md +12 -0
- package/LICENSE +201 -0
- package/NOTICE +11 -0
- package/README.md +100 -0
- package/SSVF.md +131 -0
- package/browser/attachment-conflict.mjs +6 -0
- package/browser/case-plans-ui.mjs +23 -0
- package/browser/case-plans.d.mts +20 -0
- package/browser/case-plans.mjs +89 -0
- package/browser/client-creation.d.mts +16 -0
- package/browser/client-creation.mjs +60 -0
- package/browser/client-profile-ui.mjs +59 -0
- package/browser/client-profile.d.mts +24 -0
- package/browser/client-profile.mjs +94 -0
- package/browser/client-workflows.mjs +135 -0
- package/browser/connection.d.mts +4 -0
- package/browser/connection.mjs +41 -0
- package/browser/current-living-situation.d.mts +44 -0
- package/browser/current-living-situation.mjs +161 -0
- package/browser/date-of-birth-ui.mjs +68 -0
- package/browser/date-of-birth.d.mts +28 -0
- package/browser/date-of-birth.mjs +112 -0
- package/browser/enrollment-workflow.d.mts +174 -0
- package/browser/enrollment-workflow.mjs +518 -0
- package/browser/exit-details.mjs +21 -0
- package/browser/file-store.mjs +32 -0
- package/browser/gwt-mapped.mjs +46 -0
- package/browser/household-exit.d.mts +34 -0
- package/browser/household-exit.mjs +102 -0
- package/browser/households-ui.mjs +20 -0
- package/browser/households.d.mts +22 -0
- package/browser/households.mjs +115 -0
- package/browser/index.d.mts +78 -0
- package/browser/index.mjs +46 -0
- package/browser/member-entry-date.d.mts +30 -0
- package/browser/member-entry-date.mjs +71 -0
- package/browser/need-recording.d.mts +37 -0
- package/browser/need-recording.mjs +68 -0
- package/browser/need-ui.mjs +138 -0
- package/browser/page-lock.mjs +11 -0
- package/browser/password-recovery.mjs +49 -0
- package/browser/playwright-session.mjs +196 -0
- package/browser/program-discovery.d.mts +22 -0
- package/browser/program-discovery.mjs +80 -0
- package/browser/recovery-diagnostics.mjs +9 -0
- package/browser/service-recording.d.mts +45 -0
- package/browser/service-recording.mjs +148 -0
- package/browser/service-ui.mjs +160 -0
- package/browser/session-authentication.mjs +40 -0
- package/browser/shelter-inventory.d.mts +103 -0
- package/browser/shelter-inventory.mjs +273 -0
- package/browser/shelter-ui.mjs +382 -0
- package/browser/shelters.d.mts +139 -0
- package/browser/shelters.mjs +425 -0
- package/browser/ssvf-rules.d.mts +21 -0
- package/browser/ssvf-rules.mjs +145 -0
- package/browser/ui-login.mjs +135 -0
- package/dist/cjs/bulk.d.ts +33 -0
- package/dist/cjs/bulk.d.ts.map +1 -0
- package/dist/cjs/bulk.js +42 -0
- package/dist/cjs/bulk.js.map +1 -0
- package/dist/cjs/control.d.ts +35 -0
- package/dist/cjs/control.d.ts.map +1 -0
- package/dist/cjs/control.js +141 -0
- package/dist/cjs/control.js.map +1 -0
- package/dist/cjs/effective-date.d.ts +3 -0
- package/dist/cjs/effective-date.d.ts.map +1 -0
- package/dist/cjs/effective-date.js +27 -0
- package/dist/cjs/effective-date.js.map +1 -0
- package/dist/cjs/encoding.d.ts +32 -0
- package/dist/cjs/encoding.d.ts.map +1 -0
- package/dist/cjs/encoding.js +121 -0
- package/dist/cjs/encoding.js.map +1 -0
- package/dist/cjs/enrollment-verifications.d.ts +39 -0
- package/dist/cjs/enrollment-verifications.d.ts.map +1 -0
- package/dist/cjs/enrollment-verifications.js +96 -0
- package/dist/cjs/enrollment-verifications.js.map +1 -0
- package/dist/cjs/errors.d.ts +33 -0
- package/dist/cjs/errors.d.ts.map +1 -0
- package/dist/cjs/errors.js +45 -0
- package/dist/cjs/errors.js.map +1 -0
- package/dist/cjs/generated/v2-models.d.ts +1482 -0
- package/dist/cjs/generated/v2-models.d.ts.map +1 -0
- package/dist/cjs/generated/v2-models.js +3 -0
- package/dist/cjs/generated/v2-models.js.map +1 -0
- package/dist/cjs/generated/v2.d.ts +1149 -0
- package/dist/cjs/generated/v2.d.ts.map +1 -0
- package/dist/cjs/generated/v2.js +252 -0
- package/dist/cjs/generated/v2.js.map +1 -0
- package/dist/cjs/generated/v3-models.d.ts +1420 -0
- package/dist/cjs/generated/v3-models.d.ts.map +1 -0
- package/dist/cjs/generated/v3-models.js +3 -0
- package/dist/cjs/generated/v3-models.js.map +1 -0
- package/dist/cjs/generated/v3.d.ts +1149 -0
- package/dist/cjs/generated/v3.d.ts.map +1 -0
- package/dist/cjs/generated/v3.js +252 -0
- package/dist/cjs/generated/v3.js.map +1 -0
- package/dist/cjs/gwt-enrollments.d.ts +131 -0
- package/dist/cjs/gwt-enrollments.d.ts.map +1 -0
- package/dist/cjs/gwt-enrollments.js +280 -0
- package/dist/cjs/gwt-enrollments.js.map +1 -0
- package/dist/cjs/index.d.ts +3 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +19 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/metadata.d.ts +100 -0
- package/dist/cjs/metadata.d.ts.map +1 -0
- package/dist/cjs/metadata.js +165 -0
- package/dist/cjs/metadata.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/program-discovery.d.ts +28 -0
- package/dist/cjs/program-discovery.d.ts.map +1 -0
- package/dist/cjs/program-discovery.js +58 -0
- package/dist/cjs/program-discovery.js.map +1 -0
- package/dist/cjs/race.d.ts +4 -0
- package/dist/cjs/race.d.ts.map +1 -0
- package/dist/cjs/race.js +40 -0
- package/dist/cjs/race.js.map +1 -0
- package/dist/cjs/runtime.d.ts +108 -0
- package/dist/cjs/runtime.d.ts.map +1 -0
- package/dist/cjs/runtime.js +38 -0
- package/dist/cjs/runtime.js.map +1 -0
- package/dist/cjs/subassessments.d.ts +29 -0
- package/dist/cjs/subassessments.d.ts.map +1 -0
- package/dist/cjs/subassessments.js +107 -0
- package/dist/cjs/subassessments.js.map +1 -0
- package/dist/cjs/testing.d.ts +45 -0
- package/dist/cjs/testing.d.ts.map +1 -0
- package/dist/cjs/testing.js +66 -0
- package/dist/cjs/testing.js.map +1 -0
- package/dist/cjs/transport.d.ts +19 -0
- package/dist/cjs/transport.d.ts.map +1 -0
- package/dist/cjs/transport.js +373 -0
- package/dist/cjs/transport.js.map +1 -0
- package/dist/cjs/v2.d.ts +13 -0
- package/dist/cjs/v2.d.ts.map +1 -0
- package/dist/cjs/v2.js +29 -0
- package/dist/cjs/v2.js.map +1 -0
- package/dist/cjs/v3.d.ts +13 -0
- package/dist/cjs/v3.d.ts.map +1 -0
- package/dist/cjs/v3.js +29 -0
- package/dist/cjs/v3.js.map +1 -0
- package/dist/cjs/validation.d.ts +33 -0
- package/dist/cjs/validation.d.ts.map +1 -0
- package/dist/cjs/validation.js +77 -0
- package/dist/cjs/validation.js.map +1 -0
- package/dist/cjs/verification.d.ts +82 -0
- package/dist/cjs/verification.d.ts.map +1 -0
- package/dist/cjs/verification.js +122 -0
- package/dist/cjs/verification.js.map +1 -0
- package/dist/esm/bulk.d.ts +33 -0
- package/dist/esm/bulk.d.ts.map +1 -0
- package/dist/esm/bulk.js +39 -0
- package/dist/esm/bulk.js.map +1 -0
- package/dist/esm/control.d.ts +35 -0
- package/dist/esm/control.d.ts.map +1 -0
- package/dist/esm/control.js +132 -0
- package/dist/esm/control.js.map +1 -0
- package/dist/esm/effective-date.d.ts +3 -0
- package/dist/esm/effective-date.d.ts.map +1 -0
- package/dist/esm/effective-date.js +24 -0
- package/dist/esm/effective-date.js.map +1 -0
- package/dist/esm/encoding.d.ts +32 -0
- package/dist/esm/encoding.d.ts.map +1 -0
- package/dist/esm/encoding.js +117 -0
- package/dist/esm/encoding.js.map +1 -0
- package/dist/esm/enrollment-verifications.d.ts +39 -0
- package/dist/esm/enrollment-verifications.d.ts.map +1 -0
- package/dist/esm/enrollment-verifications.js +93 -0
- package/dist/esm/enrollment-verifications.js.map +1 -0
- package/dist/esm/errors.d.ts +33 -0
- package/dist/esm/errors.d.ts.map +1 -0
- package/dist/esm/errors.js +33 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/generated/v2-models.d.ts +1482 -0
- package/dist/esm/generated/v2-models.d.ts.map +1 -0
- package/dist/esm/generated/v2-models.js +2 -0
- package/dist/esm/generated/v2-models.js.map +1 -0
- package/dist/esm/generated/v2.d.ts +1149 -0
- package/dist/esm/generated/v2.d.ts.map +1 -0
- package/dist/esm/generated/v2.js +247 -0
- package/dist/esm/generated/v2.js.map +1 -0
- package/dist/esm/generated/v3-models.d.ts +1420 -0
- package/dist/esm/generated/v3-models.d.ts.map +1 -0
- package/dist/esm/generated/v3-models.js +2 -0
- package/dist/esm/generated/v3-models.js.map +1 -0
- package/dist/esm/generated/v3.d.ts +1149 -0
- package/dist/esm/generated/v3.d.ts.map +1 -0
- package/dist/esm/generated/v3.js +247 -0
- package/dist/esm/generated/v3.js.map +1 -0
- package/dist/esm/gwt-enrollments.d.ts +131 -0
- package/dist/esm/gwt-enrollments.d.ts.map +1 -0
- package/dist/esm/gwt-enrollments.js +274 -0
- package/dist/esm/gwt-enrollments.js.map +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/metadata.d.ts +100 -0
- package/dist/esm/metadata.d.ts.map +1 -0
- package/dist/esm/metadata.js +157 -0
- package/dist/esm/metadata.js.map +1 -0
- package/dist/esm/program-discovery.d.ts +28 -0
- package/dist/esm/program-discovery.d.ts.map +1 -0
- package/dist/esm/program-discovery.js +55 -0
- package/dist/esm/program-discovery.js.map +1 -0
- package/dist/esm/race.d.ts +4 -0
- package/dist/esm/race.d.ts.map +1 -0
- package/dist/esm/race.js +37 -0
- package/dist/esm/race.js.map +1 -0
- package/dist/esm/runtime.d.ts +108 -0
- package/dist/esm/runtime.d.ts.map +1 -0
- package/dist/esm/runtime.js +33 -0
- package/dist/esm/runtime.js.map +1 -0
- package/dist/esm/subassessments.d.ts +29 -0
- package/dist/esm/subassessments.d.ts.map +1 -0
- package/dist/esm/subassessments.js +103 -0
- package/dist/esm/subassessments.js.map +1 -0
- package/dist/esm/testing.d.ts +45 -0
- package/dist/esm/testing.d.ts.map +1 -0
- package/dist/esm/testing.js +60 -0
- package/dist/esm/testing.js.map +1 -0
- package/dist/esm/transport.d.ts +19 -0
- package/dist/esm/transport.d.ts.map +1 -0
- package/dist/esm/transport.js +369 -0
- package/dist/esm/transport.js.map +1 -0
- package/dist/esm/v2.d.ts +13 -0
- package/dist/esm/v2.d.ts.map +1 -0
- package/dist/esm/v2.js +13 -0
- package/dist/esm/v2.js.map +1 -0
- package/dist/esm/v3.d.ts +13 -0
- package/dist/esm/v3.d.ts.map +1 -0
- package/dist/esm/v3.js +13 -0
- package/dist/esm/v3.js.map +1 -0
- package/dist/esm/validation.d.ts +33 -0
- package/dist/esm/validation.d.ts.map +1 -0
- package/dist/esm/validation.js +72 -0
- package/dist/esm/validation.js.map +1 -0
- package/dist/esm/verification.d.ts +82 -0
- package/dist/esm/verification.d.ts.map +1 -0
- package/dist/esm/verification.js +117 -0
- package/dist/esm/verification.js.map +1 -0
- package/docs/README.md +56 -0
- package/docs/assessments.md +103 -0
- package/docs/browser-session.md +129 -0
- package/docs/capabilities.md +75 -0
- package/docs/configuration.md +81 -0
- package/docs/current-living-situation.md +56 -0
- package/docs/dates-and-answers.md +40 -0
- package/docs/development.md +81 -0
- package/docs/durable-records.md +156 -0
- package/docs/enrollments.md +217 -0
- package/docs/files-and-notes.md +39 -0
- package/docs/getting-started.md +56 -0
- package/docs/independent-needs.md +77 -0
- package/docs/integration-methods.md +60 -0
- package/docs/profiles.md +68 -0
- package/docs/reference.md +64 -0
- package/docs/rest-client.md +97 -0
- package/docs/services.md +66 -0
- package/docs/shelter-inventory.md +64 -0
- package/docs/shelters.md +75 -0
- package/docs/troubleshooting.md +61 -0
- package/docs/using-the-sdk.md +203 -0
- package/docs/vendor-issues/README.md +46 -0
- package/docs/vendor-issues/api-coverage.md +27 -0
- package/docs/vendor-issues/attachment-authentication.md +40 -0
- package/docs/vendor-issues/date-semantics.md +51 -0
- package/docs/vendor-issues/empty-service-list-404.md +51 -0
- package/docs/vendor-issues/enrollment-complete-target-error.md +178 -0
- package/docs/vendor-issues/enrollment-effective-date-column.md +112 -0
- package/docs/vendor-issues/household-head-validation.md +26 -0
- package/docs/vendor-issues/write-contracts.md +51 -0
- package/examples/README.md +41 -0
- package/examples/current-living-situation.mts +27 -0
- package/examples/durable-records.mts +78 -0
- package/examples/enrollment-workflow.mts +46 -0
- package/examples/gwt-enrollment-session.ts +17 -0
- package/examples/gwt-file-store.mjs +2 -0
- package/examples/household-enrollment-workflow.mts +43 -0
- package/examples/household-exit-workflow.mts +35 -0
- package/examples/household-lifecycle-workflow.mts +34 -0
- package/examples/independent-needs.mts +30 -0
- package/examples/program-discovery.ts +19 -0
- package/examples/quickstart.mjs +13 -0
- package/examples/shelter-inventory.mts +30 -0
- package/examples/shelters.mts +33 -0
- package/examples/usage.ts +23 -0
- package/examples/verification-snapshot.ts +9 -0
- package/llms.txt +26 -0
- package/package.json +103 -0
- package/specs/provenance.json +17 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import {pageConnection} from './connection.mjs';
|
|
2
|
+
import { withPageLock } from './page-lock.mjs';
|
|
3
|
+
|
|
4
|
+
export class ShelterInventoryError extends Error {
|
|
5
|
+
constructor(reason) {
|
|
6
|
+
super(`Shelter inventory unavailable: ${reason}`);
|
|
7
|
+
this.name = 'ShelterInventoryError';
|
|
8
|
+
this.code = 'SHELTER_INVENTORY_UNAVAILABLE';
|
|
9
|
+
this.reason = reason;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const fail = reason => { throw new ShelterInventoryError(reason); };
|
|
13
|
+
const positive = value => {
|
|
14
|
+
if (!Number.isSafeInteger(value) || value < 1) fail('invalid-input');
|
|
15
|
+
return value;
|
|
16
|
+
};
|
|
17
|
+
const count = value => {
|
|
18
|
+
if (typeof value !== 'string' || !/^\d+$/.test(value) || !Number.isSafeInteger(Number(value))) fail('invalid-count');
|
|
19
|
+
return Number(value);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Serialized into the page. Read visible DOM only, never GWT state or credentials.
|
|
23
|
+
function readDocument() {
|
|
24
|
+
const visible = e => !!e?.getClientRects().length;
|
|
25
|
+
const clean = e => e?.textContent?.replace(/\s+/g, ' ').trim() ?? '';
|
|
26
|
+
const providerRows = [...document.querySelectorAll('tr')].filter(r => visible(r) && clean(r.cells[0]) === 'Provider' && r.cells.length >= 2);
|
|
27
|
+
const providerLabel = providerRows.length === 1 ? providerRows[0].cells[1].innerText.split('\n').map(t => t.trim()).find(Boolean) : null;
|
|
28
|
+
const lists = [...document.querySelectorAll('[id="ShelterPointMainView.unitListBox"]')].filter(visible);
|
|
29
|
+
const filters = [...document.querySelectorAll('select')].filter(e => visible(e) && [...e.options].some(o => o.value === 'ALL' && clean(o) === 'All Beds'));
|
|
30
|
+
const tables = [...document.querySelectorAll('table')].filter(t => visible(t) && t.rows[0] && [...t.rows[0].cells].map(clean).join('|') === '|Date In|Unit|Address|Bed|Hold|Client|Date of Birth|Sex|Group ID|Conf.|Codes/Notes');
|
|
31
|
+
return {
|
|
32
|
+
url: location.href,
|
|
33
|
+
busy: /Processing\.\.\.|Loading\.\.\./.test(document.body.innerText),
|
|
34
|
+
login: [...document.querySelectorAll('[id="formfield-password"]')].some(visible),
|
|
35
|
+
dialogs: [...document.querySelectorAll('[role="dialog"]')].filter(visible).length,
|
|
36
|
+
unfinished: [...document.querySelectorAll('[id^="ShelterPointCheckIn.save"], [id^="ShelterPointCheckOut.save"]')].some(visible),
|
|
37
|
+
providerLabel,
|
|
38
|
+
lists: lists.length === 1 ? [...lists[0].options].map(o => ({ value: o.value, name: clean(o), disabled: o.disabled })) : null,
|
|
39
|
+
selectedList: lists.length === 1 ? lists[0].value : null,
|
|
40
|
+
filter: filters.length === 1 ? filters[0].value : null,
|
|
41
|
+
inventory: tables.length === 1 ? [...tables[0].rows].slice(1).filter(visible).map(r => {
|
|
42
|
+
const client = r.cells[6]?.querySelector('[id^="ShelterPointMainView.shelter.clientLink-"]');
|
|
43
|
+
const controls = [...r.querySelectorAll('img')].filter(visible);
|
|
44
|
+
return {
|
|
45
|
+
cells: r.cells.length,
|
|
46
|
+
dateInText: clean(r.cells[1]), unit: clean(r.cells[2]), address: clean(r.cells[3]), bed: clean(r.cells[4]),
|
|
47
|
+
client: clean(client), clientTitle: client?.getAttribute('title') ?? '', group: clean(r.cells[9]),
|
|
48
|
+
checkIn: controls.filter(e => e.id.startsWith('ShelterPointMainView.shelter.checkInImage-') && e.title === 'Check In').length,
|
|
49
|
+
checkOut: controls.filter(e => e.id.startsWith('ShelterPointMainView.shelter.checkOutImage-') && e.title === 'Check Out').length,
|
|
50
|
+
};
|
|
51
|
+
}) : null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function readAvailabilityDocument() {
|
|
55
|
+
const visible = e => !!e?.getClientRects().length;
|
|
56
|
+
const clean = e => e.textContent.replace(/\s+/g, ' ').trim();
|
|
57
|
+
const dialogs = [...document.querySelectorAll('[role="dialog"]')].filter(visible);
|
|
58
|
+
const d = dialogs.length === 1 ? dialogs[0] : null;
|
|
59
|
+
const tables = d ? [...d.querySelectorAll('table[role="grid"]')].filter(t => t.rows[0] && [...t.rows[0].cells].map(clean).join('|') === '|Provider|Unit List|Type|Total Units|Used|Available|Overflow|Capacity') : [];
|
|
60
|
+
return {
|
|
61
|
+
rows: tables.length === 1 ? [...tables[0].rows].slice(1).filter(r => r.getAttribute('role') === 'row').map(r => [...r.cells].map(clean)) : null,
|
|
62
|
+
ranges: d ? [...d.innerText.matchAll(/Showing (\d+)-(\d+) of (\d+)/g)].map(m => m.slice(1).map(Number)) : [],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function providerFrom(document, providerId) {
|
|
66
|
+
const m = document.providerLabel?.match(/^(.*?) \((\d+)\)$/);
|
|
67
|
+
if (!m || Number(m[2]) !== providerId || !m[1]) fail('provider-mismatch');
|
|
68
|
+
return { providerId, name: m[1] };
|
|
69
|
+
}
|
|
70
|
+
function listsFrom(document, providerId, maxUnitLists) {
|
|
71
|
+
if (!Array.isArray(document.lists)) fail('unit-lists-unavailable');
|
|
72
|
+
const lists = document.lists.filter(o => o.value !== '-1').map(o => {
|
|
73
|
+
if (!/^\d+$/.test(o.value) || !o.name || o.disabled) fail('unit-list-unavailable');
|
|
74
|
+
return { providerId, unitListId: positive(Number(o.value)), name: o.name };
|
|
75
|
+
});
|
|
76
|
+
if (lists.length > maxUnitLists) fail('unit-list-limit');
|
|
77
|
+
if (new Set(lists.map(l => l.unitListId)).size !== lists.length || new Set(lists.map(l => l.name)).size !== lists.length) fail('ambiguous-unit-lists');
|
|
78
|
+
return lists.sort((a, b) => a.unitListId - b.unitListId);
|
|
79
|
+
}
|
|
80
|
+
function reconcileCatalog(provider, lists, summaries) {
|
|
81
|
+
if (lists.length !== summaries.length) fail('incomplete-unit-lists');
|
|
82
|
+
return lists.map(list => {
|
|
83
|
+
const matches = summaries.filter(s => s.providerName === provider.name && s.name === list.name);
|
|
84
|
+
if (matches.length !== 1) fail('unit-list-summary-mismatch');
|
|
85
|
+
return { ...list, type: matches[0].type, reported: matches[0].reported };
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function normalizeRows(rows, providerId, unitListId, maxBeds, tenantScope) {
|
|
89
|
+
if (!Array.isArray(rows)) fail('inventory-unavailable');
|
|
90
|
+
if (rows.length > maxBeds + 1) fail('bed-limit');
|
|
91
|
+
const beds = [], overflow = [];
|
|
92
|
+
let overflowEntryAvailable = false;
|
|
93
|
+
for (const r of rows) {
|
|
94
|
+
if (r.cells !== 12 || !r.bed) fail('unsupported-inventory-layout');
|
|
95
|
+
const match = r.client.match(/^\*?\((\d+)\)\s+(.+?)\*?$/);
|
|
96
|
+
let status = 'unknown', reason = 'unrecognized-row', occupant = null;
|
|
97
|
+
if (match && r.checkOut === 1 && r.checkIn === 0) {
|
|
98
|
+
status = 'occupied'; reason = null;
|
|
99
|
+
const scheduled = r.clientTitle.match(/^Client scheduled for check out on (.+)$/);
|
|
100
|
+
occupant = {
|
|
101
|
+
clientId: positive(Number(match[1])), displayName: match[2], dateInText: r.dateInText || null,
|
|
102
|
+
groupId: r.group ? positive(count(r.group)) : null,
|
|
103
|
+
scheduledCheckout: scheduled ? { status: 'scheduled', localDateTimeText: scheduled[1] } : r.clientTitle || r.client.startsWith('*') ? { status: 'unknown' } : { status: 'not-indicated' },
|
|
104
|
+
};
|
|
105
|
+
} else if (r.client === 'EMPTY' && r.checkIn === 1 && r.checkOut === 0 && !r.clientTitle) {
|
|
106
|
+
status = 'vacant'; reason = null;
|
|
107
|
+
} else if (r.client === 'HELD' && r.checkIn === 0 && r.checkOut === 0) {
|
|
108
|
+
status = 'unavailable'; reason = 'held';
|
|
109
|
+
} else if (r.client === 'EMPTY') reason = 'check-in-unavailable';
|
|
110
|
+
if (!r.unit && !r.address && r.bed === 'Overflow (New)') {
|
|
111
|
+
if (overflowEntryAvailable || status !== 'vacant') fail('unsupported-overflow-row');
|
|
112
|
+
overflowEntryAvailable = true;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (!r.unit && !r.address && r.bed === 'Overflow') {
|
|
116
|
+
if (!occupant) fail('unsupported-overflow-row');
|
|
117
|
+
overflow.push(occupant);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const bedKey = JSON.stringify([tenantScope, providerId, unitListId, r.unit, r.address, r.bed]);
|
|
121
|
+
beds.push({ bedKey, bedId: null, unit: r.unit, address: r.address, name: r.bed, status, reason, occupant });
|
|
122
|
+
}
|
|
123
|
+
if (beds.length > maxBeds) fail('bed-limit');
|
|
124
|
+
if (new Set(beds.map(b => b.bedKey)).size !== beds.length) fail('ambiguous-bed-identity');
|
|
125
|
+
const clients = [...beds.flatMap(b => b.occupant ? [b.occupant.clientId] : []), ...overflow.map(o => o.clientId)];
|
|
126
|
+
if (new Set(clients).size !== clients.length) fail('duplicate-occupant');
|
|
127
|
+
beds.sort((a, b) => a.bedKey < b.bedKey ? -1 : a.bedKey > b.bedKey ? 1 : 0);
|
|
128
|
+
overflow.sort((a, b) => a.clientId - b.clientId);
|
|
129
|
+
return { beds, overflow: { occupants: overflow, entryAvailable: overflowEntryAvailable } };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Read-only inventory. Navigates a dedicated Page; never reserves or assigns beds. */
|
|
133
|
+
export function createShelterInventory({ page, maxPages = 100, maxUnitLists = 1000, maxBeds = 10000, timeoutMs = 30000 }) {
|
|
134
|
+
if (!page?.evaluate || !page?.locator || !page?.url) fail('page-required');
|
|
135
|
+
for (const [value, ceiling] of [[maxPages, 100], [maxUnitLists, 1000], [maxBeds, 10000], [timeoutMs, 60000]]) {
|
|
136
|
+
positive(value);
|
|
137
|
+
if (value > ceiling) fail('invalid-input');
|
|
138
|
+
}
|
|
139
|
+
const connection=pageConnection(page),base=connection.moduleBaseUrl;
|
|
140
|
+
const control = name => page.locator(`[id="${name}"]:visible`);
|
|
141
|
+
const guard = () => {
|
|
142
|
+
const url = new URL(page.url());
|
|
143
|
+
if (url.origin + url.pathname !== base + 'index.html') fail('application-page-required');
|
|
144
|
+
};
|
|
145
|
+
const ready = async () => {
|
|
146
|
+
guard();
|
|
147
|
+
await page.waitForFunction(() => !/Processing\.\.\.|Loading\.\.\./.test(document.body.innerText), null, { timeout: timeoutMs });
|
|
148
|
+
guard();
|
|
149
|
+
};
|
|
150
|
+
const read = async () => {
|
|
151
|
+
await ready();
|
|
152
|
+
const document = await page.evaluate(readDocument);
|
|
153
|
+
if (document.login) fail('authentication-required');
|
|
154
|
+
if (document.unfinished) fail('unfinished-form');
|
|
155
|
+
return document;
|
|
156
|
+
};
|
|
157
|
+
const selectProvider = async providerId => {
|
|
158
|
+
const initial = await read();
|
|
159
|
+
if (initial.dialogs) fail('unfinished-form');
|
|
160
|
+
await control('navigation-link.shelterpt').click({ timeout: timeoutMs });
|
|
161
|
+
await ready();
|
|
162
|
+
await page.getByRole('button', { name: 'Search', exact: true }).click({ timeout: timeoutMs });
|
|
163
|
+
const dialog = page.getByRole('dialog');
|
|
164
|
+
await dialog.getByText(/Showing \d+-\d+ of \d+/).waitFor({ timeout: timeoutMs });
|
|
165
|
+
await dialog.getByLabel('Provider ID #', { exact: true }).fill(String(providerId));
|
|
166
|
+
await dialog.getByLabel('Provider ID #', { exact: true }).press('Tab');
|
|
167
|
+
await dialog.getByRole('button', { name: 'Submit', exact: true }).click();
|
|
168
|
+
await control(`SpProviderSearchPopup.setProvider-imageLink_${providerId}`).click({ timeout: timeoutMs });
|
|
169
|
+
await dialog.waitFor({ state: 'hidden', timeout: timeoutMs });
|
|
170
|
+
const document = await read();
|
|
171
|
+
return { provider: providerFrom(document, providerId), lists: listsFrom(document, providerId, maxUnitLists) };
|
|
172
|
+
};
|
|
173
|
+
const readSummaries = async () => {
|
|
174
|
+
await control('ShelterPointMainView.checkUnitAvailabilityBtn').click({ timeout: timeoutMs });
|
|
175
|
+
const dialog = page.getByRole('dialog');
|
|
176
|
+
try {
|
|
177
|
+
await control('UnitAvailabilityPopup.exit-button').waitFor({ timeout: timeoutMs });
|
|
178
|
+
const summaries = [];
|
|
179
|
+
let expectedTotal, previousEnd = 0;
|
|
180
|
+
for (let pageNumber = 1; pageNumber <= maxPages; pageNumber++) {
|
|
181
|
+
await ready();
|
|
182
|
+
const document = await page.evaluate(readAvailabilityDocument);
|
|
183
|
+
if (!document.rows || document.ranges.length !== 1) fail('availability-unavailable');
|
|
184
|
+
const [start, end, total] = document.ranges[0];
|
|
185
|
+
if (![start, end, total].every(Number.isSafeInteger) || total < 0 || total > maxUnitLists) fail('unit-list-limit');
|
|
186
|
+
if (expectedTotal !== undefined && total !== expectedTotal) fail('inventory-changed');
|
|
187
|
+
expectedTotal = total;
|
|
188
|
+
if (total === 0) {
|
|
189
|
+
if (start !== 0 || end !== 0 || document.rows.length) fail('incomplete-unit-lists');
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
if (start !== previousEnd + 1 || end < start || end > total || end - start + 1 !== document.rows.length) fail('incomplete-unit-lists');
|
|
193
|
+
for (const row of document.rows) {
|
|
194
|
+
if (row.length !== 9 || !row[1] || !row[2] || !row[3]) fail('unsupported-availability-layout');
|
|
195
|
+
const reported = { totalBeds: count(row[4]), usedBeds: count(row[5]), availableBeds: count(row[6]), overflowOccupants: count(row[7]) };
|
|
196
|
+
if (reported.totalBeds !== reported.usedBeds + reported.availableBeds) fail('inconsistent-counts');
|
|
197
|
+
summaries.push({ providerName: row[1], name: row[2], type: row[3], reported });
|
|
198
|
+
}
|
|
199
|
+
if (new Set(summaries.map(s => JSON.stringify([s.providerName, s.name]))).size !== summaries.length) fail('ambiguous-unit-lists');
|
|
200
|
+
if (end === total) return summaries;
|
|
201
|
+
if (pageNumber === maxPages) fail('page-limit');
|
|
202
|
+
previousEnd = end;
|
|
203
|
+
await dialog.getByRole('button', { name: 'Next', exact: true }).click({ timeout: timeoutMs });
|
|
204
|
+
await page.waitForFunction(oldEnd => {
|
|
205
|
+
const d = [...document.querySelectorAll('[role="dialog"]')].find(e => e.getClientRects().length);
|
|
206
|
+
const m = d?.innerText.match(/Showing (\d+)-(\d+) of (\d+)/);
|
|
207
|
+
return m && Number(m[1]) === oldEnd + 1;
|
|
208
|
+
}, end, { timeout: timeoutMs });
|
|
209
|
+
}
|
|
210
|
+
fail('page-limit');
|
|
211
|
+
} finally {
|
|
212
|
+
if (await control('UnitAvailabilityPopup.exit-button').count()) {
|
|
213
|
+
await control('UnitAvailabilityPopup.exit-button').click();
|
|
214
|
+
await dialog.waitFor({ state: 'hidden', timeout: timeoutMs });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
const run = work => withPageLock(page, async () => {
|
|
219
|
+
try { return await work(); }
|
|
220
|
+
catch (error) { if (error instanceof ShelterInventoryError) throw error; fail('read-failed'); }
|
|
221
|
+
});
|
|
222
|
+
const freshness = (startedAt, observedAt) => ({ startedAt, observedAt, completedAt: new Date().toISOString(), sourceUpdatedAt: null, atomic: false });
|
|
223
|
+
const limits = { maxPages, maxUnitLists, maxBeds };
|
|
224
|
+
return {connection,
|
|
225
|
+
listUnitLists(input) {
|
|
226
|
+
if (!input || Object.keys(input).some(k => k !== 'providerId')) fail('invalid-input');
|
|
227
|
+
const providerId = positive(input.providerId);
|
|
228
|
+
return run(async () => {
|
|
229
|
+
const startedAt = new Date().toISOString();
|
|
230
|
+
const { provider, lists } = await selectProvider(providerId);
|
|
231
|
+
const unitLists = reconcileCatalog(provider, lists, await readSummaries());
|
|
232
|
+
const document = await read();
|
|
233
|
+
providerFrom(document, providerId);
|
|
234
|
+
if (JSON.stringify(listsFrom(document, providerId, maxUnitLists)) !== JSON.stringify(lists)) fail('inventory-changed');
|
|
235
|
+
return { provider, unitLists, source: 'ui', completeness: 'complete-visible-provider', freshness: freshness(startedAt, new Date().toISOString()), limits };
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
getInventory(input) {
|
|
239
|
+
if (!input || Object.keys(input).some(k => !['providerId', 'unitListId'].includes(k))) fail('invalid-input');
|
|
240
|
+
const providerId = positive(input.providerId), unitListId = positive(input.unitListId);
|
|
241
|
+
return run(async () => {
|
|
242
|
+
const startedAt = new Date().toISOString();
|
|
243
|
+
const { provider, lists } = await selectProvider(providerId);
|
|
244
|
+
const catalog = reconcileCatalog(provider, lists, await readSummaries());
|
|
245
|
+
const unitList = catalog.find(l => l.unitListId === unitListId);
|
|
246
|
+
if (!unitList) fail('unit-list-unavailable');
|
|
247
|
+
if (unitList.reported.totalBeds + unitList.reported.overflowOccupants > maxBeds) fail('bed-limit');
|
|
248
|
+
await control('ShelterPointMainView.unitListBox').selectOption(String(unitListId));
|
|
249
|
+
await control('ShelterPointMainView.selectUnitListBtn').click();
|
|
250
|
+
await page.getByText('View All', { exact: true }).click({ timeout: timeoutMs });
|
|
251
|
+
await control('ShelterPointMainView.transmitTodaysCheckOutBtn').waitFor({ timeout: timeoutMs });
|
|
252
|
+
const display = page.locator('select:visible').filter({ has: page.locator('option[value="ALL"]', { hasText: 'All Beds' }) });
|
|
253
|
+
await display.selectOption('ALL', { timeout: timeoutMs });
|
|
254
|
+
const document = await read();
|
|
255
|
+
providerFrom(document, providerId);
|
|
256
|
+
if (document.selectedList !== String(unitListId) || document.filter !== 'ALL' || document.dialogs) fail('inventory-context-mismatch');
|
|
257
|
+
const observedAt = new Date().toISOString();
|
|
258
|
+
const { beds, overflow } = normalizeRows(document.inventory, providerId, unitListId, maxBeds, connection.scope);
|
|
259
|
+
const after = reconcileCatalog(provider, lists, await readSummaries()).find(l => l.unitListId === unitListId);
|
|
260
|
+
if (JSON.stringify(after) !== JSON.stringify(unitList)) fail('inventory-changed');
|
|
261
|
+
const final = await read();
|
|
262
|
+
providerFrom(final, providerId);
|
|
263
|
+
if (final.dialogs || final.selectedList !== String(unitListId) || final.filter !== 'ALL' || JSON.stringify(listsFrom(final, providerId, maxUnitLists)) !== JSON.stringify(lists) || JSON.stringify(final.inventory) !== JSON.stringify(document.inventory)) fail('inventory-changed');
|
|
264
|
+
if (beds.length !== unitList.reported.totalBeds || overflow.occupants.length !== unitList.reported.overflowOccupants) fail('incomplete-inventory');
|
|
265
|
+
const counts = { total: beds.length, occupied: 0, vacant: 0, unavailable: 0, unknown: 0 };
|
|
266
|
+
for (const bed of beds) counts[bed.status]++;
|
|
267
|
+
if (!counts.unknown && (counts.vacant !== unitList.reported.availableBeds || counts.occupied + counts.unavailable !== unitList.reported.usedBeds)) fail('inventory-changed');
|
|
268
|
+
const availability = counts.unknown ? { status: 'unknown', vacantCount: null, vacantBedKeys: [], reasons: ['unclassified-beds'] } : { status: 'known', vacantCount: counts.vacant, vacantBedKeys: beds.filter(b => b.status === 'vacant').map(b => b.bedKey) };
|
|
269
|
+
return { provider, unitList, source: 'ui', completeness: 'complete-visible-unit-list', freshness: freshness(startedAt, observedAt), limits, beds, counts, overflow, availability };
|
|
270
|
+
});
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|