@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.
Files changed (294) hide show
  1. package/AGENTS.md +43 -0
  2. package/API.md +86 -0
  3. package/CHANGELOG.md +12 -0
  4. package/LICENSE +201 -0
  5. package/NOTICE +11 -0
  6. package/README.md +100 -0
  7. package/SSVF.md +131 -0
  8. package/browser/attachment-conflict.mjs +6 -0
  9. package/browser/case-plans-ui.mjs +23 -0
  10. package/browser/case-plans.d.mts +20 -0
  11. package/browser/case-plans.mjs +89 -0
  12. package/browser/client-creation.d.mts +16 -0
  13. package/browser/client-creation.mjs +60 -0
  14. package/browser/client-profile-ui.mjs +59 -0
  15. package/browser/client-profile.d.mts +24 -0
  16. package/browser/client-profile.mjs +94 -0
  17. package/browser/client-workflows.mjs +135 -0
  18. package/browser/connection.d.mts +4 -0
  19. package/browser/connection.mjs +41 -0
  20. package/browser/current-living-situation.d.mts +44 -0
  21. package/browser/current-living-situation.mjs +161 -0
  22. package/browser/date-of-birth-ui.mjs +68 -0
  23. package/browser/date-of-birth.d.mts +28 -0
  24. package/browser/date-of-birth.mjs +112 -0
  25. package/browser/enrollment-workflow.d.mts +174 -0
  26. package/browser/enrollment-workflow.mjs +518 -0
  27. package/browser/exit-details.mjs +21 -0
  28. package/browser/file-store.mjs +32 -0
  29. package/browser/gwt-mapped.mjs +46 -0
  30. package/browser/household-exit.d.mts +34 -0
  31. package/browser/household-exit.mjs +102 -0
  32. package/browser/households-ui.mjs +20 -0
  33. package/browser/households.d.mts +22 -0
  34. package/browser/households.mjs +115 -0
  35. package/browser/index.d.mts +78 -0
  36. package/browser/index.mjs +46 -0
  37. package/browser/member-entry-date.d.mts +30 -0
  38. package/browser/member-entry-date.mjs +71 -0
  39. package/browser/need-recording.d.mts +37 -0
  40. package/browser/need-recording.mjs +68 -0
  41. package/browser/need-ui.mjs +138 -0
  42. package/browser/page-lock.mjs +11 -0
  43. package/browser/password-recovery.mjs +49 -0
  44. package/browser/playwright-session.mjs +196 -0
  45. package/browser/program-discovery.d.mts +22 -0
  46. package/browser/program-discovery.mjs +80 -0
  47. package/browser/recovery-diagnostics.mjs +9 -0
  48. package/browser/service-recording.d.mts +45 -0
  49. package/browser/service-recording.mjs +148 -0
  50. package/browser/service-ui.mjs +160 -0
  51. package/browser/session-authentication.mjs +40 -0
  52. package/browser/shelter-inventory.d.mts +103 -0
  53. package/browser/shelter-inventory.mjs +273 -0
  54. package/browser/shelter-ui.mjs +382 -0
  55. package/browser/shelters.d.mts +139 -0
  56. package/browser/shelters.mjs +425 -0
  57. package/browser/ssvf-rules.d.mts +21 -0
  58. package/browser/ssvf-rules.mjs +145 -0
  59. package/browser/ui-login.mjs +135 -0
  60. package/dist/cjs/bulk.d.ts +33 -0
  61. package/dist/cjs/bulk.d.ts.map +1 -0
  62. package/dist/cjs/bulk.js +42 -0
  63. package/dist/cjs/bulk.js.map +1 -0
  64. package/dist/cjs/control.d.ts +35 -0
  65. package/dist/cjs/control.d.ts.map +1 -0
  66. package/dist/cjs/control.js +141 -0
  67. package/dist/cjs/control.js.map +1 -0
  68. package/dist/cjs/effective-date.d.ts +3 -0
  69. package/dist/cjs/effective-date.d.ts.map +1 -0
  70. package/dist/cjs/effective-date.js +27 -0
  71. package/dist/cjs/effective-date.js.map +1 -0
  72. package/dist/cjs/encoding.d.ts +32 -0
  73. package/dist/cjs/encoding.d.ts.map +1 -0
  74. package/dist/cjs/encoding.js +121 -0
  75. package/dist/cjs/encoding.js.map +1 -0
  76. package/dist/cjs/enrollment-verifications.d.ts +39 -0
  77. package/dist/cjs/enrollment-verifications.d.ts.map +1 -0
  78. package/dist/cjs/enrollment-verifications.js +96 -0
  79. package/dist/cjs/enrollment-verifications.js.map +1 -0
  80. package/dist/cjs/errors.d.ts +33 -0
  81. package/dist/cjs/errors.d.ts.map +1 -0
  82. package/dist/cjs/errors.js +45 -0
  83. package/dist/cjs/errors.js.map +1 -0
  84. package/dist/cjs/generated/v2-models.d.ts +1482 -0
  85. package/dist/cjs/generated/v2-models.d.ts.map +1 -0
  86. package/dist/cjs/generated/v2-models.js +3 -0
  87. package/dist/cjs/generated/v2-models.js.map +1 -0
  88. package/dist/cjs/generated/v2.d.ts +1149 -0
  89. package/dist/cjs/generated/v2.d.ts.map +1 -0
  90. package/dist/cjs/generated/v2.js +252 -0
  91. package/dist/cjs/generated/v2.js.map +1 -0
  92. package/dist/cjs/generated/v3-models.d.ts +1420 -0
  93. package/dist/cjs/generated/v3-models.d.ts.map +1 -0
  94. package/dist/cjs/generated/v3-models.js +3 -0
  95. package/dist/cjs/generated/v3-models.js.map +1 -0
  96. package/dist/cjs/generated/v3.d.ts +1149 -0
  97. package/dist/cjs/generated/v3.d.ts.map +1 -0
  98. package/dist/cjs/generated/v3.js +252 -0
  99. package/dist/cjs/generated/v3.js.map +1 -0
  100. package/dist/cjs/gwt-enrollments.d.ts +131 -0
  101. package/dist/cjs/gwt-enrollments.d.ts.map +1 -0
  102. package/dist/cjs/gwt-enrollments.js +280 -0
  103. package/dist/cjs/gwt-enrollments.js.map +1 -0
  104. package/dist/cjs/index.d.ts +3 -0
  105. package/dist/cjs/index.d.ts.map +1 -0
  106. package/dist/cjs/index.js +19 -0
  107. package/dist/cjs/index.js.map +1 -0
  108. package/dist/cjs/metadata.d.ts +100 -0
  109. package/dist/cjs/metadata.d.ts.map +1 -0
  110. package/dist/cjs/metadata.js +165 -0
  111. package/dist/cjs/metadata.js.map +1 -0
  112. package/dist/cjs/package.json +1 -0
  113. package/dist/cjs/program-discovery.d.ts +28 -0
  114. package/dist/cjs/program-discovery.d.ts.map +1 -0
  115. package/dist/cjs/program-discovery.js +58 -0
  116. package/dist/cjs/program-discovery.js.map +1 -0
  117. package/dist/cjs/race.d.ts +4 -0
  118. package/dist/cjs/race.d.ts.map +1 -0
  119. package/dist/cjs/race.js +40 -0
  120. package/dist/cjs/race.js.map +1 -0
  121. package/dist/cjs/runtime.d.ts +108 -0
  122. package/dist/cjs/runtime.d.ts.map +1 -0
  123. package/dist/cjs/runtime.js +38 -0
  124. package/dist/cjs/runtime.js.map +1 -0
  125. package/dist/cjs/subassessments.d.ts +29 -0
  126. package/dist/cjs/subassessments.d.ts.map +1 -0
  127. package/dist/cjs/subassessments.js +107 -0
  128. package/dist/cjs/subassessments.js.map +1 -0
  129. package/dist/cjs/testing.d.ts +45 -0
  130. package/dist/cjs/testing.d.ts.map +1 -0
  131. package/dist/cjs/testing.js +66 -0
  132. package/dist/cjs/testing.js.map +1 -0
  133. package/dist/cjs/transport.d.ts +19 -0
  134. package/dist/cjs/transport.d.ts.map +1 -0
  135. package/dist/cjs/transport.js +373 -0
  136. package/dist/cjs/transport.js.map +1 -0
  137. package/dist/cjs/v2.d.ts +13 -0
  138. package/dist/cjs/v2.d.ts.map +1 -0
  139. package/dist/cjs/v2.js +29 -0
  140. package/dist/cjs/v2.js.map +1 -0
  141. package/dist/cjs/v3.d.ts +13 -0
  142. package/dist/cjs/v3.d.ts.map +1 -0
  143. package/dist/cjs/v3.js +29 -0
  144. package/dist/cjs/v3.js.map +1 -0
  145. package/dist/cjs/validation.d.ts +33 -0
  146. package/dist/cjs/validation.d.ts.map +1 -0
  147. package/dist/cjs/validation.js +77 -0
  148. package/dist/cjs/validation.js.map +1 -0
  149. package/dist/cjs/verification.d.ts +82 -0
  150. package/dist/cjs/verification.d.ts.map +1 -0
  151. package/dist/cjs/verification.js +122 -0
  152. package/dist/cjs/verification.js.map +1 -0
  153. package/dist/esm/bulk.d.ts +33 -0
  154. package/dist/esm/bulk.d.ts.map +1 -0
  155. package/dist/esm/bulk.js +39 -0
  156. package/dist/esm/bulk.js.map +1 -0
  157. package/dist/esm/control.d.ts +35 -0
  158. package/dist/esm/control.d.ts.map +1 -0
  159. package/dist/esm/control.js +132 -0
  160. package/dist/esm/control.js.map +1 -0
  161. package/dist/esm/effective-date.d.ts +3 -0
  162. package/dist/esm/effective-date.d.ts.map +1 -0
  163. package/dist/esm/effective-date.js +24 -0
  164. package/dist/esm/effective-date.js.map +1 -0
  165. package/dist/esm/encoding.d.ts +32 -0
  166. package/dist/esm/encoding.d.ts.map +1 -0
  167. package/dist/esm/encoding.js +117 -0
  168. package/dist/esm/encoding.js.map +1 -0
  169. package/dist/esm/enrollment-verifications.d.ts +39 -0
  170. package/dist/esm/enrollment-verifications.d.ts.map +1 -0
  171. package/dist/esm/enrollment-verifications.js +93 -0
  172. package/dist/esm/enrollment-verifications.js.map +1 -0
  173. package/dist/esm/errors.d.ts +33 -0
  174. package/dist/esm/errors.d.ts.map +1 -0
  175. package/dist/esm/errors.js +33 -0
  176. package/dist/esm/errors.js.map +1 -0
  177. package/dist/esm/generated/v2-models.d.ts +1482 -0
  178. package/dist/esm/generated/v2-models.d.ts.map +1 -0
  179. package/dist/esm/generated/v2-models.js +2 -0
  180. package/dist/esm/generated/v2-models.js.map +1 -0
  181. package/dist/esm/generated/v2.d.ts +1149 -0
  182. package/dist/esm/generated/v2.d.ts.map +1 -0
  183. package/dist/esm/generated/v2.js +247 -0
  184. package/dist/esm/generated/v2.js.map +1 -0
  185. package/dist/esm/generated/v3-models.d.ts +1420 -0
  186. package/dist/esm/generated/v3-models.d.ts.map +1 -0
  187. package/dist/esm/generated/v3-models.js +2 -0
  188. package/dist/esm/generated/v3-models.js.map +1 -0
  189. package/dist/esm/generated/v3.d.ts +1149 -0
  190. package/dist/esm/generated/v3.d.ts.map +1 -0
  191. package/dist/esm/generated/v3.js +247 -0
  192. package/dist/esm/generated/v3.js.map +1 -0
  193. package/dist/esm/gwt-enrollments.d.ts +131 -0
  194. package/dist/esm/gwt-enrollments.d.ts.map +1 -0
  195. package/dist/esm/gwt-enrollments.js +274 -0
  196. package/dist/esm/gwt-enrollments.js.map +1 -0
  197. package/dist/esm/index.d.ts +3 -0
  198. package/dist/esm/index.d.ts.map +1 -0
  199. package/dist/esm/index.js +3 -0
  200. package/dist/esm/index.js.map +1 -0
  201. package/dist/esm/metadata.d.ts +100 -0
  202. package/dist/esm/metadata.d.ts.map +1 -0
  203. package/dist/esm/metadata.js +157 -0
  204. package/dist/esm/metadata.js.map +1 -0
  205. package/dist/esm/program-discovery.d.ts +28 -0
  206. package/dist/esm/program-discovery.d.ts.map +1 -0
  207. package/dist/esm/program-discovery.js +55 -0
  208. package/dist/esm/program-discovery.js.map +1 -0
  209. package/dist/esm/race.d.ts +4 -0
  210. package/dist/esm/race.d.ts.map +1 -0
  211. package/dist/esm/race.js +37 -0
  212. package/dist/esm/race.js.map +1 -0
  213. package/dist/esm/runtime.d.ts +108 -0
  214. package/dist/esm/runtime.d.ts.map +1 -0
  215. package/dist/esm/runtime.js +33 -0
  216. package/dist/esm/runtime.js.map +1 -0
  217. package/dist/esm/subassessments.d.ts +29 -0
  218. package/dist/esm/subassessments.d.ts.map +1 -0
  219. package/dist/esm/subassessments.js +103 -0
  220. package/dist/esm/subassessments.js.map +1 -0
  221. package/dist/esm/testing.d.ts +45 -0
  222. package/dist/esm/testing.d.ts.map +1 -0
  223. package/dist/esm/testing.js +60 -0
  224. package/dist/esm/testing.js.map +1 -0
  225. package/dist/esm/transport.d.ts +19 -0
  226. package/dist/esm/transport.d.ts.map +1 -0
  227. package/dist/esm/transport.js +369 -0
  228. package/dist/esm/transport.js.map +1 -0
  229. package/dist/esm/v2.d.ts +13 -0
  230. package/dist/esm/v2.d.ts.map +1 -0
  231. package/dist/esm/v2.js +13 -0
  232. package/dist/esm/v2.js.map +1 -0
  233. package/dist/esm/v3.d.ts +13 -0
  234. package/dist/esm/v3.d.ts.map +1 -0
  235. package/dist/esm/v3.js +13 -0
  236. package/dist/esm/v3.js.map +1 -0
  237. package/dist/esm/validation.d.ts +33 -0
  238. package/dist/esm/validation.d.ts.map +1 -0
  239. package/dist/esm/validation.js +72 -0
  240. package/dist/esm/validation.js.map +1 -0
  241. package/dist/esm/verification.d.ts +82 -0
  242. package/dist/esm/verification.d.ts.map +1 -0
  243. package/dist/esm/verification.js +117 -0
  244. package/dist/esm/verification.js.map +1 -0
  245. package/docs/README.md +56 -0
  246. package/docs/assessments.md +103 -0
  247. package/docs/browser-session.md +129 -0
  248. package/docs/capabilities.md +75 -0
  249. package/docs/configuration.md +81 -0
  250. package/docs/current-living-situation.md +56 -0
  251. package/docs/dates-and-answers.md +40 -0
  252. package/docs/development.md +81 -0
  253. package/docs/durable-records.md +156 -0
  254. package/docs/enrollments.md +217 -0
  255. package/docs/files-and-notes.md +39 -0
  256. package/docs/getting-started.md +56 -0
  257. package/docs/independent-needs.md +77 -0
  258. package/docs/integration-methods.md +60 -0
  259. package/docs/profiles.md +68 -0
  260. package/docs/reference.md +64 -0
  261. package/docs/rest-client.md +97 -0
  262. package/docs/services.md +66 -0
  263. package/docs/shelter-inventory.md +64 -0
  264. package/docs/shelters.md +75 -0
  265. package/docs/troubleshooting.md +61 -0
  266. package/docs/using-the-sdk.md +203 -0
  267. package/docs/vendor-issues/README.md +46 -0
  268. package/docs/vendor-issues/api-coverage.md +27 -0
  269. package/docs/vendor-issues/attachment-authentication.md +40 -0
  270. package/docs/vendor-issues/date-semantics.md +51 -0
  271. package/docs/vendor-issues/empty-service-list-404.md +51 -0
  272. package/docs/vendor-issues/enrollment-complete-target-error.md +178 -0
  273. package/docs/vendor-issues/enrollment-effective-date-column.md +112 -0
  274. package/docs/vendor-issues/household-head-validation.md +26 -0
  275. package/docs/vendor-issues/write-contracts.md +51 -0
  276. package/examples/README.md +41 -0
  277. package/examples/current-living-situation.mts +27 -0
  278. package/examples/durable-records.mts +78 -0
  279. package/examples/enrollment-workflow.mts +46 -0
  280. package/examples/gwt-enrollment-session.ts +17 -0
  281. package/examples/gwt-file-store.mjs +2 -0
  282. package/examples/household-enrollment-workflow.mts +43 -0
  283. package/examples/household-exit-workflow.mts +35 -0
  284. package/examples/household-lifecycle-workflow.mts +34 -0
  285. package/examples/independent-needs.mts +30 -0
  286. package/examples/program-discovery.ts +19 -0
  287. package/examples/quickstart.mjs +13 -0
  288. package/examples/shelter-inventory.mts +30 -0
  289. package/examples/shelters.mts +33 -0
  290. package/examples/usage.ts +23 -0
  291. package/examples/verification-snapshot.ts +9 -0
  292. package/llms.txt +26 -0
  293. package/package.json +103 -0
  294. 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
+ }