@reqquest/ui 1.17.0 → 1.18.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 (100) hide show
  1. package/app/env.template +5 -0
  2. package/app/runtime/apply-env.sh +10 -0
  3. package/app/runtime/nginx.conf +37 -0
  4. package/app/src/app.css +21 -0
  5. package/app/src/app.d.ts +13 -0
  6. package/app/src/app.html +12 -0
  7. package/app/src/internal/api.ts +1707 -0
  8. package/app/src/internal/appreq-utils.ts +33 -0
  9. package/app/src/internal/components/AnnouncementBanner.svelte +47 -0
  10. package/app/src/internal/components/AppRequestCard.svelte +173 -0
  11. package/app/src/internal/components/ApplicantOptOutModal.svelte +78 -0
  12. package/app/src/internal/components/ApplicantProgramList.svelte +276 -0
  13. package/app/src/internal/components/ApplicantProgramListTooltip.svelte +44 -0
  14. package/app/src/internal/components/ApplicantPromptPage.svelte +136 -0
  15. package/app/src/internal/components/ApplicantPromptSkeleton.svelte +12 -0
  16. package/app/src/internal/components/ApplicationDetailsView.svelte +369 -0
  17. package/app/src/internal/components/ButtonLoadingIcon.svelte +28 -0
  18. package/{dist → app/src/internal}/components/DelayedSkeleton.svelte +2 -2
  19. package/app/src/internal/components/FieldNestedMultiselect.svelte +270 -0
  20. package/app/src/internal/components/IntroPanel.svelte +41 -0
  21. package/app/src/internal/components/MissingDefinitionNotification.svelte +20 -0
  22. package/app/src/internal/components/PeriodPanel.svelte +125 -0
  23. package/app/src/internal/components/ProgramStatusCell.svelte +134 -0
  24. package/app/src/internal/components/RenderDisplayComponent.svelte +42 -0
  25. package/app/src/internal/components/ReviewerActions.svelte +171 -0
  26. package/app/src/internal/components/ReviewerQuestions.svelte +444 -0
  27. package/app/src/internal/components/StatusMessageList.svelte +54 -0
  28. package/app/src/internal/components/SystemDeterminationNotice.svelte +37 -0
  29. package/app/src/internal/components/TabLinks.svelte +265 -0
  30. package/app/src/internal/components/WarningIconYellow.svelte +22 -0
  31. package/app/src/internal/components/index.ts +19 -0
  32. package/app/src/internal/csv.ts +30 -0
  33. package/app/src/internal/index.ts +8 -0
  34. package/app/src/internal/prompt-save-queue.ts +91 -0
  35. package/app/src/internal/prompt-utils.ts +40 -0
  36. package/app/src/internal/status-utils.ts +503 -0
  37. package/app/src/internal/stores/IStateStore.ts +5 -0
  38. package/app/src/internal/util.ts +27 -0
  39. package/app/src/routes/+error.svelte +4 -0
  40. package/app/src/routes/+layout.svelte +134 -0
  41. package/app/src/routes/+layout.ts +18 -0
  42. package/app/src/routes/+page.ts +14 -0
  43. package/app/src/routes/announcement/+page.svelte +164 -0
  44. package/app/src/routes/announcement/+page.ts +16 -0
  45. package/app/src/routes/dashboards/applicant/+page.svelte +494 -0
  46. package/app/src/routes/dashboards/applicant/+page.ts +95 -0
  47. package/app/src/routes/dashboards/reviewer/+page.svelte +143 -0
  48. package/app/src/routes/dashboards/reviewer/+page.ts +43 -0
  49. package/app/src/routes/periods/+page.svelte +158 -0
  50. package/app/src/routes/periods/+page.ts +13 -0
  51. package/app/src/routes/periods/[id]/configure/+page.svelte +150 -0
  52. package/app/src/routes/periods/[id]/configure/+page.ts +10 -0
  53. package/app/src/routes/requests/+page.svelte +246 -0
  54. package/app/src/routes/requests/+page.ts +24 -0
  55. package/app/src/routes/requests/[id]/+layout.ts +11 -0
  56. package/app/src/routes/requests/[id]/accept/+layout.svelte +80 -0
  57. package/app/src/routes/requests/[id]/accept/+layout.ts +12 -0
  58. package/app/src/routes/requests/[id]/accept/+page.ts +13 -0
  59. package/app/src/routes/requests/[id]/accept/[promptId]/+page.svelte +7 -0
  60. package/app/src/routes/requests/[id]/accept/[promptId]/+page.ts +10 -0
  61. package/app/src/routes/requests/[id]/accept/review/+page.svelte +87 -0
  62. package/app/src/routes/requests/[id]/apply/+layout.svelte +181 -0
  63. package/app/src/routes/requests/[id]/apply/+layout.ts +12 -0
  64. package/app/src/routes/requests/[id]/apply/+page.ts +14 -0
  65. package/app/src/routes/requests/[id]/apply/ProgressNavContainer.svelte +42 -0
  66. package/app/src/routes/requests/[id]/apply/[promptId]/+page.svelte +23 -0
  67. package/app/src/routes/requests/[id]/apply/[promptId]/+page.ts +11 -0
  68. package/app/src/routes/requests/[id]/apply/programs/+page.svelte +71 -0
  69. package/app/src/routes/requests/[id]/apply/review/+page.svelte +102 -0
  70. package/app/src/routes/requests/[id]/approve/+layout.svelte +114 -0
  71. package/app/src/routes/requests/[id]/approve/+page.ts +14 -0
  72. package/app/src/routes/requests/[id]/approve/ApproveLayout.svelte +164 -0
  73. package/app/src/routes/requests/[id]/approve/[programKey]/+page.svelte +396 -0
  74. package/app/src/routes/requests/[id]/approve/[programKey]/+page.ts +15 -0
  75. package/app/src/routes/requests/[id]/approve/activity/+page.svelte +66 -0
  76. package/app/src/routes/requests/[id]/approve/activity/+page.ts +12 -0
  77. package/app/src/routes/requests/[id]/approve/export/+page.ts +12 -0
  78. package/app/src/routes/requests/[id]/approve/export/+page@[id].svelte +41 -0
  79. package/app/src/routes/requests/[id]/export/+page.svelte +32 -0
  80. package/app/src/routes/requests/[id]/export/+page.ts +12 -0
  81. package/app/src/routes/roles/+layout.svelte +16 -0
  82. package/app/src/routes/roles/+layout.ts +14 -0
  83. package/app/src/routes/roles/+page.svelte +225 -0
  84. package/app/src/routes/roles/+page.ts +13 -0
  85. package/app/src/routes/roles/[id]/+layout.ts +10 -0
  86. package/app/src/routes/roles/[id]/+page.svelte +335 -0
  87. package/app/src/routes/roles/[id]/+page.ts +10 -0
  88. package/app/src/routes/roles/[id]/ControlWithTooltip.svelte +15 -0
  89. package/app/src/routes/roles/[id]/TagsWithTooltip.svelte +42 -0
  90. package/app/src/routes/roles/users/+layout.ts +13 -0
  91. package/app/src/routes/roles/users/+page.svelte +46 -0
  92. package/app/src/routes/roles/users/+page.ts +90 -0
  93. package/app/static/favicon.png +0 -0
  94. package/bin/reqquest-ui.js +117 -0
  95. package/cli/config.js +105 -0
  96. package/cli/sync.js +296 -0
  97. package/cli/util.js +29 -0
  98. package/dist/components.md +0 -4
  99. package/package.json +29 -15
  100. package/dist/components/DelayedSkeleton.svelte.d.ts +0 -21
