@reqquest/ui 1.16.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.
- package/app/env.template +5 -0
- package/app/runtime/apply-env.sh +10 -0
- package/app/runtime/nginx.conf +37 -0
- package/app/src/app.css +21 -0
- package/app/src/app.d.ts +13 -0
- package/app/src/app.html +12 -0
- package/app/src/internal/api.ts +1707 -0
- package/app/src/internal/appreq-utils.ts +33 -0
- package/app/src/internal/components/AnnouncementBanner.svelte +47 -0
- package/app/src/internal/components/AppRequestCard.svelte +173 -0
- package/app/src/internal/components/ApplicantOptOutModal.svelte +78 -0
- package/app/src/internal/components/ApplicantProgramList.svelte +276 -0
- package/app/src/internal/components/ApplicantProgramListTooltip.svelte +44 -0
- package/app/src/internal/components/ApplicantPromptPage.svelte +136 -0
- package/app/src/internal/components/ApplicantPromptSkeleton.svelte +12 -0
- package/app/src/internal/components/ApplicationDetailsView.svelte +369 -0
- package/app/src/internal/components/ButtonLoadingIcon.svelte +28 -0
- package/{dist → app/src/internal}/components/DelayedSkeleton.svelte +2 -2
- package/app/src/internal/components/FieldNestedMultiselect.svelte +270 -0
- package/app/src/internal/components/IntroPanel.svelte +41 -0
- package/app/src/internal/components/MissingDefinitionNotification.svelte +20 -0
- package/app/src/internal/components/PeriodPanel.svelte +125 -0
- package/app/src/internal/components/ProgramStatusCell.svelte +134 -0
- package/app/src/internal/components/RenderDisplayComponent.svelte +42 -0
- package/app/src/internal/components/ReviewerActions.svelte +171 -0
- package/app/src/internal/components/ReviewerQuestions.svelte +444 -0
- package/app/src/internal/components/StatusMessageList.svelte +54 -0
- package/app/src/internal/components/SystemDeterminationNotice.svelte +37 -0
- package/app/src/internal/components/TabLinks.svelte +265 -0
- package/app/src/internal/components/WarningIconYellow.svelte +22 -0
- package/app/src/internal/components/index.ts +19 -0
- package/app/src/internal/csv.ts +30 -0
- package/app/src/internal/index.ts +8 -0
- package/app/src/internal/prompt-save-queue.ts +91 -0
- package/app/src/internal/prompt-utils.ts +40 -0
- package/app/src/internal/status-utils.ts +503 -0
- package/app/src/internal/stores/IStateStore.ts +5 -0
- package/app/src/internal/util.ts +27 -0
- package/app/src/routes/+error.svelte +4 -0
- package/app/src/routes/+layout.svelte +134 -0
- package/app/src/routes/+layout.ts +18 -0
- package/app/src/routes/+page.ts +14 -0
- package/app/src/routes/announcement/+page.svelte +164 -0
- package/app/src/routes/announcement/+page.ts +16 -0
- package/app/src/routes/dashboards/applicant/+page.svelte +494 -0
- package/app/src/routes/dashboards/applicant/+page.ts +95 -0
- package/app/src/routes/dashboards/reviewer/+page.svelte +143 -0
- package/app/src/routes/dashboards/reviewer/+page.ts +43 -0
- package/app/src/routes/periods/+page.svelte +158 -0
- package/app/src/routes/periods/+page.ts +13 -0
- package/app/src/routes/periods/[id]/configure/+page.svelte +150 -0
- package/app/src/routes/periods/[id]/configure/+page.ts +10 -0
- package/app/src/routes/requests/+page.svelte +246 -0
- package/app/src/routes/requests/+page.ts +24 -0
- package/app/src/routes/requests/[id]/+layout.ts +11 -0
- package/app/src/routes/requests/[id]/accept/+layout.svelte +80 -0
- package/app/src/routes/requests/[id]/accept/+layout.ts +12 -0
- package/app/src/routes/requests/[id]/accept/+page.ts +13 -0
- package/app/src/routes/requests/[id]/accept/[promptId]/+page.svelte +7 -0
- package/app/src/routes/requests/[id]/accept/[promptId]/+page.ts +10 -0
- package/app/src/routes/requests/[id]/accept/review/+page.svelte +87 -0
- package/app/src/routes/requests/[id]/apply/+layout.svelte +181 -0
- package/app/src/routes/requests/[id]/apply/+layout.ts +12 -0
- package/app/src/routes/requests/[id]/apply/+page.ts +14 -0
- package/app/src/routes/requests/[id]/apply/ProgressNavContainer.svelte +42 -0
- package/app/src/routes/requests/[id]/apply/[promptId]/+page.svelte +23 -0
- package/app/src/routes/requests/[id]/apply/[promptId]/+page.ts +11 -0
- package/app/src/routes/requests/[id]/apply/programs/+page.svelte +71 -0
- package/app/src/routes/requests/[id]/apply/review/+page.svelte +102 -0
- package/app/src/routes/requests/[id]/approve/+layout.svelte +114 -0
- package/app/src/routes/requests/[id]/approve/+page.ts +14 -0
- package/app/src/routes/requests/[id]/approve/ApproveLayout.svelte +164 -0
- package/app/src/routes/requests/[id]/approve/[programKey]/+page.svelte +396 -0
- package/app/src/routes/requests/[id]/approve/[programKey]/+page.ts +15 -0
- package/app/src/routes/requests/[id]/approve/activity/+page.svelte +66 -0
- package/app/src/routes/requests/[id]/approve/activity/+page.ts +12 -0
- package/app/src/routes/requests/[id]/approve/export/+page.ts +12 -0
- package/app/src/routes/requests/[id]/approve/export/+page@[id].svelte +41 -0
- package/app/src/routes/requests/[id]/export/+page.svelte +32 -0
- package/app/src/routes/requests/[id]/export/+page.ts +12 -0
- package/app/src/routes/roles/+layout.svelte +16 -0
- package/app/src/routes/roles/+layout.ts +14 -0
- package/app/src/routes/roles/+page.svelte +225 -0
- package/app/src/routes/roles/+page.ts +13 -0
- package/app/src/routes/roles/[id]/+layout.ts +10 -0
- package/app/src/routes/roles/[id]/+page.svelte +335 -0
- package/app/src/routes/roles/[id]/+page.ts +10 -0
- package/app/src/routes/roles/[id]/ControlWithTooltip.svelte +15 -0
- package/app/src/routes/roles/[id]/TagsWithTooltip.svelte +42 -0
- package/app/src/routes/roles/users/+layout.ts +13 -0
- package/app/src/routes/roles/users/+page.svelte +46 -0
- package/app/src/routes/roles/users/+page.ts +90 -0
- package/app/static/favicon.png +0 -0
- package/bin/reqquest-ui.js +117 -0
- package/cli/config.js +105 -0
- package/cli/sync.js +296 -0
- package/cli/util.js +29 -0
- package/dist/components.md +0 -4
- package/dist/registry.js +1 -1
- package/dist/typed-client/schema.d.ts +25 -3
- package/dist/typed-client/schema.graphql +18 -2
- package/dist/typed-client/types.d.ts +9 -1
- package/dist/typed-client/types.js +29 -5
- package/package.json +29 -15
- 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,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>
|