@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,382 @@
|
|
|
1
|
+
import {pageConnection} from './connection.mjs';
|
|
2
|
+
import { decodeMappedResponse } from './gwt-mapped.mjs';
|
|
3
|
+
import { withPageLock } from './page-lock.mjs';
|
|
4
|
+
const id = n => {
|
|
5
|
+
if (!Number.isSafeInteger(n) || n < 1)
|
|
6
|
+
throw Error('Positive shelter ID required');
|
|
7
|
+
return n;
|
|
8
|
+
};
|
|
9
|
+
// Internal acceptance seam: required controls can be visible before values arrive.
|
|
10
|
+
export async function waitForShelterAssessmentAnswers(page, { timeout = 30000 } = {}) {
|
|
11
|
+
try {
|
|
12
|
+
await page.waitForFunction(() => [...document.querySelectorAll('[id^="ClientProfileTab."][aria-required="true"]')]
|
|
13
|
+
.filter(e => e.getClientRects().length)
|
|
14
|
+
.every(e => e.value && e.value !== '-Select-'), null, { timeout });
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
const missing = await page.locator('[id^="ClientProfileTab."][aria-required="true"]:visible')
|
|
18
|
+
.evaluateAll(es => es.filter(e => !e.value || e.value === '-Select-').map(e => e.getAttribute('aria-label')));
|
|
19
|
+
throw Error('Complete required shelter assessment answers before saving: ' + missing.join(', '));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Fresh forms only. Call through createShelters to journal writes. */
|
|
23
|
+
export function createShelterUi({ page }) {
|
|
24
|
+
const connection=pageConnection(page),base=connection.moduleBaseUrl;
|
|
25
|
+
let readWire;
|
|
26
|
+
let formRequests;
|
|
27
|
+
const ctl = name => page.locator(`[id="${name}"]:visible`);
|
|
28
|
+
const ready = () => page.waitForFunction(() => !document.body.innerText.includes('Processing...') && !document.body.innerText.includes('Loading...'), null, { timeout: 30000 });
|
|
29
|
+
// Assessment loading can continue after the form's Processing overlay disappears.
|
|
30
|
+
// Do not save the initially empty controls before the asynchronous answers arrive.
|
|
31
|
+
const settleForm = async () => {
|
|
32
|
+
if (!formRequests)
|
|
33
|
+
throw Error('Shelter form request tracking required');
|
|
34
|
+
const deadline = Date.now() + 30000;
|
|
35
|
+
while (Date.now() < deadline) {
|
|
36
|
+
await ready();
|
|
37
|
+
if (!formRequests.pending.size && Date.now() - formRequests.changed >= 500)
|
|
38
|
+
return;
|
|
39
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
40
|
+
}
|
|
41
|
+
throw Error('Shelter assessment requests did not settle');
|
|
42
|
+
};
|
|
43
|
+
const trackForm = async (work) => {
|
|
44
|
+
formRequests = { pending: new Set(), changed: Date.now() };
|
|
45
|
+
const started = request => { if (request.url().startsWith(base + 'service/')) {
|
|
46
|
+
formRequests.pending.add(request);
|
|
47
|
+
formRequests.changed = Date.now();
|
|
48
|
+
} };
|
|
49
|
+
const finished = request => { if (formRequests.pending.delete(request))
|
|
50
|
+
formRequests.changed = Date.now(); };
|
|
51
|
+
page.on('request', started);
|
|
52
|
+
page.on('requestfinished', finished);
|
|
53
|
+
page.on('requestfailed', finished);
|
|
54
|
+
try {
|
|
55
|
+
return await work();
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
page.off('request', started);
|
|
59
|
+
page.off('requestfinished', finished);
|
|
60
|
+
page.off('requestfailed', finished);
|
|
61
|
+
formRequests = undefined;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const requireAssessmentAnswers = () => waitForShelterAssessmentAnswers(page);
|
|
65
|
+
const guard = () => {
|
|
66
|
+
const u = new URL(page.url());
|
|
67
|
+
if (u.origin + u.pathname !== base + 'index.html')
|
|
68
|
+
throw Error('Configured shelter page required');
|
|
69
|
+
};
|
|
70
|
+
const noForms = async () => {
|
|
71
|
+
guard();
|
|
72
|
+
if (await page.locator('[role=dialog]:visible').count() || await ctl('ShelterPointCheckIn.saveBtn').count() || await ctl('ShelterPointCheckOut.saveBtn').count() || await ctl('ShelterPointCheckIn.saveExitBtn').count() || await ctl('ShelterPointCheckOut.saveExitBtn').count() || await ctl('formfield-password').count())
|
|
73
|
+
throw Error('Close unfinished forms before shelter workflow');
|
|
74
|
+
};
|
|
75
|
+
const provider = async (providerId) => {
|
|
76
|
+
await page.getByRole('button', { name: 'Search', exact: true }).click();
|
|
77
|
+
const d = page.getByRole('dialog');
|
|
78
|
+
await d.getByText(/Showing \d+-\d+ of \d+/).waitFor();
|
|
79
|
+
await d.getByLabel('Provider ID #', { exact: true }).fill(String(id(providerId)));
|
|
80
|
+
await d.getByLabel('Provider ID #', { exact: true }).press('Tab');
|
|
81
|
+
await d.getByRole('button', { name: 'Submit', exact: true }).click();
|
|
82
|
+
await ctl(`SpProviderSearchPopup.setProvider-imageLink_${providerId}`).click();
|
|
83
|
+
await d.waitFor({ state: 'hidden' });
|
|
84
|
+
await ready();
|
|
85
|
+
};
|
|
86
|
+
const inventory = async ({ providerId, unitListId }) => {
|
|
87
|
+
await noForms();
|
|
88
|
+
await ctl('navigation-link.shelterpt').click();
|
|
89
|
+
await ready();
|
|
90
|
+
await provider(providerId);
|
|
91
|
+
const list = ctl('ShelterPointMainView.unitListBox');
|
|
92
|
+
await list.locator(`option[value="${id(unitListId)}"]`).waitFor({ state: 'attached' });
|
|
93
|
+
const name = await list.locator(`option[value="${unitListId}"]`).textContent();
|
|
94
|
+
await list.selectOption(String(unitListId));
|
|
95
|
+
await ctl('ShelterPointMainView.selectUnitListBtn').click();
|
|
96
|
+
await page.getByText('View All', { exact: true }).click();
|
|
97
|
+
await ctl('ShelterPointMainView.transmitTodaysCheckOutBtn').waitFor();
|
|
98
|
+
await ready();
|
|
99
|
+
const display = page.locator('select:visible').filter({ has: page.locator('option[value="ALL"]', { hasText: 'All Beds' }) });
|
|
100
|
+
if (await display.count()) {
|
|
101
|
+
await display.selectOption('ALL');
|
|
102
|
+
await ready();
|
|
103
|
+
if (await display.inputValue() !== 'ALL')
|
|
104
|
+
throw Error('Shelter inventory filter mismatch');
|
|
105
|
+
}
|
|
106
|
+
return name;
|
|
107
|
+
};
|
|
108
|
+
const setDate = async (prefix, value) => {
|
|
109
|
+
const parts = await page.evaluate(value => {
|
|
110
|
+
const d = new Date(value);
|
|
111
|
+
if (+new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds()) !== +d)
|
|
112
|
+
throw Error('Ambiguous local shelter time');
|
|
113
|
+
return { Month: String(d.getMonth() + 1).padStart(2, '0'), Day: String(d.getDate()).padStart(2, '0'), Year: String(d.getFullYear()), Hour: String(d.getHours() % 12 || 12), Minute: String(d.getMinutes()).padStart(2, '0'), Second: String(d.getSeconds()).padStart(2, '0'), 'AM/PM': d.getHours() < 12 ? 'AM' : 'PM' };
|
|
114
|
+
}, value);
|
|
115
|
+
for (const [part, text] of Object.entries(parts)) {
|
|
116
|
+
const field = page.getByLabel(`${prefix} ${part}`, { exact: true });
|
|
117
|
+
if (['Month', 'Day', 'Year'].includes(part))
|
|
118
|
+
await field.fill(text);
|
|
119
|
+
else
|
|
120
|
+
await field.selectOption({ label: text });
|
|
121
|
+
await field.press('Tab');
|
|
122
|
+
if (await field.inputValue() !== text)
|
|
123
|
+
throw Error('Shelter date mismatch');
|
|
124
|
+
}
|
|
125
|
+
await ready();
|
|
126
|
+
};
|
|
127
|
+
const chooseMembers = async (ids, prefix, anchorClientId) => {
|
|
128
|
+
const boxes = page.locator(`[id^="${prefix}"]:visible input[type=checkbox]`);
|
|
129
|
+
for (let i = 0; i < await boxes.count(); i++) {
|
|
130
|
+
const box = boxes.nth(i), clientId = await box.evaluate(e => Number(e.parentElement.id.match(/client-(\d+)$/)?.[1]));
|
|
131
|
+
if (await box.isEnabled())
|
|
132
|
+
await box.setChecked(ids.includes(clientId));
|
|
133
|
+
if (await box.isChecked() !== ids.includes(clientId))
|
|
134
|
+
throw Error('Shelter household selection mismatch');
|
|
135
|
+
}
|
|
136
|
+
const selected = await boxes.evaluateAll(es => es.filter(e => e.checked).map(e => Number(e.parentElement.id.match(/client-(\d+)$/)?.[1])).sort((a, b) => a - b));
|
|
137
|
+
// A single-client form can omit the anchor's checkbox. Its identity was
|
|
138
|
+
// checked against the form heading; every other member must be explicit.
|
|
139
|
+
const rendered = await boxes.evaluateAll(es => es.map(e => Number(e.parentElement.id.match(/client-(\d+)$/)?.[1])));
|
|
140
|
+
if (!rendered.includes(anchorClientId) && ids.includes(anchorClientId))
|
|
141
|
+
selected.push(anchorClientId);
|
|
142
|
+
selected.sort((a, b) => a - b);
|
|
143
|
+
if (JSON.stringify(selected) !== JSON.stringify([...ids].sort((a, b) => a - b)))
|
|
144
|
+
throw Error('Shelter household selection incomplete');
|
|
145
|
+
};
|
|
146
|
+
const readEnrollments = async ({ clientIds, serviceIds = false, stayIds }) => {
|
|
147
|
+
if (!Array.isArray(clientIds) || !clientIds.length)
|
|
148
|
+
throw Error('Client IDs required');
|
|
149
|
+
clientIds.forEach(id);
|
|
150
|
+
await noForms();
|
|
151
|
+
let seed, pending;
|
|
152
|
+
const listener = r => {
|
|
153
|
+
if (r.url() === base + 'service/MetaQueryService' && r.postData()?.startsWith('7|0|'))
|
|
154
|
+
pending = r.allHeaders().then(headers => { seed = { policy: r.postData().split('|')[4], headers: Object.fromEntries(['content-type', 'x-gwt-module-base', 'x-gwt-permutation'].map(k => [k, headers[k]])) }; });
|
|
155
|
+
};
|
|
156
|
+
page.on('request', listener);
|
|
157
|
+
try {
|
|
158
|
+
await ctl('navigation-link.shelterpt').click();
|
|
159
|
+
await ready();
|
|
160
|
+
await page.goto(base + `index.html#loadClient;clientId=${clientIds[0]}`);
|
|
161
|
+
await page.getByText(new RegExp(`^Client - \\(${clientIds[0]}\\)`)).waitFor();
|
|
162
|
+
await page.getByRole('tab', { name: 'Summary', exact: true }).click();
|
|
163
|
+
await page.getByRole('tab', { name: 'Entry / Exit', exact: true }).click();
|
|
164
|
+
await ready();
|
|
165
|
+
await pending;
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
page.off('request', listener);
|
|
169
|
+
}
|
|
170
|
+
if (!seed)
|
|
171
|
+
throw Error('Fresh enrollment read context unavailable');
|
|
172
|
+
if (stayIds)
|
|
173
|
+
stayIds.forEach(id);
|
|
174
|
+
readWire = seed;
|
|
175
|
+
const query = stayIds ? `SELECT s.needServiceId AS serviceId, s.clientId AS clientId, s.shelterBedId AS bedId, s.shelterBedlistId AS unitListId FROM SpNeedService AS s WHERE s.active = TRUE AND s.shelterItem = TRUE AND s.clientId IN (${clientIds.join(',')}) AND s.needServiceId IN (${stayIds.join(',')}) ORDER BY s.needServiceId LIMIT 1001,0` : serviceIds ? `SELECT s.needServiceId AS serviceId FROM SpNeedService AS s WHERE s.active = TRUE AND s.clientId IN (${clientIds.join(',')}) ORDER BY s.needServiceId LIMIT 1001,0` : `SELECT e.entryExitId AS enrollmentId, e.clientId AS clientId, e.entryDate AS entryDate, e.exitDate AS exitDate, e.groupId AS groupId, e.providerId AS providerId, e.typeEntryExit.name AS type, e.destinationValue.name AS destination, e.reasonLeavingValue.name AS reason FROM SpEntryExit AS e WHERE e.active = TRUE AND e.client.active = TRUE AND e.clientId IN (${clientIds.join(',')}) ORDER BY e.entryExitId LIMIT 1001,0`;
|
|
176
|
+
const strings = [base, seed.policy, 'com.bowmansystems.sp5.core.client.services.GenericService', 'getMappedResults', 'java.lang.String/2004016611', 'java.util.HashMap/1797211028', 'I', 'Z', query];
|
|
177
|
+
const r = await page.evaluate(async (w) => { const r = await fetch(w.url, { method: 'POST', headers: w.headers, body: w.body, credentials: 'same-origin', redirect: 'error', signal: AbortSignal.timeout(30000) }); return { status: r.status, body: await r.text() }; }, { url: base + 'service/MetaQueryService', headers: seed.headers, body: ['7', '0', 9, ...strings, 1, 2, 3, 4, 5, 5, 6, 7, 7, 8, 9, 0, 0, 1001, 1, ''].join('|') });
|
|
178
|
+
if (r.status !== 200)
|
|
179
|
+
throw Error('Enrollment read failed');
|
|
180
|
+
const result = decodeMappedResponse(r.body);
|
|
181
|
+
if (result.total !== result.rows.length || result.total > 1000 || new Set(result.rows.map(r => serviceIds || stayIds ? r.serviceId : r.enrollmentId)).size !== result.rows.length)
|
|
182
|
+
throw Error('Incomplete enrollment read');
|
|
183
|
+
return result.rows;
|
|
184
|
+
};
|
|
185
|
+
const save = async (button, beforeSave) => {
|
|
186
|
+
let journaled = false, journalPromise, routeFailure;
|
|
187
|
+
const route = async (r) => {
|
|
188
|
+
try {
|
|
189
|
+
const request = r.request(), body = request.postData(), method = body?.split('|')[6];
|
|
190
|
+
const read = typeof method === 'string' && (method.startsWith('get') || method === 'heartbeat' || ['verifyHudSubassessment', 'loadScoringRules', 'isClientAlreadyCheckedIn', 'isProviderHouseholdDataSharingAssessmentVisible', 'loadReportConfiguration'].includes(method) || request.url() === base + 'service/MetaQueryService' && method === 'execute' && body.includes('|SELECT '));
|
|
191
|
+
if (!read) {
|
|
192
|
+
if (!body?.startsWith('7|0|'))
|
|
193
|
+
throw Error('Unknown shelter request format');
|
|
194
|
+
if (!journalPromise)
|
|
195
|
+
journalPromise = Promise.resolve().then(beforeSave);
|
|
196
|
+
await journalPromise;
|
|
197
|
+
journaled = true;
|
|
198
|
+
}
|
|
199
|
+
await r.continue();
|
|
200
|
+
}
|
|
201
|
+
catch (e) {
|
|
202
|
+
routeFailure = e;
|
|
203
|
+
await r.abort();
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
await page.route(base + 'service/**', route);
|
|
207
|
+
try {
|
|
208
|
+
await ctl(button).click();
|
|
209
|
+
await page.waitForFunction(name => !document.getElementById(name)?.getClientRects().length || [...document.querySelectorAll('[role=dialog]')].some(d => /Validation Warning/.test(d.innerText)), button, { timeout: 30000 });
|
|
210
|
+
const warning = page.getByRole('dialog').filter({ hasText: 'Validation Warning' });
|
|
211
|
+
if (await warning.count()) {
|
|
212
|
+
const detail = await warning.innerText();
|
|
213
|
+
await warning.getByRole('button', { name: 'OK', exact: true }).click();
|
|
214
|
+
throw Error('Shelter validation rejected the save: ' + detail);
|
|
215
|
+
}
|
|
216
|
+
if (routeFailure)
|
|
217
|
+
throw routeFailure;
|
|
218
|
+
if (!journaled)
|
|
219
|
+
throw Error('No shelter mutation was submitted');
|
|
220
|
+
await ready();
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
await page.unroute(base + 'service/**', route);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
const checkIn = async (input, beforeSave) => {
|
|
227
|
+
const name = await inventory(input);
|
|
228
|
+
if (name !== input.unitListName)
|
|
229
|
+
throw Error('Unit list changed');
|
|
230
|
+
const head = input.members.find(m => m.clientId === input.headClientId);
|
|
231
|
+
if (!head)
|
|
232
|
+
throw Error('Head required');
|
|
233
|
+
const row = page.getByText(head.bedName, { exact: true }).locator('xpath=ancestor::tr[1]');
|
|
234
|
+
if (await row.count() !== 1 || !await row.getByText('EMPTY', { exact: true }).isVisible())
|
|
235
|
+
throw Error('Selected head bed unavailable');
|
|
236
|
+
await row.locator('[id^="ShelterPointMainView.shelter.checkInImage-"]').click();
|
|
237
|
+
await page.waitForFunction(() => !!document.getElementById('ClientSearchView.clientId-textbox') && [...document.querySelectorAll('select option')].some(o => o.textContent === 'Full Name Reported'));
|
|
238
|
+
await ready();
|
|
239
|
+
await ctl('ClientSearchView.clientId-textbox').fill(String(input.headClientId));
|
|
240
|
+
await ctl('ClientSearchView.clientId-textbox').press('Tab');
|
|
241
|
+
if (await ctl('ClientSearchView.clientId-textbox').inputValue() !== String(input.headClientId))
|
|
242
|
+
throw Error('Shelter client search mismatch');
|
|
243
|
+
await ctl('ClientSearchView.clientIdSubmitBtn').click();
|
|
244
|
+
await ctl('ShelterPointCheckIn.saveExitBtn').waitFor();
|
|
245
|
+
await settleForm();
|
|
246
|
+
try {
|
|
247
|
+
if (!(await page.locator('body').innerText()).includes(`Unit Entry Data - (${input.headClientId})`))
|
|
248
|
+
throw Error('Shelter client mismatch');
|
|
249
|
+
const hasEntry = await page.locator('select[aria-label="Type"]').isVisible();
|
|
250
|
+
if (hasEntry !== (input.mode === 'entry-exit'))
|
|
251
|
+
throw Error('Shelter configuration disagrees with provider tracking mode');
|
|
252
|
+
await setDate('Date In', input.entryDate);
|
|
253
|
+
await settleForm();
|
|
254
|
+
await chooseMembers(input.members.map(m => m.clientId), 'ShelterHouseholdsCheckboxTree.checkbox.', input.headClientId);
|
|
255
|
+
for (const member of input.members) {
|
|
256
|
+
if (member.clientId === input.headClientId)
|
|
257
|
+
continue;
|
|
258
|
+
const box = page.locator(`[id^="ShelterHouseholdsCheckboxTree.checkbox."][id$=".client-${member.clientId}"]`);
|
|
259
|
+
const row = box.locator('xpath=ancestor::tr[1]');
|
|
260
|
+
await row.getByRole('button', { name: 'Assign Unit', exact: true }).click();
|
|
261
|
+
const d = page.getByRole('dialog').filter({ has: ctl('ChooseUnitPopup.selectBtn') });
|
|
262
|
+
const select = d.locator('select');
|
|
263
|
+
await select.locator('option').filter({ hasText: member.bedName }).waitFor({ state: 'attached' });
|
|
264
|
+
await ready();
|
|
265
|
+
const matches = await select.locator('option').evaluateAll((es, name) => es.filter(e => e.textContent === name || e.textContent.endsWith(' / ' + name)).map(e => ({ value: e.value, text: e.textContent })), member.bedName);
|
|
266
|
+
if (matches.length !== 1 || matches[0].value === '-1')
|
|
267
|
+
throw Error('Selected household bed unavailable or ambiguous');
|
|
268
|
+
await select.selectOption(matches[0].value);
|
|
269
|
+
await ctl('ChooseUnitPopup.selectBtn').click();
|
|
270
|
+
await d.waitFor({ state: 'hidden' });
|
|
271
|
+
await ready();
|
|
272
|
+
if (!(await row.innerText()).includes(member.bedName))
|
|
273
|
+
throw Error('Bed selection mismatch');
|
|
274
|
+
}
|
|
275
|
+
if (hasEntry) {
|
|
276
|
+
await provider(input.providerId);
|
|
277
|
+
await page.locator('select[aria-label="Type"]').selectOption({ label: 'HUD' });
|
|
278
|
+
await ready();
|
|
279
|
+
}
|
|
280
|
+
await settleForm();
|
|
281
|
+
const relationship = page.locator('select[aria-label="Relationship to Head of Household"]:visible');
|
|
282
|
+
if (hasEntry && await relationship.count() !== 1)
|
|
283
|
+
throw Error('Head relationship field unavailable');
|
|
284
|
+
if (await relationship.count()) {
|
|
285
|
+
if (await relationship.count() !== 1)
|
|
286
|
+
throw Error('Ambiguous head relationship field');
|
|
287
|
+
const current = await relationship.inputValue();
|
|
288
|
+
if (current !== '-Select-' && current !== 'Self (head of household)')
|
|
289
|
+
throw Error('Conflicting head relationship must be reviewed');
|
|
290
|
+
await relationship.selectOption({ label: 'Self (head of household)' });
|
|
291
|
+
}
|
|
292
|
+
await page.locator('textarea[name="codesNotesBox"]').fill(input.marker);
|
|
293
|
+
await settleForm();
|
|
294
|
+
await requireAssessmentAnswers();
|
|
295
|
+
await save('ShelterPointCheckIn.saveExitBtn', beforeSave);
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
if (await ctl('ChooseUnitPopup.cancelBtn').count())
|
|
299
|
+
await ctl('ChooseUnitPopup.cancelBtn').click();
|
|
300
|
+
if (await ctl('ShelterPointCheckIn.exitBtn').count()) {
|
|
301
|
+
await ctl('ShelterPointCheckIn.exitBtn').click();
|
|
302
|
+
await ready();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
const identity = async ({ stays, unitListId, marker }) => {
|
|
307
|
+
const rows = await readEnrollments({ clientIds: stays.map(s => s.clientId), stayIds: stays.map(s => s.serviceId) });
|
|
308
|
+
if (rows.length !== stays.length)
|
|
309
|
+
throw Error('Shelter identity read incomplete');
|
|
310
|
+
for (const row of rows) {
|
|
311
|
+
if (!stays.some(s => s.serviceId === row.serviceId && s.clientId === row.clientId) || row.unitListId !== unitListId)
|
|
312
|
+
throw Error('Shelter identity mismatch');
|
|
313
|
+
id(row.bedId);
|
|
314
|
+
const strings = [base, readWire.policy, 'com.bowmansystems.sp5.core.client.services.GenericService', 'getNeedService', 'I'];
|
|
315
|
+
const result = await page.evaluate(async (w) => { const r = await fetch(w.url, { method: 'POST', headers: w.headers, body: w.body, credentials: 'same-origin', redirect: 'error', signal: AbortSignal.timeout(30000) }); return { status: r.status, body: await r.text() }; }, { url: base + 'service/UnitListService', headers: readWire.headers, body: ['7', '0', 5, ...strings, 1, 2, 3, 4, 1, 5, row.serviceId, ''].join('|') });
|
|
316
|
+
if (result.status !== 200 || !result.body.startsWith('//OK[') || !result.body.includes(JSON.stringify(marker)))
|
|
317
|
+
throw Error('Shelter source marker missing or changed');
|
|
318
|
+
}
|
|
319
|
+
return rows;
|
|
320
|
+
};
|
|
321
|
+
const checkout = async (input, beforeSave) => {
|
|
322
|
+
if (await inventory(input) !== input.unitListName)
|
|
323
|
+
throw Error('Unit list changed');
|
|
324
|
+
const anchor = input.stays.find(s => s.clientId === input.clientIds[0]);
|
|
325
|
+
if (!anchor)
|
|
326
|
+
throw Error('Checkout target missing');
|
|
327
|
+
const row = page.locator('[id^="ShelterPointMainView.shelter.checkOutImage-"]:visible').locator('xpath=ancestor::tr[1]').filter({ hasText: '(' + anchor.clientId + ')' });
|
|
328
|
+
if (await row.count() !== 1 || !await row.getByText(anchor.bedName, { exact: true }).isVisible())
|
|
329
|
+
throw Error('Checkout bed or client changed');
|
|
330
|
+
const load = page.waitForResponse(r => r.url() === base + 'service/UnitListService' && r.request().postData()?.includes('|getNeedService|'));
|
|
331
|
+
void load.catch(() => { });
|
|
332
|
+
await row.locator('[title="Check Out"]').click();
|
|
333
|
+
const response = await load;
|
|
334
|
+
if (!response.request().postData().endsWith('|' + anchor.serviceId + '|') || response.status() !== 200 || !(await response.text()).startsWith('//OK['))
|
|
335
|
+
throw Error('Checkout stay identity mismatch');
|
|
336
|
+
await ctl('ShelterPointCheckOut.saveExitBtn').waitFor();
|
|
337
|
+
await settleForm();
|
|
338
|
+
try {
|
|
339
|
+
if (!(await page.locator('body').innerText()).includes('Unit Exit Data - (' + anchor.clientId + ')'))
|
|
340
|
+
throw Error('Checkout client mismatch');
|
|
341
|
+
await setDate('Date Out', input.checkoutDate);
|
|
342
|
+
await settleForm();
|
|
343
|
+
await chooseMembers(input.clientIds, 'HouseholdsCheckboxTree.householdCheckbox.', anchor.clientId);
|
|
344
|
+
for (const [field, value] of [['Destination', input.destination], ['Reason For Leaving', input.reasonForLeaving]]) {
|
|
345
|
+
const select = page.locator('select[aria-label="' + field + '"]');
|
|
346
|
+
if (input.mode === 'entry-exit') {
|
|
347
|
+
const matches = await select.locator('option').evaluateAll((es, label) => es.filter(e => e.textContent.replace(/\s*\(HUD\)\s*$/, '').trim() === label.replace(/\s*\(HUD\)\s*$/, '').trim()).map(e => e.value), value);
|
|
348
|
+
if (matches.length !== 1)
|
|
349
|
+
throw Error('Checkout choice unavailable or ambiguous');
|
|
350
|
+
await select.selectOption(matches[0]);
|
|
351
|
+
}
|
|
352
|
+
else if (await select.isVisible() && await select.inputValue() !== '-Select-')
|
|
353
|
+
throw Error('Unexpected nightly exit choices');
|
|
354
|
+
}
|
|
355
|
+
await settleForm();
|
|
356
|
+
await requireAssessmentAnswers();
|
|
357
|
+
await save('ShelterPointCheckOut.saveExitBtn', beforeSave);
|
|
358
|
+
}
|
|
359
|
+
finally {
|
|
360
|
+
if (await ctl('ShelterPointCheckOut.exitBtn').count()) {
|
|
361
|
+
await ctl('ShelterPointCheckOut.exitBtn').click();
|
|
362
|
+
await ready();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
return {connection,
|
|
367
|
+
assertBedsAvailable: input => withPageLock(page, async () => {
|
|
368
|
+
await inventory(input);
|
|
369
|
+
for (const member of input.members) {
|
|
370
|
+
const row = page.getByText(member.bedName, { exact: true }).locator('xpath=ancestor::tr[1]');
|
|
371
|
+
if (await row.count() !== 1 || !await row.getByText('EMPTY', { exact: true }).isVisible() || await row.locator('[id^="ShelterPointMainView.shelter.checkInImage-"]').count() !== 1)
|
|
372
|
+
throw Error('Selected household bed unavailable');
|
|
373
|
+
}
|
|
374
|
+
}),
|
|
375
|
+
listEnrollments: input => withPageLock(page, () => readEnrollments(input)),
|
|
376
|
+
verifyStayIdentity: input => withPageLock(page, () => identity(input)),
|
|
377
|
+
checkout: (input, beforeSave) => withPageLock(page, () => trackForm(() => checkout(input, beforeSave))),
|
|
378
|
+
confirmNoServices: input => withPageLock(page, async () => (await readEnrollments({ clientIds: [id(input.clientId)], serviceIds: true })).length === 0),
|
|
379
|
+
getUnitList: input => withPageLock(page, async () => ({ unitListId: input.unitListId, name: await inventory(input) })),
|
|
380
|
+
checkIn: (input, beforeSave) => withPageLock(page, () => trackForm(() => checkIn(input, beforeSave))),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { Page } from 'playwright-core';
|
|
2
|
+
import type { WellSkyClient, GwtEnrollmentHelpers } from '../dist/esm/index.js';
|
|
3
|
+
import type { UiMutationStore } from './index.mjs';
|
|
4
|
+
export type ShelterMode = 'night-by-night' | 'entry-exit';
|
|
5
|
+
export interface ShelterProgram {
|
|
6
|
+
providerId: number;
|
|
7
|
+
name: string;
|
|
8
|
+
projectType: string;
|
|
9
|
+
trackingMethod: string;
|
|
10
|
+
mode: ShelterMode;
|
|
11
|
+
}
|
|
12
|
+
export interface ShelterMemberBed {
|
|
13
|
+
clientId: number; /** Exact unique bed name within the selected unit list. Overflow is currently unsupported. */
|
|
14
|
+
bedName: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ShelterCheckInInput {
|
|
17
|
+
key: string;
|
|
18
|
+
providerId: number;
|
|
19
|
+
unitListId: number;
|
|
20
|
+
householdId: number;
|
|
21
|
+
headClientId: number;
|
|
22
|
+
entryDate: string;
|
|
23
|
+
members: ShelterMemberBed[];
|
|
24
|
+
/** Night-by-night only. Default requires covering enrollments. Explicit creation journals the first head enrollment and attaches members. Entry/exit check-in creates its own enrollments through the UI. */
|
|
25
|
+
enrollmentStrategy?: 'require-existing' | 'create-if-missing';
|
|
26
|
+
}
|
|
27
|
+
export interface ShelterCheckoutInput {
|
|
28
|
+
key: string;
|
|
29
|
+
checkInKey: string;
|
|
30
|
+
clientIds: number[];
|
|
31
|
+
checkoutDate: string;
|
|
32
|
+
/** Calendar date of the last occupied night, established by the caller. Checkout must be the following calendar day in timeZone. */
|
|
33
|
+
lastNight: string;
|
|
34
|
+
timeZone: string;
|
|
35
|
+
/** Required only for entry/exit mode. Conditional Other/subsidy destinations are not supported by this initial shelter adapter. */
|
|
36
|
+
destination?: string | null;
|
|
37
|
+
reasonForLeaving?: string | null;
|
|
38
|
+
}
|
|
39
|
+
export interface ShelterStay {
|
|
40
|
+
serviceId: number;
|
|
41
|
+
clientId: number;
|
|
42
|
+
providerId: number;
|
|
43
|
+
needId: number;
|
|
44
|
+
start: string;
|
|
45
|
+
end: string | null;
|
|
46
|
+
unitListName: string;
|
|
47
|
+
bedName: string;
|
|
48
|
+
groupId: number | null;
|
|
49
|
+
note: string;
|
|
50
|
+
}
|
|
51
|
+
export interface ShelterEnrollment {
|
|
52
|
+
enrollmentId: number;
|
|
53
|
+
clientId: number;
|
|
54
|
+
providerId: number;
|
|
55
|
+
entryDate: string;
|
|
56
|
+
exitDate: string | null;
|
|
57
|
+
groupId: number | null;
|
|
58
|
+
type: string;
|
|
59
|
+
destination: string | null;
|
|
60
|
+
reason: string | null;
|
|
61
|
+
}
|
|
62
|
+
export interface ShelterResult {
|
|
63
|
+
key: string;
|
|
64
|
+
outcome: 'verified' | 'already-present' | 'reconciled';
|
|
65
|
+
mode: ShelterMode;
|
|
66
|
+
stays: ShelterStay[];
|
|
67
|
+
enrollments: ShelterEnrollment[];
|
|
68
|
+
}
|
|
69
|
+
export interface ShelterUiPort {
|
|
70
|
+
assertBedsAvailable(input: ShelterCheckInInput): Promise<void>;
|
|
71
|
+
verifyStayIdentity(input: {
|
|
72
|
+
stays: ShelterStay[];
|
|
73
|
+
unitListId: number;
|
|
74
|
+
marker: string;
|
|
75
|
+
}): Promise<Array<{
|
|
76
|
+
serviceId: number;
|
|
77
|
+
clientId: number;
|
|
78
|
+
bedId: number;
|
|
79
|
+
unitListId: number;
|
|
80
|
+
}>>;
|
|
81
|
+
listEnrollments(input: {
|
|
82
|
+
clientIds: number[];
|
|
83
|
+
}): Promise<ShelterEnrollment[]>;
|
|
84
|
+
confirmNoServices(input: {
|
|
85
|
+
clientId: number;
|
|
86
|
+
}): Promise<boolean>;
|
|
87
|
+
getUnitList(input: {
|
|
88
|
+
providerId: number;
|
|
89
|
+
unitListId: number;
|
|
90
|
+
}): Promise<{
|
|
91
|
+
unitListId: number;
|
|
92
|
+
name: string;
|
|
93
|
+
}>;
|
|
94
|
+
/** One fresh UI save, after beforeSave resolves. Never replay a failed save. */
|
|
95
|
+
checkIn(input: ShelterCheckInInput & {
|
|
96
|
+
mode: ShelterMode;
|
|
97
|
+
unitListName: string;
|
|
98
|
+
marker: string;
|
|
99
|
+
}, beforeSave: () => Promise<void>): Promise<void>;
|
|
100
|
+
/** One fresh UI save, after beforeSave resolves. Exact selected stay IDs must be verified first. */
|
|
101
|
+
checkout(input: ShelterCheckInInput & ShelterCheckoutInput & {
|
|
102
|
+
mode: ShelterMode;
|
|
103
|
+
unitListName: string;
|
|
104
|
+
stays: ShelterStay[];
|
|
105
|
+
}, beforeSave: () => Promise<void>): Promise<void>;
|
|
106
|
+
}
|
|
107
|
+
export interface Shelters {
|
|
108
|
+
/** Cancels only a journaled check-in with no shelter save attempt and no uncertain enrollment write. Never deletes server records. Retained enrollments are returned explicitly. */
|
|
109
|
+
cancelUnsubmittedCheckIn(input: {
|
|
110
|
+
key: string;
|
|
111
|
+
}): Promise<{
|
|
112
|
+
key: string;
|
|
113
|
+
outcome: 'cancelled' | 'already-cancelled';
|
|
114
|
+
enrollments: ShelterEnrollment[];
|
|
115
|
+
}>;
|
|
116
|
+
getProgram(input: {
|
|
117
|
+
providerId: number;
|
|
118
|
+
}): Promise<ShelterProgram>;
|
|
119
|
+
checkIn(input: ShelterCheckInInput): Promise<ShelterResult>;
|
|
120
|
+
/** Night-by-night checkout preserves program enrollment. Entry/exit checkout requires an enrollment exit for selected members. */
|
|
121
|
+
checkout(input: ShelterCheckoutInput): Promise<ShelterResult>;
|
|
122
|
+
}
|
|
123
|
+
export function readShelterProgram(api: Pick<WellSkyClient, 'providers'>, input: {
|
|
124
|
+
providerId: number;
|
|
125
|
+
}): Promise<ShelterProgram>;
|
|
126
|
+
export function createShelters(options: {
|
|
127
|
+
api: Pick<WellSkyClient, 'tenant' | 'providers' | 'households' | 'services' | 'enrollments'>;
|
|
128
|
+
ui: ShelterUiPort;
|
|
129
|
+
store: UiMutationStore;
|
|
130
|
+
enrollmentHelpers?: Pick<GwtEnrollmentHelpers, 'addHouseholdMembers'>;
|
|
131
|
+
}): Shelters;
|
|
132
|
+
export function createShelterUi(options: {
|
|
133
|
+
page: Page;
|
|
134
|
+
}): ShelterUiPort;
|
|
135
|
+
export class ShelterConflictError extends Error {
|
|
136
|
+
readonly code: 'SHELTER_CONFLICT';
|
|
137
|
+
readonly reason: string;
|
|
138
|
+
constructor(reason: string);
|
|
139
|
+
}
|