@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,246 @@
1
+ <script lang="ts">
2
+ import { ColumnList, FieldDate, FieldMultiselect, FieldSelect, FieldTextInput, FilterUI, IntroPanel, Pagination, Panel, PanelFormDialog } from '@txstate-mws/carbon-svelte'
3
+ import View from 'carbon-icons-svelte/lib/View.svelte'
4
+ import DocExport from 'carbon-icons-svelte/lib/DocumentExport.svelte'
5
+ import { DateTime } from 'luxon'
6
+ import { htmlEncode, isBlank, isNotBlank, keyby, sortby, toQuery } from 'txstate-utils'
7
+ import { goto } from '$app/navigation'
8
+ import { resolve } from '$app/paths'
9
+ import { api, FieldNestedMultiselect, getProgramStatusFilterItems, getReviewerStatusFilterOptions, getReviewerStatusTags, ProgramStatusCell, twoLineDateHtml, REVIEWER_STATUS_CONFIG } from '$internal'
10
+ import { enumApplicationRescindedStatus } from '@reqquest/ui'
11
+ import { uiRegistry } from '$local'
12
+ import type { PageData } from './$types.js'
13
+ import { _defaultRequestListFilters } from './+page.js'
14
+
15
+ export let data: PageData
16
+
17
+ $: ({ appRequests, appRequestIndexes: indexes, allPeriods, openPeriods, access, filters, programs } = data)
18
+ const statusFilterItems = getReviewerStatusFilterOptions({ includeClosed: true })
19
+ $: requests = appRequests.map(r => ({ ...r, indexByCat: keyby(r.indexCategories, 'category') }))
20
+ $: indexColumns = sortby(indexes.filter(idx => idx.appRequestListPriority), 'appRequestListPriority').map(idx => ({
21
+ id: idx.category,
22
+ label: idx.categoryLabel,
23
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- r.indexByCat[idx.category] may be undefined if no values are present
24
+ render: (r: typeof requests[number]) => htmlEncode(r.indexByCat[idx.category]?.values.map(v => v.label).join(', '))
25
+ }))
26
+ $: filterIndexes = sortby(indexes.filter(idx => idx.listFiltersPriority && (idx.values.length || !idx.listable)), 'listFiltersPriority')
27
+
28
+ let unlistableIndexItems: Record<string, { value: string, label: string }[] | undefined> = {}
29
+ function searchIndexItems (category: string) {
30
+ return (e: Event) => {
31
+ (async () => {
32
+ const search = (e.target as HTMLInputElement).value
33
+ if (isBlank(search)) {
34
+ unlistableIndexItems[category] = undefined
35
+ } else {
36
+ unlistableIndexItems[category] = await api.searchIndexItems(category, search)
37
+ }
38
+ unlistableIndexItems = unlistableIndexItems // trigger reactivity
39
+ })().catch(console.error)
40
+ }
41
+ }
42
+
43
+ let createDialog = false
44
+ let lastInsertedId: string | undefined
45
+ async function openCreateDialog () {
46
+ createDialog = true
47
+ }
48
+
49
+ function closeCreateDialog () {
50
+ createDialog = false
51
+ }
52
+
53
+ async function onCreateSaved () {
54
+ closeCreateDialog()
55
+ await goto(resolve(`/requests/${lastInsertedId}/approve`))
56
+ }
57
+
58
+ async function validateAppRequest (data: { periodId: string, login: string }) {
59
+ const response = await api.createAppRequest(data.periodId, data.login, true)
60
+ return response.messages
61
+ }
62
+
63
+ async function submitAppRequest (data: { periodId: string, login: string }) {
64
+ const response = await api.createAppRequest(data.periodId, data.login)
65
+ if (response.success) lastInsertedId = response.id
66
+ return {
67
+ ...response,
68
+ data
69
+ }
70
+ }
71
+ let showDateFilters = isNotBlank(filters?.closedAfter) || isNotBlank(filters?.closedBefore) || isNotBlank(filters?.updatedAfter) || isNotBlank(filters?.updatedBefore) || isNotBlank(filters?.submittedAfter) || isNotBlank(filters?.submittedBefore)
72
+
73
+ async function downloadCSV (ids?: string[]) {
74
+ const ticket = await api.getDownloadTicket()
75
+ const query = toQuery({ f: ids?.length ? { ids } : (filters ?? _defaultRequestListFilters) } as unknown as Parameters<typeof toQuery>[0])
76
+ location.href = `${api.baseUrl}/csv/${ticket}/requests/requests${DateTime.now().toFormat('yyyyLLddHHmmss')}.csv?${query}`
77
+ }
78
+ </script>
79
+ <div class='intro-wide [ px-[20px] ]'>
80
+ <div class="requests-filters">
81
+ <FilterUI search>
82
+ <svelte:fragment slot="quickfilters">
83
+ <FieldMultiselect
84
+ path="status"
85
+ labelText="Application status"
86
+ label="Choose one or more"
87
+ hideLabel={false}
88
+ json
89
+ items={statusFilterItems}
90
+ />
91
+ <FieldNestedMultiselect
92
+ path="applicationStatuses"
93
+ labelText="Program status"
94
+ items={getProgramStatusFilterItems()}
95
+ />
96
+ <FieldMultiselect
97
+ path="programKeys"
98
+ labelText="Program"
99
+ label="Choose one or more"
100
+ hideLabel={false}
101
+ items={programs.map(p => ({ value: p.key, label: p.title }))}
102
+ />
103
+ </svelte:fragment>
104
+ <FieldMultiselect
105
+ path="periodIds"
106
+ label="Periods"
107
+ placeholder="Select Periods"
108
+ items={allPeriods.map(p => ({ value: p.id, label: p.name }))}
109
+ filterable
110
+ />
111
+ {#each filterIndexes as filterIdx (filterIdx.category)}
112
+ <FieldMultiselect path="indexes.{filterIdx.category}"
113
+ label={filterIdx.categoryLabel}
114
+ placeholder={filterIdx.categoryLabel}
115
+ items={filterIdx.listable ? filterIdx.values : unlistableIndexItems[filterIdx.category] ?? []}
116
+ filterable={!filterIdx.listable}
117
+ on:input={!filterIdx.listable ? searchIndexItems(filterIdx.category) : () => {}}
118
+ />
119
+ {/each}
120
+ <FieldDate
121
+ path="createdAfter"
122
+ labelText="Created After"
123
+ placeholder="Select a date"
124
+ beginningOfDay
125
+ />
126
+ <FieldDate
127
+ path="createdBefore"
128
+ labelText="Created Before"
129
+ placeholder="Select a date"
130
+ endOfDay
131
+ />
132
+ <Panel bind:expanded={showDateFilters} expandable title="Additional Date Filters">
133
+ <FieldDate
134
+ path="submittedAfter"
135
+ labelText="Submitted After"
136
+ placeholder="Select a date"
137
+ beginningOfDay
138
+ />
139
+ <FieldDate
140
+ path="submittedBefore"
141
+ labelText="Submitted Before"
142
+ placeholder="Select a date"
143
+ endOfDay
144
+ />
145
+ <FieldDate
146
+ path="closedAfter"
147
+ labelText="Closed After"
148
+ placeholder="Select a date"
149
+ beginningOfDay
150
+ />
151
+ <FieldDate
152
+ path="closedBefore"
153
+ labelText="Closed Before"
154
+ placeholder="Select a date"
155
+ endOfDay
156
+ />
157
+ <FieldDate
158
+ path="updatedAfter"
159
+ labelText="Updated After"
160
+ placeholder="Select a date"
161
+ beginningOfDay
162
+ />
163
+ <FieldDate
164
+ path="updatedBefore"
165
+ labelText="Updated Before"
166
+ placeholder="Select a date"
167
+ endOfDay
168
+ />
169
+ </Panel>
170
+ </FilterUI>
171
+ </div>
172
+ <IntroPanel title="All Applications" subtitle="This is where you can see all applications submitted to the business app. Browse them all or use the filters above to narrow down applications." />
173
+ <ColumnList
174
+ autoHideColumns
175
+ title={uiRegistry.getPlural('appRequest')}
176
+ columns={[
177
+ { id: 'id', label: 'Request #', fixed: '90px', minWidth: 90, tags: r => [{ label: r.id }] },
178
+ { id: 'login', label: uiRegistry.getWord('login'), minWidth: 100, tags: r => [{ label: r.applicant.login, type: 'green' }] },
179
+ { id: 'period', label: uiRegistry.getWord('period'), minWidth: 150, render: r => htmlEncode(r.period.name) },
180
+ { id: 'name', label: 'Name', render: r => r.applicant.fullname, grow: 2 },
181
+ { id: 'dateSubmitted', label: 'Submitted', minWidth: 120, render: r => twoLineDateHtml(r.createdAt) },
182
+ { id: 'program', label: 'Program', minWidth: 220, component: ProgramStatusCell },
183
+ { id: 'status', label: 'Application status', minWidth: 150, tags: r => getReviewerStatusTags(r.status, r.phase, r.closedAt) },
184
+ ...indexColumns,
185
+ { id: 'lastUpdated', label: 'Last Updated', minWidth: 120, render: r => twoLineDateHtml(r.updatedAt) }
186
+ ]}
187
+ selectedActions={rows => [
188
+ { label: 'Download selected', icon: DocExport, onClick: () => downloadCSV(rows.map(r => r.id)) }
189
+ ]}
190
+ listActions={[
191
+ ...(access.createAppRequestOther
192
+ ? [
193
+ { label: `Create ${uiRegistry.getWord('appRequest')}`, onClick: openCreateDialog }
194
+ ]
195
+ : []
196
+ ),
197
+ { label: 'Download', icon: DocExport, onClick: async () => { await downloadCSV() } }
198
+ ]}
199
+ actions={row => [
200
+ { icon: View, label: 'View', onClick: () => { goto(`/requests/${row.id}/approve`) } }
201
+ ]}
202
+ rows={requests}
203
+ >
204
+ <svelte:fragment let:row>
205
+ <div class="[ mb-2 ]"><strong>Programs</strong></div>
206
+ <ProgramStatusCell {row} rollup={false} />
207
+ </svelte:fragment>
208
+ </ColumnList>
209
+ <Pagination
210
+ totalItems={data.pageInfo.appRequests?.totalItems}
211
+ page={data.pageInfo.appRequests?.currentPage}
212
+ pageSize={25}
213
+ chooseSize
214
+ />
215
+ </div>
216
+
217
+ <PanelFormDialog open={createDialog} title={`Create ${uiRegistry.getWord('appRequest')}`} validate={validateAppRequest} submit={submitAppRequest} on:cancel={closeCreateDialog} on:saved={onCreateSaved}>
218
+ <FieldSelect
219
+ labelText={uiRegistry.getWord('period')}
220
+ path="periodId"
221
+ items={openPeriods.map(p => ({ value: p.id, label: p.name }))}
222
+ required
223
+ helperText={`Select the ${uiRegistry.getWord('period').toLowerCase()} in which you want to create an ${uiRegistry.getWord('appRequest').toLowerCase()}.`}
224
+ />
225
+ <FieldTextInput
226
+ path="login"
227
+ labelText={`Applicant ${uiRegistry.getWord('login')}`}
228
+ required
229
+ notNull
230
+ helperText={`Enter the ${uiRegistry.getWord('login').toLowerCase()} of the applicant for this request.`}
231
+ />
232
+ </PanelFormDialog>
233
+
234
+ <style>
235
+ .intro-wide :global(.intro-panel .content-start) {
236
+ max-width: none;
237
+ }
238
+ /* the quick-filter fields carry labels above them; bottom-align the row so the search box and
239
+ More filters button sit level with the fields rather than with the labels */
240
+ .requests-filters :global(.filter-ui-container) {
241
+ align-items: flex-end;
242
+ }
243
+ .app-requests-intro {
244
+ background-color: var(--cds-layer);
245
+ }
246
+ </style>
@@ -0,0 +1,24 @@
1
+ import { extractMergedFilters, extractPaginationParams } from '@txstate-mws/carbon-svelte'
2
+ import { api, splitStatusFilter } from '$internal'
3
+ import type { AppRequestFilter } from '@reqquest/ui'
4
+ import type { PageLoad } from './$types'
5
+ import { error } from '@sveltejs/kit'
6
+
7
+ export const _defaultRequestListFilters = {}
8
+
9
+ export const load: PageLoad = async ({ url, parent }) => {
10
+ const { access } = await parent()
11
+ // access is missing only when the layout failed; it renders layoutError in place of the page, so don't mask it
12
+ if (!access) return {} as never
13
+ if (!access.viewAppRequestList) throw error(403)
14
+ const { page, pagesize } = extractPaginationParams(url)
15
+ const { status, ...rest } = extractMergedFilters(url)
16
+ const filters: Omit<AppRequestFilter, 'indexes'> = { ...rest, ...splitStatusFilter(status) }
17
+ const [response, allPeriods, programs] = await Promise.all([
18
+ api.getAppRequests(filters, { page, perPage: pagesize }),
19
+ api.getPeriodList(),
20
+ api.getPrograms()
21
+ ])
22
+ const openPeriods = allPeriods.filter(p => p.actions.createAppRequest)
23
+ return { ...response, allPeriods, openPeriods, filters, programs }
24
+ }
@@ -0,0 +1,11 @@
1
+ import { api } from '$internal'
2
+ import { error } from '@sveltejs/kit'
3
+ import { uiRegistry } from '$local'
4
+ import type { LayoutLoad } from './$types'
5
+
6
+ export const load: LayoutLoad = async input => {
7
+ const appRequest = await api.getBasicRequestData(input.params.id)
8
+ input.depends('request:id')
9
+ if (!appRequest) throw error(404, `${uiRegistry.getWord('appRequest')} not found. It may have been deleted or you do not have permission to view it.`)
10
+ return { basicRequestData: appRequest, requestId: input.params.id }
11
+ }
@@ -0,0 +1,80 @@
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 type { LayoutData } from './$types'
8
+
9
+ export let data: LayoutData
10
+ $: ({ appRequestForExport, applicationsAccept } = data)
11
+
12
+ function getNavItems (..._: any[]) {
13
+ const navItems: RootStepItem[] = []
14
+ let nextHref: string | undefined
15
+ let prevHref: string | undefined
16
+ let foundCurrent = false
17
+ let lastprompt: string | undefined
18
+
19
+ for (const application of applicationsAccept) {
20
+ const substeps: StepItem[] = []
21
+ for (const requirement of application.requirements) {
22
+ for (const prompt of requirement.prompts) {
23
+ if (foundCurrent) {
24
+ nextHref = resolve(`/requests/${appRequestForExport.id}/accept/${prompt.id}`)
25
+ foundCurrent = false
26
+ }
27
+ if ($page.params.promptId === prompt.id) {
28
+ foundCurrent = true
29
+ if (lastprompt) prevHref = resolve(`/requests/${appRequestForExport.id}/accept/${lastprompt}`)
30
+ }
31
+ lastprompt = prompt.id
32
+ substeps.push({
33
+ id: `prompt${prompt.id}`,
34
+ label: prompt.navTitle,
35
+ href: resolve(`/requests/${appRequestForExport.id}/accept/${prompt.id}`),
36
+ type: $page.params.promptId === prompt.id
37
+ ? 'current'
38
+ : prompt.answered && !prompt.invalidated ? 'complete' : 'available'
39
+ })
40
+ }
41
+ }
42
+ if (substeps.length > 0) {
43
+ navItems.push({
44
+ id: `application${application.id}`,
45
+ label: application.navTitle,
46
+ href: substeps[0].href,
47
+ type: substeps.some(s => s.type === 'current')
48
+ ? 'current'
49
+ : substeps.every(s => s.type === 'complete')
50
+ ? 'complete'
51
+ : 'available',
52
+ substeps
53
+ })
54
+ }
55
+ }
56
+ lastprompt = undefined
57
+ navItems.push({
58
+ id: 'review',
59
+ label: 'Review submission',
60
+ href: resolve(`/requests/${appRequestForExport.id}/accept/review`),
61
+ type: $page.route.id === '/requests/[id]/accept/review' ? 'current' : 'available',
62
+ substeps: []
63
+ })
64
+ nextHref ??= resolve(`/requests/${appRequestForExport.id}/accept/review`)
65
+ return { navItems, nextHref, prevHref }
66
+ }
67
+ $: ({ navItems, nextHref, prevHref } = getNavItems(appRequestForExport, applicationsAccept, $page))
68
+ $: notInNavigation = !navItems.some(item => item.type === 'current' || item.substeps.some(s => s.type === 'current'))
69
+ const nextHrefStore = writable({ nextHref, prevHref })
70
+ $: nextHrefStore.set({ nextHref, prevHref })
71
+ setContext('nextHref', nextHrefStore)
72
+ </script>
73
+
74
+ <ProgressNav steps={navItems}>
75
+ {#if notInNavigation}
76
+ <ErrorPage error={{ status: 404, message: 'Not Found' }} />
77
+ {:else}
78
+ <slot />
79
+ {/if}
80
+ </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.viewAcceptUI) throw error(404, 'This application cannot be accepted at this time.')
11
+ return { ...applicationInfo, appRequestForExport: { ...appRequest, applications: undefined } }
12
+ }
@@ -0,0 +1,13 @@
1
+ import { redirect } from '@sveltejs/kit'
2
+ import { resolve } from '$app/paths'
3
+ import type { PageLoad } from './$types'
4
+
5
+ export const load: PageLoad = async ({ params, parent }) => {
6
+ const { applicationsAccept } = await parent()
7
+ for (const prompt of applicationsAccept.flatMap(a => a.requirements).flatMap(r => r.prompts)) {
8
+ if (!prompt.answered || prompt.invalidated) {
9
+ throw redirect(303, resolve(`/requests/${params.id}/accept/${prompt.id}`))
10
+ }
11
+ }
12
+ throw redirect(303, resolve(`/requests/${params.id}/accept/review`))
13
+ }
@@ -0,0 +1,7 @@
1
+ <script lang="ts">
2
+ import { ApplicantPromptPage } from '$internal'
3
+ import type { PageData } from './$types'
4
+
5
+ export let data: PageData
6
+ </script>
7
+ <ApplicantPromptPage {data} statusReasons={data.promptsById[data.prompt.id]?.statusReasons ?? []} />
@@ -0,0 +1,10 @@
1
+ import { api } from '$internal'
2
+ import { error } from '@sveltejs/kit'
3
+ import type { PageLoad } from './$types'
4
+
5
+ export const load: PageLoad = async ({ params, depends, parent }) => {
6
+ const { appRequest, prompt } = await api.getApplicantPrompt(params.id, params.promptId)
7
+ depends('request:apply')
8
+ if (!prompt) throw error(404, 'Prompt not found')
9
+ return { prompt, dataVersion: appRequest.dataVersion }
10
+ }
@@ -0,0 +1,87 @@
1
+ <script lang="ts">
2
+ import { toasts } from '@txstate-mws/svelte-components'
3
+ import { Button } 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
+
15
+ /**
16
+ * This page will allow applicants to review all the information they've
17
+ * entered so far, and to navigate to any prompts that need correction, then
18
+ * allows them to submit the app request.
19
+ */
20
+ export let data: PageData
21
+ $: ({ appRequestForExport, applicationsAccept } = data)
22
+
23
+ const nextHref = getContext<Writable<{ nextHref?: ResolvedPathname, prevHref?: ResolvedPathname }>>('nextHref')
24
+ $: hasPreviousPrompt = !!$nextHref.prevHref
25
+
26
+ async function handleBack () {
27
+ if ($nextHref.prevHref) {
28
+ // eslint-disable-next-line svelte/no-navigation-without-resolve -- already resolved
29
+ await goto($nextHref.prevHref)
30
+ }
31
+ }
32
+
33
+ async function onSubmit () {
34
+ const resp = await api.appRequestPhaseChange(appRequestForExport.id, 'acceptOffer')
35
+ if (resp.success) await goto(resolve('/dashboards/applicant'))
36
+ else 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.' })
37
+ }
38
+
39
+ afterNavigate(() => {
40
+ const h2 = document.querySelector('h2')
41
+ if (h2) {
42
+ h2.tabIndex = -1
43
+ h2.focus()
44
+ }
45
+ })
46
+ </script>
47
+
48
+ <ApplicationDetailsView
49
+ appRequest={appRequestForExport}
50
+ applications={applicationsAccept}
51
+ appData={appRequestForExport.data}
52
+ {uiRegistry}
53
+ title="Review Your Submission"
54
+ subtitle="Please review all information before submitting."
55
+ expandable={false}
56
+ showWarningsInline={true}
57
+ showAppRequestStatus={false}
58
+ statusDisplay="icons"
59
+ >
60
+ <svelte:fragment slot="footer">
61
+ <footer class="[ mt-6 ] flow">
62
+ {#if uiRegistry.config.supportUrl}
63
+ <p class="text-center">If you think there was a mistake please review your answers or reach out through the support page.</p>
64
+ {/if}
65
+ <div class="footer-actions">
66
+ {#if hasPreviousPrompt}
67
+ <Button kind="ghost" on:click={handleBack}>Back</Button>
68
+ {/if}
69
+ {#if uiRegistry.config.supportUrl}
70
+ <Button kind="ghost" href={uiRegistry.config.supportUrl}>Support</Button>
71
+ {/if}
72
+ {#if appRequestForExport.status === enumAppRequestStatus.READY_TO_ACCEPT}
73
+ <Button icon={Touch_1} on:click={onSubmit}>Submit</Button>
74
+ {/if}
75
+ </div>
76
+ </footer>
77
+ </svelte:fragment>
78
+ </ApplicationDetailsView>
79
+
80
+ <style>
81
+ .footer-actions {
82
+ display: flex;
83
+ gap: 3rem;
84
+ justify-content: center;
85
+ margin-top: 2rem;
86
+ }
87
+ </style>