@@ -0,0 +1,1707 @@
1
+ import { PUBLIC_API_BASE, PUBLIC_AUTH_REDIRECT, PUBLIC_SHOW_DUPLICATE_PROMPTS } from '$env/static/public'
2
+ import { error } from '@sveltejs/kit'
3
+ import { APIBase, type MutationResponseFromAPI } from '@txstate-mws/sveltekit-utils'
4
+ import type { Feedback } from '@txstate-mws/svelte-forms'
5
+ import { DateTime } from 'luxon'
6
+ import { keyby, omit, pick } from 'txstate-utils'
7
+ import {
8
+ createClient, enumAppRequestIndexDestination, enumIneligiblePhases, enumPromptVisibility, enumRequirementStatus, enumRequirementType,
9
+ type AccessRoleGrantCreate, type AccessRoleGrantUpdate, type AccessRoleGroup, type AccessRoleInput, type AccessUserFilter,
10
+ type AppRequestActivityFilters, type AppRequestFilter, type IneligiblePhases, type Pagination, type PeriodUpdate, type PromptVisibility,
11
+ type RequirementStatus, type RequirementType, type PhaseChangeMutations, type CompletionStatus, type StatusReason,
12
+ type PeriodFilters
13
+ } from '@reqquest/ui'
14
+ import { applicantVisiblePromptVisibilities } from './status-utils.js'
15
+
16
+ export const showDupePrompts = PUBLIC_SHOW_DUPLICATE_PROMPTS.trim() === 'true'
17
+ // the per-key configuration fields attached to prompt instances client-side, see API.attachPromptConfigs
18
+ export type PromptConfig = { configurationData: Record<string, any>, gatheredConfigData: Record<string, any> }
19
+ export type DashboardAppRequest = Awaited<ReturnType<typeof api.getApplicantRequests>>[number]
20
+ export type AppRequestForExportResponse = Awaited<ReturnType<typeof api.getAppRequestForExport>>
21
+ export type PromptForEditing = Awaited<ReturnType<typeof api.getApplicantPrompt>>['prompt']
22
+ export type ReviewData = Awaited<ReturnType<typeof api.getReviewData>>
23
+ export type PromptDataLegion = Awaited<ReturnType<typeof api.getPromptDataLegion>>
24
+ export type BasicRequestData = Awaited<ReturnType<typeof api.getBasicRequestData>>
25
+
26
+ class API extends APIBase {
27
+ baseUrl = PUBLIC_API_BASE
28
+ client = createClient({
29
+ url: this.baseUrl,
30
+ fetcher: async operation => {
31
+ if (Array.isArray(operation)) throw new Error('Batching not supported')
32
+ const data = await this.graphql(operation.query, operation.variables)
33
+ return { data }
34
+ }
35
+ })
36
+
37
+ async getDownloadTicket () {
38
+ const resp = await this.post<{ ticket: string }>('/ticket')
39
+ return resp.ticket
40
+ }
41
+
42
+ async getAccess () {
43
+ const response = await this.client.query({
44
+ __name: 'GetAccess',
45
+ access: {
46
+ user: {
47
+ login: true,
48
+ fullname: true
49
+ },
50
+ createPeriod: true,
51
+ createRole: true,
52
+ viewRoleManagement: true,
53
+ viewPeriodManagement: true,
54
+ viewReviewerInterface: true,
55
+ viewApplicantDashboard: true,
56
+ viewAppRequestList: true,
57
+ createAppRequestSelf: true,
58
+ createAppRequestOther: true,
59
+ viewAnnouncementManagement: true,
60
+ manageAnnouncements: true,
61
+ viewMetrics: true
62
+ }
63
+ })
64
+ return response.access
65
+ }
66
+
67
+ async getAccessUser (login: string) {
68
+ const filter: AccessUserFilter = { logins: [login] }
69
+ const response = await this.client.query({
70
+ __name: 'GetAccessUser',
71
+ accessUsers: {
72
+ __args: { filter },
73
+ login: true,
74
+ fullname: true,
75
+ otherInfo: true
76
+ }
77
+ })
78
+ return response.accessUsers[0]
79
+ }
80
+
81
+ async getAccessUsers (accessUsersFilter: AccessUserFilter, pageFilter: Pagination) {
82
+ const filter = accessUsersFilter
83
+ const paged = pageFilter
84
+ const response = await this.client.query({
85
+ __name: 'GetAccessUsers',
86
+ accessUsers: {
87
+ __args: { filter, paged },
88
+ login: true,
89
+ fullname: true,
90
+ groups: true,
91
+ roles: {
92
+ name: true
93
+ },
94
+ otherIdentifiers: {
95
+ id: true,
96
+ label: true
97
+ },
98
+ otherInfo: true
99
+ },
100
+ pageInfo: {
101
+ accessUsers: {
102
+ currentPage: true,
103
+ totalItems: true,
104
+ hasNextPage: true,
105
+ perPage: true,
106
+ categories: {
107
+ tags: {
108
+ tag: true,
109
+ label: true
110
+ },
111
+ category: true,
112
+ label: true,
113
+ useInFilters: true,
114
+ useInList: true
115
+ }
116
+ }
117
+ }
118
+ })
119
+ return { users: response.accessUsers, pageInfo: response.pageInfo.accessUsers }
120
+ }
121
+
122
+ async getApplicantRequests (additionalFilters: AppRequestFilter = {}) {
123
+ const filter: AppRequestFilter = { ...additionalFilters }
124
+ const response = await this.client.query({
125
+ __name: 'GetApplicantRequests',
126
+ appRequests: {
127
+ __args: { filter },
128
+ id: true,
129
+ status: true,
130
+ phase: true,
131
+ createdAt: true,
132
+ updatedAt: true,
133
+ closedAt: true,
134
+ applicant: {
135
+ login: true,
136
+ fullname: true,
137
+ otherIdentifiers: {
138
+ id: true
139
+ }
140
+ },
141
+ period: {
142
+ id: true,
143
+ name: true,
144
+ openDate: true,
145
+ closeDate: true
146
+ },
147
+ applications: {
148
+ id: true,
149
+ title: true,
150
+ status: true,
151
+ rescindedStatus: true,
152
+ statusReason: true,
153
+ ineligiblePhase: true,
154
+ requirements: {
155
+ id: true,
156
+ type: true,
157
+ status: true,
158
+ statusReason: true,
159
+ prompts: {
160
+ id: true,
161
+ visibility: true,
162
+ moot: true,
163
+ invalidated: true,
164
+ invalidatedReason: true
165
+ }
166
+ }
167
+ },
168
+ actions: {
169
+ acceptOffer: true,
170
+ cancel: true,
171
+ reopen: true,
172
+ returnToApplicant: true,
173
+ returnToOffer: true,
174
+ submit: true
175
+ }
176
+ }
177
+ })
178
+ return response.appRequests
179
+ }
180
+
181
+ async getReviewerDashboardRequests (filter: AppRequestFilter = {}, paged?: Pagination) {
182
+ const response = await this.client.query({
183
+ __name: 'GetReviewerDashboardRequests',
184
+ appRequestIndexes: {
185
+ __args: { for: enumAppRequestIndexDestination.REVIEWER_DASHBOARD },
186
+ category: true,
187
+ categoryLabel: true,
188
+ reviewerDashboardPriority: true,
189
+ listable: true,
190
+ values: {
191
+ value: true,
192
+ label: true
193
+ }
194
+ },
195
+ appRequests: {
196
+ __args: { filter, paged },
197
+ id: true,
198
+ phase: true,
199
+ status: true,
200
+ createdAt: true,
201
+ updatedAt: true,
202
+ closedAt: true,
203
+ indexCategories: {
204
+ __args: { for: enumAppRequestIndexDestination.REVIEWER_DASHBOARD },
205
+ categoryLabel: true,
206
+ values: {
207
+ label: true
208
+ }
209
+ },
210
+ applicant: {
211
+ login: true,
212
+ fullname: true,
213
+ otherIdentifiers: {
214
+ id: true
215
+ }
216
+ },
217
+ period: {
218
+ name: true
219
+ },
220
+ applications: {
221
+ id: true,
222
+ title: true,
223
+ programKey: true,
224
+ status: true,
225
+ rescindedStatus: true
226
+ },
227
+ actions: {
228
+ review: true
229
+ }
230
+ },
231
+ pageInfo: {
232
+ appRequests: {
233
+ totalItems: true
234
+ }
235
+ }
236
+ })
237
+
238
+ return response
239
+ }
240
+
241
+ /** First-time vs returning applicant counts (an applicant's first request in a period is first-time). */
242
+ async getAppRequestApplicantCounts (filter: AppRequestFilter = {}) {
243
+ const response = await this.client.query({
244
+ __name: 'GetAppRequestApplicantCounts',
245
+ countAppRequestApplicants: {
246
+ __args: { filter },
247
+ firstTime: true,
248
+ returning: true
249
+ }
250
+ })
251
+ return response.countAppRequestApplicants
252
+ }
253
+
254
+ /** Average seconds from submission to decision across applications, from the Metrics API. Requires access.viewMetrics. */
255
+ async getReviewDecisionTiming () {
256
+ const response = await this.client.query({
257
+ __name: 'GetReviewDecisionTiming',
258
+ applicationMetrics: {
259
+ toDecision: {
260
+ avg: true
261
+ }
262
+ }
263
+ })
264
+ return response.applicationMetrics.toDecision.avg ?? null
265
+ }
266
+
267
+ async getPrograms () {
268
+ const response = await this.client.query({
269
+ __name: 'GetPrograms',
270
+ programs: {
271
+ key: true,
272
+ title: true
273
+ }
274
+ })
275
+ return response.programs
276
+ }
277
+
278
+ async getApplicationCount (filter: AppRequestFilter = {}) {
279
+ const response = await this.client.query({
280
+ __name: 'GetApplicationCount',
281
+ countAppRequests: {
282
+ __args: { filter }
283
+ }
284
+ })
285
+ return response.countAppRequests
286
+ }
287
+
288
+ async getApplicantPrompt (appRequestId: string, promptId: string) {
289
+ const response = await this.client.query({
290
+ __name: 'GetApplicantPrompt',
291
+ appRequests: {
292
+ __args: { filter: { ids: [appRequestId] } },
293
+ data: true,
294
+ dataVersion: true,
295
+ prompt: {
296
+ __args: { promptId },
297
+ id: true,
298
+ key: true,
299
+ title: true,
300
+ description: true,
301
+ answered: true,
302
+ visibility: true,
303
+ hasSavedData: true,
304
+ preloadData: true,
305
+ prestageData: true,
306
+ fetchedData: true,
307
+ configurationData: true,
308
+ gatheredConfigData: true,
309
+ invalidated: true,
310
+ invalidatedReason: true
311
+ }
312
+ }
313
+ })
314
+ return { appRequest: response.appRequests[0], prompt: response.appRequests[0].prompt }
315
+ }
316
+
317
+ async stagePrompt (promptId: string, dataVersion?: number) {
318
+ const response = await this.graphqlWithUploads<{ stagePrompt: MutationResponseFromAPI }>(`
319
+ mutation StagePrompt($promptId: ID!, $dataVersion: Int) {
320
+ stagePrompt(promptId: $promptId, dataVersion: $dataVersion) {
321
+ success
322
+ messages {
323
+ message
324
+ type
325
+ arg
326
+ }
327
+ }
328
+ }
329
+ `, { promptId, dataVersion })
330
+ return this.mutationForDialog(response.stagePrompt)
331
+ }
332
+
333
+ async updatePrompt (promptId: string, data: any, validateOnly: boolean, dataVersion?: number, overrideInvalidated?: boolean) {
334
+ const response = await this.graphqlWithUploads<{ updatePrompt: MutationResponseFromAPI }>(`
335
+ mutation UpdatePrompt($promptId: ID!, $data: JsonData!, $validateOnly: Boolean!, $dataVersion: Int, $overrideInvalidated: Boolean) {
336
+ updatePrompt(promptId: $promptId, data: $data, validateOnly: $validateOnly, dataVersion: $dataVersion, overrideInvalidated: $overrideInvalidated) {
337
+ success
338
+ messages {
339
+ message
340
+ type
341
+ arg
342
+ }
343
+ appRequest {
344
+ data
345
+ dataVersion
346
+ }
347
+ }
348
+ }
349
+ `, { promptId, data, validateOnly, dataVersion, overrideInvalidated })
350
+ return { ...this.mutationForDialog(response.updatePrompt), data: response.updatePrompt.appRequest.data, dataVersion: response.updatePrompt.appRequest.dataVersion as number | undefined }
351
+ }
352
+
353
+ async updateConfiguration (periodId: string, definitionKey: string, data: any, validateOnly: boolean) {
354
+ const response = await this.client.mutation({
355
+ __name: 'UpdateConfiguration',
356
+ updateConfiguration: {
357
+ __args: { periodId, key: definitionKey, data, validateOnly },
358
+ success: true,
359
+ messages: {
360
+ message: true,
361
+ type: true,
362
+ arg: true
363
+ }
364
+ }
365
+ })
366
+ return this.mutationForDialog(response.updateConfiguration)
367
+ }
368
+
369
+ async getAppRequestData (appRequestId: string) {
370
+ const response = await this.client.query({
371
+ __name: 'GetAppRequestData',
372
+ appRequests: {
373
+ __args: { filter: { ids: [appRequestId] } },
374
+ id: true,
375
+ data: true,
376
+ applications: {
377
+ requirements: {
378
+ prompts: {
379
+ key: true,
380
+ gatheredConfigData: true
381
+ }
382
+ }
383
+ }
384
+ }
385
+ })
386
+ if (response.appRequests.length === 0) return { data: {}, applications: [] as typeof response.appRequests[0]['applications'] }
387
+ const appRequest = response.appRequests[0]
388
+ return appRequest
389
+ }
390
+
391
+ static splitPromptsForApplicant<P extends { id: string, key: string, visibility: PromptVisibility, moot: boolean }, R extends { type: RequirementType, status: RequirementStatus, statusReason: string | null, blame?: string[] | null, prompts: P[] }, A extends { title: string, ineligiblePhase: IneligiblePhases | null, requirements: R[] }>(applications: A[]) {
392
+ type ReturnPrompt = P & { statusReasons: StatusReason[] }
393
+ type ReturnRequirement = R & { prompts: ReturnPrompt[] }
394
+ type ReturnApplication = A & { requirements: ReturnRequirement[], completionStatus: CompletionStatus, hasWarning: boolean, warningReasons: string[], ineligibleReasons: string[], metReasons: string[]}
395
+ const prequalPrompts: ReturnPrompt[] = []
396
+ const postqualPrompts: ReturnPrompt[] = []
397
+ const qualPrompts: ReturnPrompt[] = []
398
+ const applicationsReviewWithDupes: ReturnApplication[] = []
399
+ const applicationsReviewNoDupes: ReturnApplication[] = []
400
+ const applicationsForNavWithDupes: ReturnApplication[] = []
401
+ const applicationsForNavNoDupes: ReturnApplication[] = []
402
+ const applicationsAcceptWithDupes: ReturnApplication[] = []
403
+ const applicationsAcceptNoDupes: ReturnApplication[] = []
404
+ const promptsById: Record<string, ReturnPrompt> = {}
405
+ const promptsByKey: Record<string, ReturnPrompt[]> = {}
406
+ // every prompt object we hand out, dupes included, so the status reasons pass below reaches all of them
407
+ const allPromptRefs: ReturnPrompt[] = []
408
+
409
+ const seenForReview = new Set<string>()
410
+ const seenForNav = new Set<string>()
411
+ const seenForAccept = new Set<string>()
412
+
413
+ const requirementTypesForNavigation = new Set<RequirementType>([enumRequirementType.PREQUAL, enumRequirementType.POSTQUAL, enumRequirementType.QUALIFICATION])
414
+ const requirementTypesForReview = new Set<RequirementType>([enumRequirementType.QUALIFICATION, enumRequirementType.PREAPPROVAL, enumRequirementType.APPROVAL, enumRequirementType.WORKFLOW])
415
+ for (const application of applications) {
416
+ const requirementsReviewWithDupes: ReturnRequirement[] = []
417
+ const requirementsReviewNoDupes: ReturnRequirement[] = []
418
+ const requirementsForNavWithDupes: ReturnRequirement[] = []
419
+ const requirementsForNavNoDupes: ReturnRequirement[] = []
420
+ const requirementsAcceptWithDupes: ReturnRequirement[] = []
421
+ const requirementsAcceptNoDupes: ReturnRequirement[] = []
422
+ for (const requirement of application.requirements) {
423
+ const promptsForNavWithDupes: ReturnPrompt[] = []
424
+ const promptsForNavNoDupes: ReturnPrompt[] = []
425
+ const promptsReviewNoDupes: ReturnPrompt[] = []
426
+ const promptsReviewWithDupes: ReturnPrompt[] = []
427
+ const promptsAcceptWithDupes: ReturnPrompt[] = []
428
+ const promptsAcceptNoDupes: ReturnPrompt[] = []
429
+ for (const prompt of requirement.prompts) {
430
+ const retPrompt: ReturnPrompt = { ...prompt, statusReasons: [] }
431
+ const withDupesPrompt = { ...retPrompt }
432
+ allPromptRefs.push(retPrompt, withDupesPrompt)
433
+ promptsById[prompt.id] = retPrompt
434
+ if (prompt.moot || prompt.visibility === enumPromptVisibility.UNREACHABLE) continue
435
+ promptsByKey[prompt.key] ??= []
436
+ promptsByKey[prompt.key].push(retPrompt)
437
+ if (!applicantVisiblePromptVisibilities.has(prompt.visibility)) continue
438
+ promptsReviewWithDupes.push(withDupesPrompt)
439
+ if (requirementTypesForNavigation.has(requirement.type)) promptsForNavWithDupes.push(withDupesPrompt)
440
+ if (requirement.type === enumRequirementType.ACCEPTANCE) promptsAcceptWithDupes.push(withDupesPrompt)
441
+ if (!seenForReview.has(prompt.key)) promptsReviewNoDupes.push(retPrompt)
442
+ seenForReview.add(prompt.key)
443
+ if (requirementTypesForNavigation.has(requirement.type)) {
444
+ if (!seenForNav.has(prompt.key)) promptsForNavNoDupes.push(retPrompt)
445
+ seenForNav.add(prompt.key)
446
+ }
447
+ if (requirement.type === enumRequirementType.ACCEPTANCE) {
448
+ if (!seenForAccept.has(prompt.key)) promptsAcceptNoDupes.push(retPrompt)
449
+ seenForAccept.add(prompt.key)
450
+ }
451
+ }
452
+ if (requirement.type === enumRequirementType.PREQUAL) prequalPrompts.push(...promptsForNavNoDupes)
453
+ else if (requirement.type === enumRequirementType.POSTQUAL) postqualPrompts.push(...promptsForNavNoDupes)
454
+ else if (requirementTypesForReview.has(requirement.type)) {
455
+ requirementsReviewWithDupes.push({ ...requirement, prompts: promptsReviewWithDupes })
456
+ requirementsReviewNoDupes.push({ ...requirement, prompts: promptsReviewNoDupes })
457
+ if (requirement.type === enumRequirementType.QUALIFICATION) {
458
+ qualPrompts.push(...promptsForNavNoDupes)
459
+ requirementsForNavWithDupes.push({ ...requirement, prompts: promptsForNavWithDupes })
460
+ requirementsForNavNoDupes.push({ ...requirement, prompts: promptsForNavNoDupes })
461
+ }
462
+ } else if (requirement.type === enumRequirementType.ACCEPTANCE) {
463
+ requirementsAcceptWithDupes.push({ ...requirement, prompts: promptsAcceptWithDupes })
464
+ requirementsAcceptNoDupes.push({ ...requirement, prompts: promptsAcceptNoDupes })
465
+ }
466
+ }
467
+ const reqsForCompletion = application.requirements.filter(r => r.type !== enumRequirementType.POSTQUAL)
468
+ let completionStatus: CompletionStatus = (application.ineligiblePhase) ? 'INELIGIBLE' : 'ELIGIBLE' // ineligible is always precise state, while eligible could be in eligible or pending state
469
+ let completionStatusForNav: CompletionStatus = (application.ineligiblePhase) ? 'INELIGIBLE' : 'ELIGIBLE' // ineligible is always precise state, while eligible could be in eligible or pending state
470
+ const warningReasons: string[] = []
471
+ const warningReasonsFull: string[] = []
472
+ const ineligibleReasons: string[] = []
473
+ const ineligibleReasonsFull: string[] = []
474
+ const metReasons: string[] = []
475
+ const metReasonsFull: string[] = []
476
+ let hasWarning = false
477
+ let hasWarningForNav = false
478
+ for (const req of reqsForCompletion) {
479
+ const showWarnings = application.ineligiblePhase !== enumIneligiblePhases.PREQUAL || req.type === enumRequirementType.PREQUAL
480
+ // PENDING only demotes ELIGIBLE; INELIGIBLE is precise and PENDING just remains. The two statuses are
481
+ // demoted independently: the first PENDING requirement may be a non-navigation type (e.g. an APPROVAL
482
+ // requirement interleaved between QUALIFICATION ones), and that must not stop a later PENDING
483
+ // QUALIFICATION requirement from marking the nav status PENDING.
484
+ if (req.status === enumRequirementStatus.PENDING) {
485
+ if (completionStatus === 'ELIGIBLE') completionStatus = 'PENDING'
486
+ if (completionStatusForNav === 'ELIGIBLE' && requirementTypesForNavigation.has(req.type)) completionStatusForNav = 'PENDING'
487
+ }
488
+ //WARNING makes no completion status changes, whatever application state remains (pending, inel, eli), just append warning reasons
489
+ else if (req.status === enumRequirementStatus.WARNING) {
490
+ hasWarning = true
491
+ if (requirementTypesForNavigation.has(req.type)) hasWarningForNav = true
492
+ if (req.statusReason && showWarnings) {
493
+ if (requirementTypesForNavigation.has(req.type)) warningReasons.push(req.statusReason)
494
+ warningReasonsFull.push(req.statusReason)
495
+ }
496
+ // DISQUALIFYING, eligibility is already known by app ineligiblePhase, but always log ineligible reasons
497
+ // showWarnings above already limits PREQUAL-dead apps to their PREQUAL requirements' reasons
498
+ } else if (req.status === enumRequirementStatus.DISQUALIFYING) {
499
+ if (req.statusReason && showWarnings) {
500
+ if (requirementTypesForNavigation.has(req.type)) ineligibleReasons.push(req.statusReason)
501
+ ineligibleReasonsFull.push(req.statusReason)
502
+ }
503
+ // MET, eligibility is already known by app ineligiblePhase, but always log MET reasons
504
+ } else if (req.status === enumRequirementStatus.MET) {
505
+ if (req.statusReason) {
506
+ if (requirementTypesForNavigation.has(req.type)) metReasons.push(req.statusReason)
507
+ metReasonsFull.push(req.statusReason)
508
+ }
509
+ // NOT APPLICABLE, reference here is for completness, but not implemented as not_applicable currently changes nothing, nor logs reasons
510
+ } //else if (req.status === enumRequirementStatus.NOT_APPLICABLE) {
511
+ //}
512
+ }
513
+ applicationsReviewWithDupes.push({ ...application, requirements: requirementsReviewWithDupes, completionStatus, warningReasons: warningReasonsFull, ineligibleReasons: ineligibleReasonsFull, metReasons: metReasonsFull, hasWarning })
514
+ applicationsReviewNoDupes.push({ ...application, requirements: requirementsReviewNoDupes, completionStatus, warningReasons: warningReasonsFull, ineligibleReasons: ineligibleReasonsFull, metReasons: metReasonsFull, hasWarning })
515
+ applicationsForNavWithDupes.push({ ...application, requirements: requirementsForNavWithDupes, completionStatus: completionStatusForNav, warningReasons, ineligibleReasons, metReasons: metReasonsFull, hasWarning: hasWarningForNav })
516
+ applicationsForNavNoDupes.push({ ...application, requirements: requirementsForNavNoDupes, completionStatus: completionStatusForNav, warningReasons, ineligibleReasons, metReasons: metReasonsFull, hasWarning: hasWarningForNav })
517
+ applicationsAcceptWithDupes.push({ ...application, requirements: requirementsAcceptWithDupes, completionStatus, warningReasons: warningReasonsFull, ineligibleReasons: ineligibleReasonsFull, metReasons: metReasonsFull, hasWarning })
518
+ applicationsAcceptNoDupes.push({ ...application, requirements: requirementsAcceptNoDupes, completionStatus, warningReasons: warningReasonsFull, ineligibleReasons: ineligibleReasonsFull, metReasons: metReasonsFull, hasWarning })
519
+ }
520
+ const reasonsByKey: Record<string, StatusReason[]> = {}
521
+ for (const application of applications) {
522
+ for (const requirement of application.requirements) {
523
+ const blamed = !!requirement.blame?.length
524
+ const targets = blamed
525
+ ? requirement.blame!
526
+ : requirement.prompts.filter(p => p.visibility !== enumPromptVisibility.UNREACHABLE).map(p => p.key)
527
+ for (const key of targets) {
528
+ (reasonsByKey[key] ??= []).push({ ...pick(requirement, 'status', 'statusReason'), programName: application.title, blamed })
529
+ }
530
+ }
531
+ }
532
+ for (const prompt of allPromptRefs) prompt.statusReasons = reasonsByKey[prompt.key] ?? []
533
+ return { prequalPrompts, postqualPrompts, qualPrompts, applicationsReviewWithDupes, applicationsReviewNoDupes, applicationsForNavWithDupes, applicationsForNavNoDupes, applicationsAcceptWithDupes, applicationsAcceptNoDupes, promptsByKey, promptsById }
534
+ }
535
+
536
+ async getAppRequestForExport (appRequestId: string) {
537
+ const response = await this.client.query({
538
+ __name: 'GetAppRequestForExport',
539
+ appRequests: {
540
+ __args: { filter: { ids: [appRequestId] } },
541
+ id: true,
542
+ status: true,
543
+ phase: true,
544
+ closedAt: true,
545
+ data: true,
546
+ dataVersion: true,
547
+ period: {
548
+ name: true
549
+ },
550
+ applications: {
551
+ id: true,
552
+ status: true,
553
+ rescindedStatus: true,
554
+ ineligiblePhase: true,
555
+ hiddenIneligiblePreSubmit: true,
556
+ statusReason: true,
557
+ title: true,
558
+ navTitle: true,
559
+ applicantDescription: true,
560
+ eligibilityDescription: true,
561
+ requirements: {
562
+ id: true,
563
+ type: true,
564
+ status: true,
565
+ statusReason: true,
566
+ blame: true,
567
+ prompts: {
568
+ id: true,
569
+ key: true,
570
+ title: true,
571
+ navTitle: true,
572
+ answered: true,
573
+ visibility: true,
574
+ moot: true,
575
+ invalidated: true,
576
+ invalidatedReason: true,
577
+ optOut: true
578
+ }
579
+ }
580
+ },
581
+ actions: {
582
+ viewApplyUI: true,
583
+ viewAcceptUI: true
584
+ }
585
+ }
586
+ })
587
+ if (response.appRequests.length === 0) throw error(404, 'Application request not found')
588
+ type ResponseAppRequest = (typeof response)['appRequests'][0]
589
+ type ResponseApplication = ResponseAppRequest['applications'][0] & { requirements: ResponseRequirement[] }
590
+ type ResponseRequirement = ResponseAppRequest['applications'][0]['requirements'][0] & { prompts: ResponsePrompt[] }
591
+ type ResponsePrompt = ResponseAppRequest['applications'][0]['requirements'][0]['prompts'][0] & PromptConfig
592
+
593
+ // configuration is per prompt key, not per prompt instance, so it is fetched once per key rather than
594
+ // repeated under every program and requirement that shares the prompt
595
+ const applications = (response.appRequests[0]?.applications ?? []) as ResponseApplication[]
596
+ await this.attachPromptConfigs(appRequestId, applications)
597
+
598
+ const splitInfo = API.splitPromptsForApplicant<ResponsePrompt, ResponseRequirement, ResponseApplication>(applications)
599
+ return {
600
+ ...omit(splitInfo, 'applicationsForNavNoDupes', 'applicationsForNavWithDupes', 'applicationsReviewNoDupes', 'applicationsReviewWithDupes'),
601
+ applicationsForNav: showDupePrompts ? splitInfo.applicationsForNavWithDupes : splitInfo.applicationsForNavNoDupes,
602
+ applicationsReview: showDupePrompts ? splitInfo.applicationsReviewWithDupes : splitInfo.applicationsReviewNoDupes,
603
+ applicationsAccept: showDupePrompts ? splitInfo.applicationsAcceptWithDupes : splitInfo.applicationsAcceptNoDupes,
604
+ appRequest: response.appRequests[0]
605
+ }
606
+ }
607
+
608
+ async createAppRequest (periodId?: string, login?: string, validateOnly?: boolean) {
609
+ const messages: Feedback[] = []
610
+ if (!periodId) messages.push({ message: 'You must select a period to create an application.', type: 'error', path: 'periodId' })
611
+ if (!login) messages.push({ message: 'You must provide a login to create an application.', type: 'error', path: 'login' })
612
+ if (messages.length > 0) return { success: false, messages, data: { periodId, login }, id: undefined }
613
+
614
+ const response = await this.client.mutation({
615
+ __name: 'CreateAppRequest',
616
+ createAppRequest: {
617
+ __args: { periodId: periodId!, login: login!, validateOnly },
618
+ success: true,
619
+ messages: {
620
+ message: true,
621
+ type: true,
622
+ arg: true
623
+ },
624
+ appRequest: {
625
+ id: true
626
+ }
627
+ }
628
+ })
629
+ return { ...this.mutationForDialog(response.createAppRequest), id: response.createAppRequest.appRequest?.id }
630
+ }
631
+
632
+ async appRequestPhaseChange (appRequestId: string, phaseChange: PhaseChangeMutations) {
633
+ const response = await this.graphql<Record<string, { success: boolean, messages: { message: string, type: 'error' | 'warning' | 'success', arg?: string }[] }>>(`
634
+ mutation AppRequestPhaseChange($appRequestId: ID!) {
635
+ ${phaseChange}(appRequestId: $appRequestId) {
636
+ success
637
+ messages {
638
+ message
639
+ type
640
+ arg
641
+ }
642
+ }
643
+ }
644
+ `, { appRequestId })
645
+ return this.mutationForDialog(response[phaseChange])
646
+ }
647
+
648
+ async cancelAppRequest (appRequestId: string, dataVersion?: number) {
649
+ const response = await this.client.mutation({
650
+ __name: 'CancelAppRequest',
651
+ cancelAppRequest: {
652
+ __args: { appRequestId, dataVersion },
653
+ success: true,
654
+ messages: {
655
+ message: true,
656
+ type: true,
657
+ arg: true
658
+ }
659
+ }
660
+ })
661
+ return this.mutationForDialog(response.cancelAppRequest)
662
+ }
663
+
664
+ async reopenAppRequest (appRequestId: string) {
665
+ const response = await this.client.mutation({
666
+ __name: 'ReopenAppRequest',
667
+ reopenAppRequest: {
668
+ __args: { appRequestId },
669
+ success: true,
670
+ messages: {
671
+ message: true,
672
+ type: true,
673
+ arg: true
674
+ }
675
+ }
676
+ })
677
+ return this.mutationForDialog(response.reopenAppRequest)
678
+ }
679
+
680
+ async advanceWorkflow (applicationId: string) {
681
+ const response = await this.client.mutation({
682
+ __name: 'AdvanceWorkflow',
683
+ advanceWorkflow: {
684
+ __args: { applicationId },
685
+ success: true,
686
+ messages: {
687
+ message: true,
688
+ type: true,
689
+ arg: true
690
+ }
691
+ }
692
+ })
693
+ return this.mutationForDialog(response.advanceWorkflow)
694
+ }
695
+
696
+ async reverseWorkflow (applicationId: string) {
697
+ const response = await this.client.mutation({
698
+ __name: 'ReverseWorkflow',
699
+ reverseWorkflow: {
700
+ __args: { applicationId },
701
+ success: true,
702
+ messages: {
703
+ message: true,
704
+ type: true,
705
+ arg: true
706
+ }
707
+ }
708
+ })
709
+ return this.mutationForDialog(response.reverseWorkflow)
710
+ }
711
+
712
+ async rescind (applicationId: string, reason: string, validateOnly?: boolean) {
713
+ const response = await this.client.mutation({
714
+ __name: 'Rescind',
715
+ rescind: {
716
+ __args: { applicationId, reason, validateOnly },
717
+ success: true,
718
+ messages: {
719
+ message: true,
720
+ type: true,
721
+ arg: true
722
+ }
723
+ }
724
+ })
725
+ return this.mutationForDialog(response.rescind)
726
+ }
727
+
728
+ async restore (applicationId: string, reason: string, validateOnly?: boolean) {
729
+ const response = await this.client.mutation({
730
+ __name: 'Restore',
731
+ restore: {
732
+ __args: { applicationId, reason, validateOnly },
733
+ success: true,
734
+ messages: {
735
+ message: true,
736
+ type: true,
737
+ arg: true
738
+ }
739
+ }
740
+ })
741
+ return this.mutationForDialog(response.restore)
742
+ }
743
+
744
+ async addNote (appRequestId: string, content: string, persistent?: boolean, validateOnly?: boolean) {
745
+ const response = await this.client.mutation({
746
+ __name: 'AddNote',
747
+ addNote: {
748
+ __args: { appRequestId, content, persistent, validateOnly },
749
+ success: true,
750
+ messages: {
751
+ message: true,
752
+ type: true,
753
+ arg: true
754
+ }
755
+ }
756
+ })
757
+ return this.mutationForDialog(response.addNote, { dataName: 'note' })
758
+ }
759
+
760
+ async updateNote (noteId: string, content: string) {
761
+ const response = await this.client.mutation({
762
+ __name: 'UpdateNote',
763
+ updateNote: {
764
+ __args: { noteId, content },
765
+ success: true,
766
+ messages: {
767
+ message: true,
768
+ type: true,
769
+ arg: true
770
+ }
771
+ }
772
+ })
773
+ return this.mutationForDialog(response.updateNote, { dataName: 'note' })
774
+ }
775
+
776
+ async togglePersistence (noteId: string) {
777
+ const response = await this.client.mutation({
778
+ __name: 'TogglePersistence',
779
+ togglePersistence: {
780
+ __args: { noteId },
781
+ success: true,
782
+ messages: {
783
+ message: true,
784
+ type: true,
785
+ arg: true
786
+ }
787
+ }
788
+ })
789
+ return this.mutationForDialog(response.togglePersistence, { dataName: 'note' })
790
+ }
791
+
792
+ async deleteNote (noteId: string) {
793
+ const response = await this.client.mutation({
794
+ __name: 'DeleteNote',
795
+ deleteNote: {
796
+ __args: { noteId }
797
+ }
798
+ })
799
+ return response.deleteNote
800
+ }
801
+
802
+ async closeAppRequest (appRequestId: string) {
803
+ const response = await this.client.mutation({
804
+ __name: 'CloseAppRequest',
805
+ closeAppRequest: {
806
+ __args: { appRequestId },
807
+ success: true,
808
+ messages: {
809
+ message: true,
810
+ type: true,
811
+ arg: true
812
+ }
813
+ }
814
+ })
815
+ return this.mutationForDialog(response.closeAppRequest)
816
+ }
817
+
818
+ async getAppRequests (
819
+ filter?: Omit<AppRequestFilter, 'indexes'> & { indexes?: Record<string, string[]> },
820
+ paged?: Pagination,
821
+ dest = enumAppRequestIndexDestination.APP_REQUEST_LIST) {
822
+ const processedFilter = {
823
+ ...filter,
824
+ indexes: filter?.indexes ? Object.entries(filter.indexes).map(([category, tags]) => ({ category, tags })) : undefined
825
+ }
826
+ const response = await this.client.query({
827
+ __name: 'GetAppRequests',
828
+ appRequests: {
829
+ __args: { filter: processedFilter, paged },
830
+ id: true,
831
+ phase: true,
832
+ createdAt: true,
833
+ closedAt: true,
834
+ updatedAt: true,
835
+ applications: {
836
+ title: true,
837
+ programKey: true,
838
+ status: true,
839
+ rescindedStatus: true
840
+ },
841
+ applicant: {
842
+ login: true,
843
+ fullname: true
844
+ },
845
+ status: true,
846
+ statusReason: true,
847
+ period: {
848
+ id: true,
849
+ name: true
850
+ },
851
+ indexCategories: {
852
+ __args: { for: dest },
853
+ category: true,
854
+ categoryLabel: true,
855
+ values: {
856
+ value: true,
857
+ label: true
858
+ }
859
+ },
860
+ actions: {
861
+ review: true
862
+ }
863
+ },
864
+ appRequestIndexes: {
865
+ category: true,
866
+ categoryLabel: true,
867
+ appRequestListPriority: true,
868
+ listFiltersPriority: true,
869
+ listable: true,
870
+ values: {
871
+ __args: { inUse: true },
872
+ value: true,
873
+ label: true
874
+ }
875
+ },
876
+ pageInfo: {
877
+ appRequests: {
878
+ currentPage: true,
879
+ totalItems: true,
880
+ hasNextPage: true,
881
+ perPage: true,
882
+ categories: {
883
+ tags: {
884
+ tag: true,
885
+ label: true
886
+ },
887
+ category: true,
888
+ label: true,
889
+ useInFilters: true,
890
+ useInList: true
891
+ }
892
+ }
893
+ }
894
+
895
+ })
896
+ return response
897
+ }
898
+
899
+ async searchIndexItems (category: string, search: string) {
900
+ const response = await this.client.query({
901
+ __name: 'SearchIndexItems',
902
+ appRequestIndexes: {
903
+ __args: { categories: [category] },
904
+ values: {
905
+ __args: { search },
906
+ value: true,
907
+ label: true
908
+ }
909
+ }
910
+ })
911
+ return response.appRequestIndexes[0]?.values ?? []
912
+ }
913
+
914
+ async getBasicRequestData (appRequestId: string) {
915
+ const response = await this.client.query({
916
+ __name: 'GetBasicRequestData',
917
+ appRequests: {
918
+ __args: { filter: { ids: [appRequestId] } },
919
+ status: true,
920
+ phase: true,
921
+ closedAt: true,
922
+ applicant: {
923
+ login: true,
924
+ fullname: true,
925
+ otherIdentifiers: {
926
+ id: true,
927
+ label: true
928
+ },
929
+ otherInfo: true
930
+ },
931
+ period: {
932
+ id: true,
933
+ name: true,
934
+ code: true,
935
+ openDate: true,
936
+ closeDate: true,
937
+ archiveDate: true
938
+ },
939
+ applications: {
940
+ id: true,
941
+ navTitle: true,
942
+ programKey: true,
943
+ status: true,
944
+ ineligiblePhase: true
945
+ },
946
+ actions: {
947
+ acceptOffer: true,
948
+ cancel: true,
949
+ close: true,
950
+ completeRequest: true,
951
+ completeReview: true,
952
+ reopen: true,
953
+ returnToApplicant: true,
954
+ returnToNonBlocking: true,
955
+ returnToOffer: true,
956
+ returnToReview: true,
957
+ review: true,
958
+ submit: true
959
+ }
960
+ }
961
+ })
962
+ if (response.appRequests.length === 0) return undefined
963
+ return response.appRequests[0]
964
+ }
965
+
966
+ async getReviewData (appRequestId: string, programKey?: string) {
967
+ const response = await this.client.query({
968
+ __name: 'GetReviewData',
969
+ appRequests: {
970
+ __args: { filter: { ids: [appRequestId] } },
971
+ id: true,
972
+ dataVersion: true,
973
+ status: true,
974
+ phase: true,
975
+ createdAt: true,
976
+ submittedAt: true,
977
+ closedAt: true,
978
+ data: true,
979
+ applications: {
980
+ ...(programKey ? { __args: { programKeys: [programKey] } } : {}),
981
+ id: true,
982
+ phase: true,
983
+ status: true,
984
+ statusReason: true,
985
+ title: true,
986
+ navTitle: true,
987
+ programKey: true,
988
+ rescindedStatus: true,
989
+ workflowStage: {
990
+ key: true,
991
+ blocking: true
992
+ },
993
+ nextWorkflowStage: {
994
+ key: true,
995
+ title: true
996
+ },
997
+ previousWorkflowStage: {
998
+ key: true,
999
+ title: true
1000
+ },
1001
+ actions: {
1002
+ advanceWorkflow: true,
1003
+ reverseWorkflow: true,
1004
+ rescindApplication: true,
1005
+ restoreApplication: true
1006
+ },
1007
+ requirements: {
1008
+ id: true,
1009
+ key: true,
1010
+ type: true,
1011
+ title: true,
1012
+ status: true,
1013
+ statusReason: true,
1014
+ blame: true,
1015
+ workflowStage: {
1016
+ key: true,
1017
+ title: true,
1018
+ blocking: true
1019
+ },
1020
+ prompts: {
1021
+ id: true,
1022
+ key: true,
1023
+ title: true,
1024
+ navTitle: true,
1025
+ answered: true,
1026
+ visibility: true,
1027
+ moot: true,
1028
+ invalidated: true,
1029
+ invalidatedReason: true,
1030
+ optOut: true,
1031
+ noDisplay: true,
1032
+ actions: {
1033
+ update: true
1034
+ }
1035
+ }
1036
+ }
1037
+ },
1038
+ notes: {
1039
+ id: true,
1040
+ content: true,
1041
+ persistent: true,
1042
+ createdAt: true,
1043
+ author: {
1044
+ login: true,
1045
+ fullname: true
1046
+ }
1047
+ },
1048
+ actions: {
1049
+ acceptOffer: true,
1050
+ cancel: true,
1051
+ close: true,
1052
+ completeRequest: true,
1053
+ completeReview: true,
1054
+ reopen: true,
1055
+ returnToApplicant: true,
1056
+ returnToNonBlocking: true,
1057
+ returnToOffer: true,
1058
+ returnToReview: true,
1059
+ review: true,
1060
+ submit: true,
1061
+ createNote: true,
1062
+ createPersistentNote: true
1063
+ }
1064
+ }
1065
+ })
1066
+ if (response.appRequests.length === 0) return undefined
1067
+ const appRequest = response.appRequests[0]
1068
+ type ResponsePrompt = (typeof appRequest)['applications'][0]['requirements'][0]['prompts'][0] & PromptConfig
1069
+ type ResponseRequirement = Omit<(typeof appRequest)['applications'][0]['requirements'][0], 'prompts'> & { prompts: ResponsePrompt[] }
1070
+ type ResponseApplication = Omit<(typeof appRequest)['applications'][0], 'requirements'> & { requirements: ResponseRequirement[] }
1071
+ const applications = appRequest.applications as ResponseApplication[]
1072
+ await this.attachPromptConfigs(appRequestId, applications)
1073
+ return { ...appRequest, applications }
1074
+ }
1075
+
1076
+ async getRequestActivity (appRequestId: string, filters?: AppRequestActivityFilters, paged?: Pagination) {
1077
+ const response = await this.client.query({
1078
+ __name: 'GetRequestActivity',
1079
+ appRequestActivity: {
1080
+ __args: { id: appRequestId, filters, paged },
1081
+ id: true,
1082
+ user: {
1083
+ login: true,
1084
+ fullname: true
1085
+ },
1086
+ impersonatedBy: {
1087
+ login: true,
1088
+ fullname: true
1089
+ },
1090
+ action: true,
1091
+ description: true,
1092
+ data: true,
1093
+ createdAt: true
1094
+ },
1095
+ appRequestActivityActions: {
1096
+ __args: { id: appRequestId }
1097
+ },
1098
+ // Includes admins who only appear as impersonators
1099
+ appRequestActivityUsers: {
1100
+ __args: { id: appRequestId },
1101
+ login: true,
1102
+ fullname: true
1103
+ },
1104
+ pageInfo: {
1105
+ appRequestsActivity: {
1106
+ currentPage: true,
1107
+ totalItems: true,
1108
+ hasNextPage: true,
1109
+ perPage: true
1110
+ }
1111
+ }
1112
+ })
1113
+ return {
1114
+ activity: response.appRequestActivity,
1115
+ actions: response.appRequestActivityActions,
1116
+ people: response.appRequestActivityUsers,
1117
+ pageInfo: response.pageInfo.appRequestsActivity
1118
+ }
1119
+ }
1120
+
1121
+ async getPromptData (appRequestId: string, promptId: string) {
1122
+ const response = await this.client.query({
1123
+ __name: 'GetPromptData',
1124
+ appRequests: {
1125
+ __args: { filter: { ids: [appRequestId] } },
1126
+ dataVersion: true,
1127
+ prompt: {
1128
+ __args: { promptId },
1129
+ data: true,
1130
+ preloadData: true,
1131
+ prestageData: true,
1132
+ configurationData: true,
1133
+ gatheredConfigData: true,
1134
+ fetchedData: true
1135
+ }
1136
+ }
1137
+ })
1138
+ const appRequest = response.appRequests[0]
1139
+ return { ...appRequest.prompt, dataVersion: appRequest.dataVersion }
1140
+ }
1141
+
1142
+ /**
1143
+ * What the reviewer sidebar's "Ineligibility Information" dialog shows: every application's outcome plus
1144
+ * the statuses behind it. Loaded when the dialog opens, since the review page itself only carries the
1145
+ * displayed program.
1146
+ */
1147
+ async getIneligibilityInfo (appRequestId: string) {
1148
+ const response = await this.client.query({
1149
+ __name: 'GetIneligibilityInfo',
1150
+ appRequests: {
1151
+ __args: { filter: { ids: [appRequestId] } },
1152
+ id: true,
1153
+ phase: true,
1154
+ closedAt: true,
1155
+ applications: {
1156
+ id: true,
1157
+ title: true,
1158
+ status: true,
1159
+ statusReason: true,
1160
+ rescindedStatus: true,
1161
+ requirements: {
1162
+ id: true,
1163
+ status: true,
1164
+ statusReason: true,
1165
+ prompts: {
1166
+ optOut: true
1167
+ }
1168
+ }
1169
+ }
1170
+ }
1171
+ })
1172
+ return response.appRequests[0]
1173
+ }
1174
+
1175
+ async getPromptConfigs (appRequestId: string, promptIds: string[]) {
1176
+ if (!promptIds.length) return []
1177
+ const response = await this.client.query({
1178
+ __name: 'GetPromptConfigs',
1179
+ appRequests: {
1180
+ __args: { filter: { ids: [appRequestId] } },
1181
+ applications: {
1182
+ requirements: {
1183
+ prompts: {
1184
+ __args: { filter: { ids: promptIds } },
1185
+ id: true,
1186
+ key: true,
1187
+ configurationData: true,
1188
+ gatheredConfigData: true
1189
+ }
1190
+ }
1191
+ }
1192
+ }
1193
+ })
1194
+ return response.appRequests[0]?.applications.flatMap(a => a.requirements.flatMap(r => r.prompts)) ?? []
1195
+ }
1196
+
1197
+ private async attachPromptConfigs<P extends { id: string, key: string, visibility: PromptVisibility, moot: boolean | null }> (appRequestId: string, applications: { requirements: { prompts: P[] }[] }[]) {
1198
+ const representativeByKey: Record<string, string> = {}
1199
+ const instancesByKey: Record<string, P[]> = {}
1200
+ for (const application of applications) {
1201
+ for (const requirement of application.requirements) {
1202
+ for (const prompt of requirement.prompts) {
1203
+ (instancesByKey[prompt.key] ??= []).push(prompt)
1204
+ if (representativeByKey[prompt.key] == null && !prompt.moot && prompt.visibility !== enumPromptVisibility.UNREACHABLE) representativeByKey[prompt.key] = prompt.id
1205
+ }
1206
+ }
1207
+ }
1208
+ const configs = await this.getPromptConfigs(appRequestId, Object.values(representativeByKey))
1209
+ const configByKey = keyby(configs, 'key')
1210
+ for (const [key, instances] of Object.entries(instancesByKey)) {
1211
+ const config = configByKey[key]
1212
+ for (const prompt of instances) {
1213
+ (prompt as P & PromptConfig).configurationData = config?.configurationData ?? {}
1214
+ ;(prompt as P & PromptConfig).gatheredConfigData = config?.gatheredConfigData ?? {}
1215
+ }
1216
+ }
1217
+ }
1218
+
1219
+ async getPromptDataLegion (appRequestId: string, promptIds: string[]) {
1220
+ const response = await this.client.query({
1221
+ __name: 'GetPromptDataLegion',
1222
+ appRequests: {
1223
+ __args: { filter: { ids: [appRequestId] } },
1224
+ applications: {
1225
+ requirements: {
1226
+ prompts: {
1227
+ __args: { filter: { ids: promptIds } },
1228
+ id: true,
1229
+ data: true,
1230
+ preloadData: true,
1231
+ prestageData: true,
1232
+ configurationData: true,
1233
+ gatheredConfigData: true,
1234
+ fetchedData: true
1235
+ }
1236
+ }
1237
+ }
1238
+ }
1239
+ })
1240
+ const appRequest = response.appRequests[0]
1241
+ return appRequest.applications.flatMap(req => req.requirements.flatMap(r => r.prompts))
1242
+ }
1243
+
1244
+ async getPeriodList (filter?: PeriodFilters) {
1245
+ const response = await this.client.query({
1246
+ __name: 'GetPeriodList',
1247
+ periods: {
1248
+ __args: { filter },
1249
+ id: true,
1250
+ name: true,
1251
+ code: true,
1252
+ openDate: true,
1253
+ closeDate: true,
1254
+ archiveDate: true,
1255
+ reviewed: true,
1256
+ actions: {
1257
+ update: true,
1258
+ delete: true,
1259
+ createAppRequest: true
1260
+ }
1261
+ }
1262
+ })
1263
+ return response.periods
1264
+ }
1265
+
1266
+ async getOpenPeriods (filter?: PeriodFilters) {
1267
+ const response = await this.client.query({
1268
+ __name: 'GetOpenPeriods',
1269
+ periods: {
1270
+ __args: { filter: { ...filter, openNow: true } },
1271
+ id: true,
1272
+ name: true,
1273
+ code: true,
1274
+ openDate: true,
1275
+ closeDate: true,
1276
+ archiveDate: true,
1277
+ reviewed: true
1278
+ }
1279
+ })
1280
+ return response.periods
1281
+ }
1282
+
1283
+ async createPeriod (period: PeriodUpdate, validateOnly: boolean, copyPeriodId?: string) {
1284
+ const response = await this.client.mutation({
1285
+ __name: 'CreatePeriod',
1286
+ createPeriod: {
1287
+ __args: { period, validateOnly, copyPeriodId },
1288
+ success: true,
1289
+ messages: {
1290
+ message: true,
1291
+ type: true,
1292
+ arg: true
1293
+ }
1294
+ }
1295
+ })
1296
+ return api.mutationForDialog(response.createPeriod, { prefix: 'period' })
1297
+ }
1298
+
1299
+ async updatePeriod (periodId: string, period: PeriodUpdate, validateOnly: boolean) {
1300
+ const response = await this.client.mutation({
1301
+ __name: 'UpdatePeriod',
1302
+ updatePeriod: {
1303
+ __args: { periodId, update: period, validateOnly },
1304
+ success: true,
1305
+ messages: {
1306
+ message: true,
1307
+ type: true,
1308
+ arg: true
1309
+ }
1310
+ }
1311
+ })
1312
+ return this.mutationForDialog(response.updatePeriod, { prefix: 'period' })
1313
+ }
1314
+
1315
+ async markPeriodReviewed (periodId: string, validateOnly: boolean) {
1316
+ const response = await this.client.mutation({
1317
+ __name: 'MarkPeriodReviewed',
1318
+ markPeriodReviewed: {
1319
+ __args: { periodId, validateOnly },
1320
+ success: true,
1321
+ messages: {
1322
+ message: true,
1323
+ type: true,
1324
+ arg: true
1325
+ }
1326
+ }
1327
+ })
1328
+ return this.mutationForDialog(response.markPeriodReviewed, { prefix: 'period' })
1329
+ }
1330
+
1331
+ async deletePeriod (periodId: string) {
1332
+ const response = await this.client.mutation({
1333
+ __name: 'DeletePeriod',
1334
+ deletePeriod: {
1335
+ __args: { periodId },
1336
+ success: true
1337
+ }
1338
+ })
1339
+ return response.deletePeriod.success
1340
+ }
1341
+
1342
+ async disablePeriodProgramRequirements (periodId: string, requirementKey: string, disabled: boolean) {
1343
+ const response = await this.client.mutation({
1344
+ __name: 'updatePeriodProgram',
1345
+ updatePeriodRequirement: {
1346
+ __args: { periodId, requirementKey, disabled },
1347
+ success: true
1348
+ }
1349
+ })
1350
+ return response.updatePeriodRequirement.success
1351
+ }
1352
+
1353
+ async getPeriodConfigurations (periodId: string) {
1354
+ const response = await this.client.query({
1355
+ __name: 'GetPeriodConfigurations',
1356
+ periods: {
1357
+ __args: { filter: { ids: [periodId] } },
1358
+ id: true,
1359
+ name: true,
1360
+ code: true,
1361
+ openDate: true,
1362
+ closeDate: true,
1363
+ archiveDate: true,
1364
+ reviewed: true,
1365
+ programs: {
1366
+ key: true,
1367
+ title: true,
1368
+ navTitle: true,
1369
+ enabled: true,
1370
+ configuration: {
1371
+ data: true,
1372
+ actions: {
1373
+ update: true
1374
+ }
1375
+ },
1376
+ requirements: {
1377
+ key: true,
1378
+ title: true,
1379
+ description: true,
1380
+ enabled: true,
1381
+ type: true,
1382
+ configuration: {
1383
+ data: true,
1384
+ actions: {
1385
+ update: true
1386
+ }
1387
+ },
1388
+ prompts: {
1389
+ key: true,
1390
+ title: true,
1391
+ description: true,
1392
+ configuration: {
1393
+ data: true,
1394
+ actions: {
1395
+ update: true
1396
+ }
1397
+ }
1398
+ }
1399
+ }
1400
+ }
1401
+ }
1402
+ })
1403
+ if (!response.periods.length) return { period: undefined, programs: [] }
1404
+ const period = response.periods[0]
1405
+ return { programs: period.programs, period }
1406
+ }
1407
+
1408
+ async getConfigurationFetched (periodId: string, definitionKey: string) {
1409
+ const response = await this.client.query({
1410
+ __name: 'GetConfigurationFetched',
1411
+ periods: {
1412
+ __args: { filter: { ids: [periodId] } },
1413
+ configurations: {
1414
+ __args: { filter: { keys: [definitionKey] } },
1415
+ fetchedData: true
1416
+ }
1417
+ }
1418
+ })
1419
+ return response.periods[0].configurations[0]?.fetchedData ?? {}
1420
+ }
1421
+
1422
+ async getRoleList () {
1423
+ const response = await this.client.query({
1424
+ __name: 'GetRoleList',
1425
+ roles: {
1426
+ id: true,
1427
+ name: true,
1428
+ description: true,
1429
+ groups: {
1430
+ groupName: true,
1431
+ managers: {
1432
+ fullname: true,
1433
+ email: true
1434
+ },
1435
+ dateAdded: true,
1436
+ dateCreated: true
1437
+ },
1438
+ actions: {
1439
+ update: true,
1440
+ delete: true
1441
+ }
1442
+ }
1443
+ })
1444
+ if (response.roles.length === 0) return undefined
1445
+ const roles = response.roles
1446
+ return roles.map(role => ({ ...role, groups: role.groups.map((group: AccessRoleGroup) => ({
1447
+ ...group,
1448
+ dateAdded: DateTime.fromISO(group.dateAdded),
1449
+ dateCreated: group.dateCreated ? DateTime.fromISO(group.dateCreated) : undefined
1450
+ })) }))
1451
+ }
1452
+
1453
+ async getRoleDetails (roleId: string) {
1454
+ const response = await this.client.query({
1455
+ __name: 'GetRoleDetails',
1456
+ roles: {
1457
+ __args: { filter: { ids: [roleId] } },
1458
+ id: true,
1459
+ name: true,
1460
+ description: true,
1461
+ groups: {
1462
+ groupName: true,
1463
+ managers: {
1464
+ fullname: true,
1465
+ email: true
1466
+ },
1467
+ dateAdded: true,
1468
+ dateCreated: true
1469
+ },
1470
+ grants: {
1471
+ id: true,
1472
+ controlGroup: {
1473
+ name: true,
1474
+ title: true,
1475
+ description: true
1476
+ },
1477
+ allow: true,
1478
+ controls: true,
1479
+ tags: {
1480
+ category: true,
1481
+ categoryLabel: true,
1482
+ tag: true,
1483
+ label: true,
1484
+ description: true
1485
+ },
1486
+ actions: {
1487
+ update: true,
1488
+ delete: true
1489
+ }
1490
+ },
1491
+ actions: {
1492
+ update: true,
1493
+ delete: true
1494
+ }
1495
+ }
1496
+ })
1497
+ if (!response.roles.length) return undefined
1498
+ const role = response.roles[0]
1499
+ return { ...role, groups: role.groups.map((group: AccessRoleGroup) => ({
1500
+ ...group,
1501
+ dateAdded: DateTime.fromISO(group.dateAdded),
1502
+ dateCreated: group.dateCreated ? DateTime.fromISO(group.dateCreated) : undefined
1503
+ })) }
1504
+ }
1505
+
1506
+ async getAuthorizationInfo () {
1507
+ const response = await this.client.query({
1508
+ __name: 'GetAuthorizationInfo',
1509
+ controlGroups: {
1510
+ name: true,
1511
+ title: true,
1512
+ description: true,
1513
+ tags: {
1514
+ category: true,
1515
+ label: true,
1516
+ description: true,
1517
+ listable: true,
1518
+ tags: {
1519
+ value: true,
1520
+ label: true,
1521
+ description: true
1522
+ }
1523
+ },
1524
+ controls: {
1525
+ name: true,
1526
+ description: true
1527
+ }
1528
+ }
1529
+ })
1530
+ return response.controlGroups
1531
+ }
1532
+
1533
+ async upsertRole (roleId: string | undefined, role: AccessRoleInput, validateOnly: boolean, copyRoleId?: string) {
1534
+ if (roleId != null) {
1535
+ const response = await this.client.mutation({
1536
+ __name: 'UpdateRole',
1537
+ roleUpdate: {
1538
+ __args: { roleId, role, validateOnly },
1539
+ success: true,
1540
+ messages: {
1541
+ message: true,
1542
+ type: true,
1543
+ arg: true
1544
+ }
1545
+ }
1546
+ })
1547
+ return this.mutationForDialog(response.roleUpdate)
1548
+ } else {
1549
+ const response = await this.client.mutation({
1550
+ __name: 'CreateRole',
1551
+ roleCreate: {
1552
+ __args: { role, copyRoleId, validateOnly },
1553
+ success: true,
1554
+ messages: {
1555
+ message: true,
1556
+ type: true,
1557
+ arg: true
1558
+ }
1559
+ }
1560
+ })
1561
+ return this.mutationForDialog(response.roleCreate)
1562
+ }
1563
+ }
1564
+
1565
+ async deleteRole (roleId: string) {
1566
+ const response = await this.client.mutation({
1567
+ __name: 'DeleteRole',
1568
+ roleDelete: {
1569
+ __args: { roleId },
1570
+ success: true
1571
+ }
1572
+ })
1573
+ return response.roleDelete.success
1574
+ }
1575
+
1576
+ async updateGrant (grantId: string, grant: AccessRoleGrantUpdate, validateOnly: boolean) {
1577
+ const response = await this.client.mutation({
1578
+ __name: 'UpdateGrant',
1579
+ roleUpdateGrant: {
1580
+ __args: { grantId, grant, validateOnly },
1581
+ success: true,
1582
+ messages: {
1583
+ message: true,
1584
+ type: true,
1585
+ arg: true
1586
+ }
1587
+ }
1588
+ })
1589
+ return this.mutationForDialog(response.roleUpdateGrant)
1590
+ }
1591
+
1592
+ async createGrant (roleId: string, grant: AccessRoleGrantCreate, validateOnly: boolean) {
1593
+ const response = await this.client.mutation({
1594
+ __name: 'CreateGrant',
1595
+ roleAddGrant: {
1596
+ __args: { roleId, grant, validateOnly },
1597
+ success: true,
1598
+ messages: {
1599
+ message: true,
1600
+ type: true,
1601
+ arg: true
1602
+ }
1603
+ }
1604
+ })
1605
+ return this.mutationForDialog(response.roleAddGrant)
1606
+ }
1607
+
1608
+ async deleteGrant (grantId: string) {
1609
+ const response = await this.client.mutation({
1610
+ __name: 'DeleteGrant',
1611
+ roleDeleteGrant: {
1612
+ __args: { grantId },
1613
+ success: true,
1614
+ messages: {
1615
+ message: true,
1616
+ type: true,
1617
+ arg: true
1618
+ }
1619
+ }
1620
+ })
1621
+ return this.mutationForDialog(response.roleDeleteGrant)
1622
+ }
1623
+
1624
+ async getAnnouncement (active?: boolean) {
1625
+ const response = await this.client.query({
1626
+ __name: 'announcement',
1627
+ announcements:{
1628
+ __args: {
1629
+ filter: active == null ? {} : { active }
1630
+ },
1631
+ id: true,
1632
+ isActive: true,
1633
+ subject: true,
1634
+ body: true,
1635
+ start: true,
1636
+ end: true,
1637
+ link: true,
1638
+ linkText: true,
1639
+ enabled: true,
1640
+ type: true
1641
+ }
1642
+ })
1643
+
1644
+ return response.announcements.length ? response.announcements[0] : undefined
1645
+ }
1646
+
1647
+ async createAnnouncement (announcement: any, validateOnly = true) {
1648
+ const response = await this.client.mutation({
1649
+ __name: 'CreateAnnouncement',
1650
+ createAnnouncement: {
1651
+ __args: {
1652
+ announcement,
1653
+ validateOnly
1654
+ },
1655
+ success: true,
1656
+ messages: {
1657
+ message: true,
1658
+ type: true,
1659
+ arg: true
1660
+ }
1661
+ }
1662
+ })
1663
+
1664
+ return this.mutationForDialog(response.createAnnouncement)
1665
+ }
1666
+
1667
+ async updateAnnouncement (announcementId: string, announcement: any, validateOnly = true) {
1668
+ const response = await this.client.mutation({
1669
+ __name: 'UpdateAnnouncement',
1670
+ updateAnnouncement: {
1671
+ __args: {
1672
+ announcementId,
1673
+ announcement,
1674
+ validateOnly
1675
+ },
1676
+ success: true,
1677
+ messages: {
1678
+ message: true,
1679
+ type: true,
1680
+ arg: true
1681
+ }
1682
+ }
1683
+ })
1684
+
1685
+ return this.mutationForDialog(response.updateAnnouncement)
1686
+ }
1687
+
1688
+ async deleteAnnouncement (id: string) {
1689
+ const response = await this.client.mutation({
1690
+ __name: 'CreateAnnouncement',
1691
+ deleteAnnouncement: {
1692
+ __args: {
1693
+ announcementId: id
1694
+ },
1695
+ success: true,
1696
+ messages: {
1697
+ message: true,
1698
+ type: true,
1699
+ arg: true
1700
+ }
1701
+ }
1702
+ })
1703
+ return response.deleteAnnouncement.success
1704
+ }
1705
+ }
1706
+
1707
+ export const api = new API(PUBLIC_API_BASE, PUBLIC_AUTH_REDIRECT)