@reqquest/ui 1.17.0 → 1.19.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 (102) 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 +508 -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 +5 -4
  99. package/dist/registry.d.ts +17 -0
  100. package/dist/registry.js +3 -0
  101. package/package.json +29 -15
  102. package/dist/components/DelayedSkeleton.svelte.d.ts +0 -21
@@ -0,0 +1,181 @@
1
+ <script lang="ts">
2
+ import { ErrorPage, ProgressNav, type RootStepItem, type StepItem } from '@txstate-mws/carbon-svelte'
3
+ import { setContext } from 'svelte'
4
+ import { writable } from 'svelte/store'
5
+ import { resolve } from '$app/paths'
6
+ import { page } from '$app/stores'
7
+ import { applicantVisiblePromptVisibilities, submissionRequirementTypes } from '$internal'
8
+ import { enumRequirementType } from '@reqquest/ui'
9
+ import type { LayoutData } from './$types'
10
+
11
+ export let data: LayoutData
12
+ $: ({ appRequestForExport, prequalPrompts, postqualPrompts, applicationsForNav } = data)
13
+
14
+ function getNavItems (..._: any[]) {
15
+ const navItems: RootStepItem[] = []
16
+ let nextHref: string | undefined
17
+ let prevHref: string | undefined
18
+ let foundCurrent = false
19
+ let lastprompt: string | undefined
20
+ for (const prompt of prequalPrompts) {
21
+ if (foundCurrent) {
22
+ nextHref = resolve(`/requests/${appRequestForExport.id}/apply/${prompt.id}`)
23
+ foundCurrent = false
24
+ } else if ($page.params.promptId === prompt.id) {
25
+ foundCurrent = true
26
+ if (lastprompt) prevHref = resolve(`/requests/${appRequestForExport.id}/apply/${lastprompt}`)
27
+ }
28
+ lastprompt = prompt.id
29
+ navItems.push({
30
+ id: `prompt${prompt.id}`,
31
+ label: prompt.navTitle,
32
+ href: resolve(`/requests/${appRequestForExport.id}/apply/${prompt.id}`),
33
+ type: $page.params.promptId === prompt.id
34
+ ? 'current'
35
+ : prompt.answered && !prompt.invalidated
36
+ ? 'complete'
37
+ : prompt.answered && prompt.invalidated
38
+ ? 'warning'
39
+ : 'available',
40
+ substeps: []
41
+ })
42
+ }
43
+ const outstandingPrequal = prequalPrompts.find(p =>
44
+ (!p.answered || p.invalidated) && applicantVisiblePromptVisibilities.has(p.visibility) && !p.moot)
45
+ if (outstandingPrequal) {
46
+ nextHref = resolve(`/requests/${appRequestForExport.id}/apply/${outstandingPrequal.id}`)
47
+ } else {
48
+ if (foundCurrent) nextHref = resolve(`/requests/${appRequestForExport.id}/apply/programs`)
49
+ foundCurrent = false
50
+ if ($page.route.id === '/requests/[id]/apply/programs') {
51
+ const promptsUnderPrograms = applicationsForNav
52
+ .flatMap(app => app.requirements.filter(r => r.type === enumRequirementType.QUALIFICATION))
53
+ .flatMap(r => r.prompts)
54
+ .filter(r => !r.optOut)
55
+ const qualFinished = promptsUnderPrograms.every(p => p.answered && !p.invalidated)
56
+ const postFinished = postqualPrompts.every(p => p.answered && !p.invalidated)
57
+ if (!qualFinished) {
58
+ const prompt = promptsUnderPrograms.find(p => !p.answered || p.invalidated)
59
+ nextHref = resolve(`/requests/${appRequestForExport.id}/apply/${prompt!.id}`)
60
+ } else if (!postFinished) {
61
+ const prompt = postqualPrompts.find(p => !p.answered || p.invalidated)
62
+ nextHref = resolve(`/requests/${appRequestForExport.id}/apply/${prompt!.id}`)
63
+ } else {
64
+ nextHref = resolve(`/requests/${appRequestForExport.id}/apply/review`)
65
+ }
66
+ if (lastprompt) {
67
+ prevHref = resolve(`/requests/${appRequestForExport.id}/apply/${lastprompt}`)
68
+ }
69
+ }
70
+ const programReview: RootStepItem = {
71
+ id: 'programs',
72
+ label: 'Program review',
73
+ href: resolve(`/requests/${appRequestForExport.id}/apply/programs`),
74
+ type: $page.route.id === '/requests/[id]/apply/programs' ? 'current' : 'available',
75
+ substeps: []
76
+ }
77
+ navItems.push(programReview)
78
+ let pastProgramReview = false
79
+ for (const application of applicationsForNav) {
80
+ const substeps: StepItem[] = []
81
+ lastprompt = undefined
82
+ for (const requirement of application.requirements) {
83
+ if (!submissionRequirementTypes.has(requirement.type)) continue
84
+ for (const prompt of requirement.prompts) {
85
+ if (prompt.optOut) continue
86
+ if (foundCurrent) {
87
+ nextHref = resolve(`/requests/${appRequestForExport.id}/apply/${prompt.id}`)
88
+ foundCurrent = false
89
+ }
90
+ if ($page.params.promptId === prompt.id) {
91
+ pastProgramReview = true
92
+ foundCurrent = true
93
+ if (lastprompt) prevHref = resolve(`/requests/${appRequestForExport.id}/apply/${lastprompt}`)
94
+ else prevHref = resolve(`/requests/${appRequestForExport.id}/apply/programs`)
95
+ }
96
+ lastprompt = prompt.id
97
+ substeps.push({
98
+ id: `prompt${prompt.id}`,
99
+ label: prompt.navTitle,
100
+ href: resolve(`/requests/${appRequestForExport.id}/apply/${prompt.id}`),
101
+ type: $page.params.promptId === prompt.id
102
+ ? 'current'
103
+ : prompt.answered && !prompt.invalidated
104
+ ? 'complete'
105
+ : prompt.answered && prompt.invalidated
106
+ ? 'warning'
107
+ : 'available'
108
+ })
109
+ }
110
+ }
111
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- it can definitely be both true and false, typescript is doing something crazy
112
+ if (foundCurrent) {
113
+ nextHref = resolve(`/requests/${appRequestForExport.id}/apply/programs`)
114
+ foundCurrent = false
115
+ }
116
+ if (substeps.length > 0) {
117
+ navItems.push({
118
+ id: `application${application.id}`,
119
+ label: application.navTitle,
120
+ href: substeps[0].href,
121
+ type: substeps.some(s => s.type === 'warning')
122
+ ? 'warning'
123
+ : substeps.some(s => s.type === 'current')
124
+ ? 'current'
125
+ : substeps.every(s => s.type === 'complete')
126
+ ? 'complete'
127
+ : 'available',
128
+ substeps
129
+ })
130
+ }
131
+ }
132
+ lastprompt = undefined
133
+ for (const prompt of postqualPrompts) {
134
+ if (foundCurrent) {
135
+ nextHref = resolve(`/requests/${appRequestForExport.id}/apply/${prompt.id}`)
136
+ foundCurrent = false
137
+ }
138
+ if ($page.params.promptId === prompt.id) {
139
+ pastProgramReview = true
140
+ foundCurrent = true
141
+ if (lastprompt) prevHref = resolve(`/requests/${appRequestForExport.id}/apply/${lastprompt}`)
142
+ else prevHref = resolve(`/requests/${appRequestForExport.id}/apply/programs`)
143
+ }
144
+ lastprompt = prompt.id
145
+ navItems.push({
146
+ id: `prompt${prompt.id}`,
147
+ label: prompt.navTitle,
148
+ href: resolve(`/requests/${appRequestForExport.id}/apply/${prompt.id}`),
149
+ type: $page.params.promptId === prompt.id
150
+ ? 'current'
151
+ : prompt.answered && !prompt.invalidated ? 'complete' : 'available',
152
+ substeps: []
153
+ })
154
+ }
155
+ if ($page.route.id === '/requests/[id]/apply/review') pastProgramReview = true
156
+ navItems.push({
157
+ id: 'review',
158
+ label: 'Review submission',
159
+ href: resolve(`/requests/${appRequestForExport.id}/apply/review`),
160
+ type: $page.route.id === '/requests/[id]/apply/review' ? 'current' : 'available',
161
+ substeps: []
162
+ })
163
+ if (pastProgramReview) programReview.type = 'complete'
164
+ }
165
+ nextHref ??= resolve(`/requests/${appRequestForExport.id}/apply/review`)
166
+ return { navItems, nextHref, prevHref }
167
+ }
168
+ $: ({ navItems, nextHref, prevHref } = getNavItems(appRequestForExport, prequalPrompts, $page))
169
+ $: notInNavigation = !navItems.some(item => item.type === 'current' || item.substeps.some(s => s.type === 'current'))
170
+ const nextHrefStore = writable({ nextHref, prevHref })
171
+ $: nextHrefStore.set({ nextHref, prevHref })
172
+ setContext('nextHref', nextHrefStore)
173
+ </script>
174
+
175
+ <ProgressNav steps={navItems}>
176
+ {#if notInNavigation}
177
+ <ErrorPage error={{ status: 404, message: 'Not Found' }} />
178
+ {:else}
179
+ <slot />
180
+ {/if}
181
+ </ProgressNav>
@@ -0,0 +1,12 @@
1
+ import { api } from '$internal'
2
+ import { error } from '@sveltejs/kit'
3
+ import type { LayoutLoad } from './$types'
4
+
5
+ export const load: LayoutLoad = async ({ params, depends }) => {
6
+ const { appRequest, ...applicationInfo } = await api.getAppRequestForExport(params.id)
7
+ depends('request:apply')
8
+ // separate depend key so the nav/answered state can be refreshed without re-running the prompt page load, which was remounting the current prompt and could flash a skeleton.
9
+ depends('request:apply:nav')
10
+ if (!appRequest.actions.viewApplyUI) throw error(404, 'This application cannot be edited at this time.')
11
+ return { ...applicationInfo, appRequestForExport: { ...appRequest, applications: undefined } }
12
+ }
@@ -0,0 +1,14 @@
1
+ import { redirect } from '@sveltejs/kit'
2
+ import { resolve } from '$app/paths'
3
+ import { applicantVisiblePromptVisibilities } from '$internal'
4
+ import type { PageLoad } from './$types'
5
+
6
+ export const load: PageLoad = async ({ params, parent }) => {
7
+ const { prequalPrompts } = await parent()
8
+ for (const prompt of prequalPrompts) {
9
+ if ((!prompt.answered || prompt.invalidated) && applicantVisiblePromptVisibilities.has(prompt.visibility) && !prompt.moot) {
10
+ throw redirect(303, resolve(`/requests/${params.id}/apply/${prompt.id}`))
11
+ }
12
+ }
13
+ throw redirect(303, resolve(`/requests/${params.id}/apply/programs`))
14
+ }
@@ -0,0 +1,42 @@
1
+ <script lang="ts">
2
+ export let title: string | undefined = undefined
3
+ export let subtitle: string | undefined = undefined
4
+ </script>
5
+ <div class="progress-nav-outer">
6
+ <div class="progress-nav-inner">
7
+ {#if title}<h2>{title}</h2>{/if}
8
+ {#if subtitle}<div class="progress-nav-subtitle">{subtitle}</div>{/if}
9
+ <slot />
10
+ </div>
11
+ </div>
12
+
13
+ <style>
14
+ .progress-nav-outer {
15
+ container-type: inline-size;
16
+ }
17
+
18
+ .progress-nav-inner {
19
+ width: min(90%, 800px);
20
+ margin: 0 auto;
21
+ }
22
+ @media (max-width: 600px) {
23
+ .progress-nav-inner {
24
+ width: 100%;
25
+ }
26
+ }
27
+
28
+ h2, .progress-nav-subtitle {
29
+ text-align: center;
30
+ }
31
+
32
+ h2 {
33
+ font-weight: bold;
34
+ font-size: 1.5rem;
35
+ margin-bottom: 1.2rem;
36
+ }
37
+
38
+ .progress-nav-subtitle {
39
+ font-size: 0.9rem;
40
+ margin-bottom: 1.2rem;
41
+ }
42
+ </style>
@@ -0,0 +1,23 @@
1
+ <script lang="ts">
2
+ import { ApplicantPromptPage } from '$internal'
3
+ import DelayedSkeleton from '$internal/components/DelayedSkeleton.svelte';
4
+ import { uiRegistry } from '$local';
5
+ import type { PageData } from './$types'
6
+
7
+ export let data: PageData
8
+ $: loader = uiRegistry.getPrompt(data.basicPromptData.key)?.loader
9
+
10
+
11
+ </script>
12
+
13
+ {#await data.applicantPromptPromise}
14
+ <div class="prompt-intro flow max-w-screen-md mx-auto pt-10 px-6">
15
+ <!-- svelte-ignore a11y_autofocus -->
16
+ <h2 id="prompt-title" tabindex="-1" autofocus class="font-bold text-2xl leading-normal text-center">{data.basicPromptData.title}</h2>
17
+ </div>
18
+ {#if loader}
19
+ <DelayedSkeleton {loader} />
20
+ {/if}
21
+ {:then { prompt, appRequest }}
22
+ <ApplicantPromptPage data={{ prompt, appRequest, dataVersion: appRequest.dataVersion }} statusReasons={data.basicPromptData?.statusReasons ?? []} />
23
+ {/await}
@@ -0,0 +1,11 @@
1
+ import { api } from '$internal'
2
+ import { error } from '@sveltejs/kit'
3
+ import type { PageLoad } from './$types'
4
+
5
+ export const load: PageLoad = async ({ url, params, depends, parent }) => {
6
+ const { promptsById } = await parent()
7
+ depends('request:apply')
8
+ const basicPromptData = promptsById[params.promptId]
9
+ const applicantPromptPromise = api.getApplicantPrompt(params.id, params.promptId)
10
+ return { applicantPromptPromise, basicPromptData }
11
+ }
@@ -0,0 +1,71 @@
1
+ <script lang="ts">
2
+ import { Button } from 'carbon-components-svelte'
3
+ import { Panel } from '@txstate-mws/carbon-svelte'
4
+ import { getContext } from 'svelte'
5
+ import type { Writable } from 'svelte/store'
6
+ import { afterNavigate } from '$app/navigation'
7
+ import type { PageData } from './$types'
8
+ import ProgressNavContainer from '../ProgressNavContainer.svelte'
9
+ import { ApplicantProgramList, DEFAULT_APPLICANT_NO_PROGRAMS_MESSAGE, isHiddenFromApplicant, isIneligibleByApplicantRequirement } from '$internal'
10
+ import { uiRegistry } from '$local'
11
+ import { enumApplicationStatus } from '@reqquest/ui'
12
+
13
+ export let data: PageData
14
+ $: ({ applicationsForNav, appRequestForExport } = data)
15
+ $: eligibleApplicationsForNav = applicationsForNav.filter(a => a.ineligiblePhase == null)
16
+ $: ineligibleApplicationsForNav = applicationsForNav.filter(a => isIneligibleByApplicantRequirement(a) && !isHiddenFromApplicant(a))
17
+ $: noProgramsToList = !eligibleApplicationsForNav.length && !ineligibleApplicationsForNav.length && applicationsForNav.some(isHiddenFromApplicant)
18
+
19
+ let prevHref: string | undefined
20
+ let nextHref: string | undefined
21
+ const getNextHref = getContext<Writable<{ nextHref: string | undefined, prevHref: string | undefined }>>('nextHref')
22
+ $: ({ prevHref, nextHref } = $getNextHref)
23
+
24
+ afterNavigate(() => {
25
+ const h2 = document.querySelector('h2')
26
+ if (h2) {
27
+ h2.tabIndex = -1
28
+ h2.focus()
29
+ }
30
+ })
31
+ </script>
32
+
33
+ <ProgressNavContainer title="Your potential benefits" subtitle={noProgramsToList ? (uiRegistry.config.applicantNoProgramsMessage ?? DEFAULT_APPLICANT_NO_PROGRAMS_MESSAGE) : 'Based on your responses so far, you may be eligible for the benefits listed below. If you believe you qualify for additional benefits, you may need to review your answers, complete additional questions, or provide supporting documentation before eligibility can be confirmed.'}>
34
+ <div class="max-w-screen-md mx-auto">
35
+ {#if eligibleApplicationsForNav.length > 0}
36
+ <ApplicantProgramList applications={eligibleApplicationsForNav} appRequest={appRequestForExport} />
37
+ {/if}
38
+ {#if eligibleApplicationsForNav.some(a => a.status === enumApplicationStatus.INELIGIBLE)}
39
+ <div class="program-helptext">
40
+ If you believe you should be eligible, read the tooltips above and review your answers. If you believe there is an error, please contact us.
41
+ </div>
42
+ {/if}
43
+ </div>
44
+ {#if ineligibleApplicationsForNav.length > 0}
45
+ <div class="max-w-screen-md mx-auto">
46
+ <Panel title="Ineligible benefits" expandable={true} expanded={true}>
47
+ <ApplicantProgramList applications={ineligibleApplicationsForNav} appRequest={appRequestForExport} />
48
+ </Panel>
49
+ </div>
50
+ {/if}
51
+ <div class='form-submit flex gap-12 justify-center mt-16'>
52
+ {#if prevHref}
53
+ <Button kind="ghost" href={prevHref}>Back</Button>
54
+ {/if}
55
+ <Button type="submit" href={nextHref}>
56
+ {#if nextHref?.endsWith('/review')}
57
+ Review {uiRegistry.getWord('appRequest')}
58
+ {:else}
59
+ Continue
60
+ {/if}
61
+ </Button>
62
+ </div>
63
+
64
+ </ProgressNavContainer>
65
+
66
+ <style>
67
+ .program-helptext {
68
+ text-align: center;
69
+ font-size: 0.9rem;
70
+ }
71
+ </style>
@@ -0,0 +1,102 @@
1
+ <script lang="ts">
2
+ import { toasts } from '@txstate-mws/svelte-components'
3
+ import { Button, Loading } from 'carbon-components-svelte'
4
+ import Touch_1 from 'carbon-icons-svelte/lib/Touch_1.svelte'
5
+ import { getContext } from 'svelte'
6
+ import type { Writable } from 'svelte/store'
7
+ import { afterNavigate, goto } from '$app/navigation'
8
+ import { resolve } from '$app/paths'
9
+ import type { ResolvedPathname } from '$app/types'
10
+ import { api, ApplicationDetailsView } from '$internal'
11
+ import { enumAppRequestStatus } from '@reqquest/ui'
12
+ import type { PageData } from './$types'
13
+ import { uiRegistry } from '$local'
14
+ import { enumIneligiblePhases } from '@reqquest/ui'
15
+
16
+ /**
17
+ * This page will allow applicants to review all the information they've
18
+ * entered so far, and to navigate to any prompts that need correction, then
19
+ * allows them to submit the app request.
20
+ */
21
+ export let data: PageData
22
+ $: ({ appRequestForExport, prequalPrompts, postqualPrompts, applicationsForNav } = data)
23
+
24
+ const nextHref = getContext<Writable<{ nextHref?: ResolvedPathname, prevHref?: ResolvedPathname }>>('nextHref')
25
+ $: previousHref = nextHref ? $nextHref.prevHref : undefined
26
+
27
+ $: loading = false
28
+
29
+ async function handleBack () {
30
+ if (previousHref) {
31
+ // eslint-disable-next-line svelte/no-navigation-without-resolve -- already resolved
32
+ await goto(previousHref)
33
+ }
34
+ }
35
+
36
+ async function onSubmit () {
37
+ loading = true
38
+ const resp = await api.appRequestPhaseChange(appRequestForExport.id, 'submitAppRequest')
39
+ loading = false
40
+ if (resp.success) {
41
+ toasts.add({ type: 'success', title: 'Submission Succeeded', message: 'Successfully submitted application for review' })
42
+ await goto(resolve('/dashboards/applicant'))
43
+ } else {
44
+ toasts.add({ type: 'error', title: 'Submission Failed', message: resp.messages.map(m => m.message).join('\n') || 'There was an error submitting your application. Please try again later.' })
45
+ }
46
+ }
47
+
48
+ afterNavigate(({ from }) => {
49
+ if (from?.url?.pathname) previousHref = from.url.pathname + from.url.search
50
+ const h2 = document.querySelector('h2')
51
+ if (h2) {
52
+ h2.tabIndex = -1
53
+ h2.focus()
54
+ }
55
+ })
56
+ </script>
57
+ {#if loading}
58
+ <Loading />
59
+ {/if}
60
+ <div class:max-w-screen-md={uiRegistry.config.applicantReviewMaxWidth !== false} class:mx-auto={uiRegistry.config.applicantReviewMaxWidth !== false}>
61
+ <ApplicationDetailsView
62
+ appRequest={appRequestForExport}
63
+ applications={applicationsForNav}
64
+ appData={appRequestForExport.data}
65
+ {prequalPrompts}
66
+ {postqualPrompts}
67
+ {uiRegistry}
68
+ title={uiRegistry.config.applicantReview?.title ?? "Review your application"}
69
+ subtitle={uiRegistry.config.applicantReview?.subTitle ?? "Confirm the benefits shown are the ones you are requesting and that your responses are correct, or make changes before submitting."}
70
+ expandable={false}
71
+ showWarningsInline={true}
72
+ showAppRequestStatus={false}
73
+ statusDisplay="icons"
74
+ >
75
+ <svelte:fragment slot="footer">
76
+ <footer class="[ mt-6 ] flow">
77
+ {#if uiRegistry.config.supportUrl}
78
+ <p class="text-center">If you think this was a mistake please review your answers or reach out through the support page.</p>
79
+ {/if}
80
+ <div class="footer-actions">
81
+ {#if previousHref}
82
+ <Button kind="ghost" on:click={handleBack}>Back</Button>
83
+ {/if}
84
+ {#if uiRegistry.config.supportUrl}
85
+ <Button kind="ghost" href={uiRegistry.config.supportUrl}>Support</Button>
86
+ {/if}
87
+ {#if appRequestForExport.status === enumAppRequestStatus.READY_TO_SUBMIT}
88
+ <Button icon={Touch_1} on:click={onSubmit}>Submit application</Button>
89
+ {/if}
90
+ </div>
91
+ </footer>
92
+ </svelte:fragment>
93
+ </ApplicationDetailsView>
94
+ </div>
95
+ <style>
96
+ .footer-actions {
97
+ display: flex;
98
+ gap: 3rem;
99
+ justify-content: center;
100
+ margin-top: 2rem;
101
+ }
102
+ </style>
@@ -0,0 +1,114 @@
1
+ <script lang="ts">
2
+ import { resolve } from '$app/paths'
3
+ import { page } from '$app/stores'
4
+ import type { LayoutData } from './$types.js'
5
+ import { IntroPanel, TabLinks, applicantStatuses, getReviewerStatusTags, longNumericTime } from '$internal'
6
+ import { uiRegistry } from '$local'
7
+ import { enumAppRequestPhase } from '@reqquest/ui'
8
+ import { isIneligibleByApplicantRequirement } from '$internal'
9
+
10
+ export let data: LayoutData
11
+ $: ({ basicRequestData, requestId } = data)
12
+ $: eligibleApplications = basicRequestData.applications.filter(a => !isIneligibleByApplicantRequirement(a))
13
+ $: ineligibleApplications = basicRequestData.applications.filter(a => isIneligibleByApplicantRequirement(a))
14
+ $: activeIneligible = ineligibleApplications.find(a => resolve(`/requests/${requestId}/approve/${a.programKey}`) === $page.url.pathname)
15
+ $: navTabs = [
16
+ ...(eligibleApplications.map(a => ({
17
+ label: a.navTitle,
18
+ href: resolve(`/requests/${requestId}/approve/${a.programKey}`)
19
+ }))),
20
+ {
21
+ label: 'Activity log',
22
+ href: resolve(`/requests/${requestId}/approve/activity`)
23
+ }
24
+ ]
25
+ $: ineligibleTab = {
26
+ label: activeIneligible?.navTitle ?? 'Ineligible programs',
27
+ disabled: !ineligibleApplications.length,
28
+ selected: activeIneligible != null
29
+ }
30
+ $: loading = false
31
+
32
+ </script>
33
+
34
+ <IntroPanel
35
+ title={basicRequestData.period.name + (basicRequestData.period.code ? ` (${basicRequestData.period.code})` : '')}
36
+ subtitle={`Review and complete the ${uiRegistry.getWord('appRequest').toLowerCase()} below or advance it in the workflow.`}
37
+ tags={getReviewerStatusTags(basicRequestData.status, basicRequestData.phase, basicRequestData.closedAt)}
38
+ >
39
+ <div class="block-end flex items-center" slot="block-end">
40
+ <section class="text-base text-center flex-col gap-2 mr-[12px]">
41
+ <dl class="flex gap-4 text-sm">
42
+ <div class="flex flex-col bg-[var(--cds-ui-03,#d9d9d9)] py-4 px-4 justify-center">
43
+ <dt class="font-bold">
44
+ {#if basicRequestData.closedAt}
45
+ Closed
46
+ {:else if applicantStatuses.has(basicRequestData.status)}
47
+ Submit By
48
+ {:else if basicRequestData.phase !== enumAppRequestPhase.COMPLETE}
49
+ Review By
50
+ {:else}
51
+ Auto-Closes
52
+ {/if}:
53
+ </dt>
54
+ <dd>
55
+ {#if basicRequestData.closedAt}
56
+ <time datetime={basicRequestData.closedAt}>{longNumericTime(basicRequestData.closedAt)}</time>
57
+ {:else if applicantStatuses.has(basicRequestData.status)}
58
+ {#if basicRequestData.period.closeDate}
59
+ <time datetime={basicRequestData.period.closeDate}>{longNumericTime(basicRequestData.period.closeDate)}</time>
60
+ {:else}
61
+ No deadline
62
+ {/if}
63
+ {:else if basicRequestData.phase !== enumAppRequestPhase.COMPLETE}
64
+ {#if basicRequestData.period.archiveDate}
65
+ <time datetime={basicRequestData.period.archiveDate}>{longNumericTime(basicRequestData.period.archiveDate)}</time>
66
+ {:else}
67
+ No deadline
68
+ {/if}
69
+ {:else}
70
+ {#if basicRequestData.period.archiveDate}
71
+ <time datetime={basicRequestData.period.archiveDate}>{longNumericTime(basicRequestData.period.archiveDate)}</time>
72
+ {:else}
73
+ Never
74
+ {/if}
75
+ {/if}
76
+ </dd>
77
+ </div>
78
+ </dl>
79
+ </section>
80
+ </div>
81
+ </IntroPanel>
82
+ {#snippet ineligibleProgramList()}
83
+ <ul class="ineligible-programs">
84
+ {#each ineligibleApplications as a}
85
+ <li>
86
+ <a href={resolve(`/requests/${requestId}/approve/${a.programKey}`)}>{a.navTitle}</a>
87
+ </li>
88
+ {/each}
89
+ </ul>
90
+ {/snippet}
91
+ <TabLinks tabs={[...navTabs, { ...ineligibleTab, panel: ineligibleProgramList }]} />
92
+ <slot />
93
+ <style>
94
+ .block-end :global(.bx--label) {
95
+ position: absolute;
96
+ top: 0;
97
+ left: 0;
98
+ transform: translateY(-100%);
99
+ }
100
+ .ineligible-programs {
101
+ padding: 0.25rem 0;
102
+ margin: 0;
103
+ list-style: none;
104
+ }
105
+ .ineligible-programs a {
106
+ display: block;
107
+ padding: 0.5rem 1rem;
108
+ color: var(--cds-text-01, #161616);
109
+ text-decoration: none;
110
+ }
111
+ .ineligible-programs a:hover {
112
+ background-color: var(--cds-hover-ui, #e5e5e5);
113
+ }
114
+ </style>
@@ -0,0 +1,14 @@
1
+ import { redirect } from '@sveltejs/kit'
2
+ import { base } from '$app/paths'
3
+ import type { PageLoad } from './$types'
4
+ import { excludeApplicantRequirementIneligibleApps } from '$internal'
5
+
6
+ export const load: PageLoad = async ({ params, parent }) => {
7
+ const { basicRequestData } = await parent()
8
+ // exclude previously ineligible applications from default landing
9
+ const eligibleApps = excludeApplicantRequirementIneligibleApps([basicRequestData])
10
+ const key = eligibleApps[0]?.applications[0]?.programKey ?? basicRequestData.applications[0]?.programKey // if no eligibile get first ineligible
11
+ // every program can be hidden from reviewers by showIneligiblePreSubmit: false, leaving only the activity log
12
+ if (!key) throw redirect(303, `${base}/requests/${params.id}/approve/activity`)
13
+ throw redirect(303, `${base}/requests/${params.id}/approve/${key}`)
14
+ }