@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.
- 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/package.json +29 -15
- package/dist/components/DelayedSkeleton.svelte.d.ts +0 -21
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import type { TagItem } from '@txstate-mws/carbon-svelte'
|
|
2
|
+
import { enumApplicationRescindedStatus, enumApplicationStatus, enumAppRequestPhase, enumAppRequestStatus, enumPromptVisibility, enumRequirementType, type AppRequestPhase, type AppRequestStatus, type PromptVisibility, type RequirementType } from '@reqquest/ui'
|
|
3
|
+
import { longNumericTime } from './util.js'
|
|
4
|
+
import { uiRegistry } from '$local'
|
|
5
|
+
|
|
6
|
+
interface AppStatusConfig {
|
|
7
|
+
description: string
|
|
8
|
+
tags: TagItem[]
|
|
9
|
+
waitingOn?: 'Applicant' | 'System' | 'Reviewer'
|
|
10
|
+
navigation?: {
|
|
11
|
+
label: string
|
|
12
|
+
href: (requestId: string) => string
|
|
13
|
+
}
|
|
14
|
+
// UI actions
|
|
15
|
+
buttonText: string
|
|
16
|
+
actionType: 'navigate' | 'download' | 'export' | 'none'
|
|
17
|
+
category: 'current' | 'past'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ApplicationStatusTagInfo {
|
|
21
|
+
label: string
|
|
22
|
+
description: string
|
|
23
|
+
color: TagItem['type']
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ========================================
|
|
27
|
+
// === AppRequest Status ===
|
|
28
|
+
// ========================================
|
|
29
|
+
|
|
30
|
+
export const APP_REQUEST_STATUS_CONFIG: Record<AppRequestStatus, AppStatusConfig> = {
|
|
31
|
+
STARTED: {
|
|
32
|
+
tags: [{ label: 'In progress', type: 'teal' }],
|
|
33
|
+
description: `${uiRegistry.getWord('appRequest')} is in progress and has not been submitted.`,
|
|
34
|
+
waitingOn: 'Applicant',
|
|
35
|
+
buttonText: `Edit ${uiRegistry.getWord('appRequest')}`,
|
|
36
|
+
actionType: 'navigate',
|
|
37
|
+
category: 'current',
|
|
38
|
+
navigation: {
|
|
39
|
+
label: `Continue ${uiRegistry.getWord('appRequest').toLowerCase()}`,
|
|
40
|
+
href: (requestId: string) => `/requests/${requestId}/apply`
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
READY_TO_SUBMIT: {
|
|
44
|
+
tags: [{ label: 'In progress', type: 'teal' }],
|
|
45
|
+
description: `${uiRegistry.getWord('appRequest')} is complete and ready to submit.`,
|
|
46
|
+
waitingOn: 'Applicant',
|
|
47
|
+
buttonText: `Edit ${uiRegistry.getWord('appRequest')}`,
|
|
48
|
+
actionType: 'navigate',
|
|
49
|
+
category: 'current',
|
|
50
|
+
navigation: {
|
|
51
|
+
label: `Continue ${uiRegistry.getWord('appRequest').toLowerCase()}`,
|
|
52
|
+
href: (requestId: string) => `/requests/${requestId}/apply`
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
PREAPPROVAL: {
|
|
56
|
+
tags: [{ label: 'Review pending', type: 'purple' }],
|
|
57
|
+
description: `${uiRegistry.getWord('appRequest')} submitted and waiting for pre-approval requirements.`,
|
|
58
|
+
waitingOn: 'Reviewer',
|
|
59
|
+
buttonText: `Export ${uiRegistry.getWord('appRequest')}`,
|
|
60
|
+
actionType: 'export',
|
|
61
|
+
category: 'current'
|
|
62
|
+
},
|
|
63
|
+
APPROVAL: {
|
|
64
|
+
tags: [{ label: 'Review pending', type: 'purple' }],
|
|
65
|
+
description: `${uiRegistry.getWord('appRequest')} is being pending review.`,
|
|
66
|
+
waitingOn: 'Reviewer',
|
|
67
|
+
buttonText: `Export ${uiRegistry.getWord('appRequest')}`,
|
|
68
|
+
actionType: 'export',
|
|
69
|
+
category: 'current'
|
|
70
|
+
},
|
|
71
|
+
REVIEW_IN_PROGRESS: {
|
|
72
|
+
tags: [{ label: 'In review', type: 'blue' }],
|
|
73
|
+
description: `${uiRegistry.getWord('appRequest')} is being reviewed.`,
|
|
74
|
+
waitingOn: 'Reviewer',
|
|
75
|
+
buttonText: `Export ${uiRegistry.getWord('appRequest')}`,
|
|
76
|
+
actionType: 'export',
|
|
77
|
+
category: 'current'
|
|
78
|
+
},
|
|
79
|
+
ACCEPTANCE: {
|
|
80
|
+
tags: [{ label: 'Offer pending', type: 'purple' }],
|
|
81
|
+
description: 'Waiting for you to respond to the offer.',
|
|
82
|
+
waitingOn: 'Applicant',
|
|
83
|
+
buttonText: 'Review Offer',
|
|
84
|
+
actionType: 'navigate',
|
|
85
|
+
category: 'current',
|
|
86
|
+
navigation: {
|
|
87
|
+
label: 'Review Offer',
|
|
88
|
+
href: (requestId: string) => `/requests/${requestId}/accept`
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
ACCEPTED: {
|
|
92
|
+
tags: [{ label: 'Offer accepted', type: 'green' }],
|
|
93
|
+
description: 'You have accepted an offer.',
|
|
94
|
+
buttonText: 'Download Offer',
|
|
95
|
+
actionType: 'download',
|
|
96
|
+
category: 'past'
|
|
97
|
+
},
|
|
98
|
+
READY_TO_ACCEPT: {
|
|
99
|
+
tags: [{ label: 'Offer pending', type: 'purple' }],
|
|
100
|
+
description: 'You have been offered and can now accept.',
|
|
101
|
+
waitingOn: 'Applicant',
|
|
102
|
+
buttonText: 'Review Offer',
|
|
103
|
+
actionType: 'navigate',
|
|
104
|
+
category: 'current',
|
|
105
|
+
navigation: {
|
|
106
|
+
label: 'Review Offer',
|
|
107
|
+
href: (requestId: string) => `/requests/${requestId}/accept`
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
REVIEW_COMPLETE: {
|
|
111
|
+
tags: [{ label: 'Review complete', type: 'blue' }],
|
|
112
|
+
description: `Your ${uiRegistry.getWord('appRequest').toLowerCase()} review is complete.`,
|
|
113
|
+
buttonText: `Export ${uiRegistry.getWord('appRequest')}`,
|
|
114
|
+
actionType: 'export',
|
|
115
|
+
category: 'current'
|
|
116
|
+
},
|
|
117
|
+
APPROVED: {
|
|
118
|
+
tags: [{ label: 'Approved', type: 'green' }],
|
|
119
|
+
description: `Your ${uiRegistry.getWord('appRequest').toLowerCase()} has been approved.`,
|
|
120
|
+
buttonText: '',
|
|
121
|
+
actionType: 'none',
|
|
122
|
+
category: 'past'
|
|
123
|
+
},
|
|
124
|
+
NOT_APPROVED: {
|
|
125
|
+
tags: [{ label: 'Ineligible', type: 'red' }],
|
|
126
|
+
description: `Your ${uiRegistry.getWord('appRequest').toLowerCase()} was not approved.`,
|
|
127
|
+
buttonText: '',
|
|
128
|
+
actionType: 'none',
|
|
129
|
+
category: 'past'
|
|
130
|
+
},
|
|
131
|
+
NOT_ACCEPTED: {
|
|
132
|
+
tags: [{ label: 'Offer declined', type: 'gray' }],
|
|
133
|
+
description: 'The offer was not accepted.',
|
|
134
|
+
buttonText: '',
|
|
135
|
+
actionType: 'none',
|
|
136
|
+
category: 'past'
|
|
137
|
+
},
|
|
138
|
+
CANCELLED: {
|
|
139
|
+
tags: [{ label: 'Cancelled', type: 'gray' }],
|
|
140
|
+
description: `${uiRegistry.getWord('appRequest')} was cancelled before submission.`,
|
|
141
|
+
buttonText: '',
|
|
142
|
+
actionType: 'none',
|
|
143
|
+
category: 'past'
|
|
144
|
+
},
|
|
145
|
+
WITHDRAWN: {
|
|
146
|
+
tags: [{ label: 'Withdrawn', type: 'gray' }],
|
|
147
|
+
description: `${uiRegistry.getWord('appRequest')} was withdrawn after submission.`,
|
|
148
|
+
buttonText: '',
|
|
149
|
+
actionType: 'none',
|
|
150
|
+
category: 'past'
|
|
151
|
+
},
|
|
152
|
+
DISQUALIFIED: {
|
|
153
|
+
tags: [{ label: 'Ineligible', type: 'red' }],
|
|
154
|
+
description: `All ${uiRegistry.getPlural('appRequest').toLowerCase()} have been disqualified.`,
|
|
155
|
+
waitingOn: 'Applicant',
|
|
156
|
+
buttonText: '',
|
|
157
|
+
actionType: 'none',
|
|
158
|
+
category: 'past'
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const REVIEWER_STATUS_CONFIG: Record<AppRequestStatus, { description: string, label: string, color: TagItem['type'] }> = {
|
|
163
|
+
STARTED: {
|
|
164
|
+
label: 'In progress',
|
|
165
|
+
description: `${uiRegistry.getWord('appRequest')} is in progress and has not been submitted.`,
|
|
166
|
+
color: 'teal'
|
|
167
|
+
},
|
|
168
|
+
READY_TO_SUBMIT: {
|
|
169
|
+
label: 'Ready to submit',
|
|
170
|
+
description: `${uiRegistry.getWord('appRequest')} is complete and ready to submit.`,
|
|
171
|
+
color: 'teal'
|
|
172
|
+
},
|
|
173
|
+
PREAPPROVAL: {
|
|
174
|
+
label: 'Review pending',
|
|
175
|
+
description: `${uiRegistry.getWord('appRequest')} submitted and waiting for pre-approval requirements.`,
|
|
176
|
+
color: 'purple'
|
|
177
|
+
},
|
|
178
|
+
APPROVAL: {
|
|
179
|
+
label: 'Review pending',
|
|
180
|
+
description: `${uiRegistry.getWord('appRequest')} is waiting for a reviewer to begin.`,
|
|
181
|
+
color: 'purple'
|
|
182
|
+
},
|
|
183
|
+
REVIEW_IN_PROGRESS: {
|
|
184
|
+
label: 'In review',
|
|
185
|
+
description: `${uiRegistry.getWord('appRequest')} is being reviewed.`,
|
|
186
|
+
color: 'blue'
|
|
187
|
+
},
|
|
188
|
+
ACCEPTANCE: {
|
|
189
|
+
label: 'Awaiting acceptance',
|
|
190
|
+
description: 'Waiting for you to respond to the offer.',
|
|
191
|
+
color: 'purple'
|
|
192
|
+
},
|
|
193
|
+
ACCEPTED: {
|
|
194
|
+
label: 'Accepted',
|
|
195
|
+
description: 'You have accepted an offer.',
|
|
196
|
+
color: 'green'
|
|
197
|
+
},
|
|
198
|
+
READY_TO_ACCEPT: {
|
|
199
|
+
label: 'Ready to accept',
|
|
200
|
+
description: 'You have been offered and can now accept.',
|
|
201
|
+
color: 'purple'
|
|
202
|
+
},
|
|
203
|
+
REVIEW_COMPLETE: {
|
|
204
|
+
label: 'Ready to release',
|
|
205
|
+
description: 'Review is ready to be released to the applicant.',
|
|
206
|
+
color: 'blue'
|
|
207
|
+
},
|
|
208
|
+
APPROVED: {
|
|
209
|
+
label: 'Approved',
|
|
210
|
+
description: `Your ${uiRegistry.getWord('appRequest').toLowerCase()} has been approved.`,
|
|
211
|
+
color: 'green'
|
|
212
|
+
},
|
|
213
|
+
NOT_APPROVED: {
|
|
214
|
+
label: 'Not approved',
|
|
215
|
+
description: `Your ${uiRegistry.getWord('appRequest').toLowerCase()} was not approved.`,
|
|
216
|
+
color: 'red'
|
|
217
|
+
},
|
|
218
|
+
NOT_ACCEPTED: {
|
|
219
|
+
label: 'Declined',
|
|
220
|
+
description: 'The offer was not accepted.',
|
|
221
|
+
color: 'gray'
|
|
222
|
+
},
|
|
223
|
+
CANCELLED: {
|
|
224
|
+
label: 'Cancelled',
|
|
225
|
+
description: `${uiRegistry.getWord('appRequest')} was cancelled before submission.`,
|
|
226
|
+
color: 'gray'
|
|
227
|
+
},
|
|
228
|
+
WITHDRAWN: {
|
|
229
|
+
label: 'Withdrawn',
|
|
230
|
+
description: `${uiRegistry.getWord('appRequest')} was withdrawn after submission.`,
|
|
231
|
+
color: 'gray'
|
|
232
|
+
},
|
|
233
|
+
DISQUALIFIED: {
|
|
234
|
+
label: 'Not qualified',
|
|
235
|
+
description: `All ${uiRegistry.getPlural('appRequest').toLowerCase()} have been disqualified.`,
|
|
236
|
+
color: 'red'
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export const CLOSED_STATUS_FILTER = 'CLOSED'
|
|
241
|
+
export interface StatusFilterOption { value: (AppRequestStatus | typeof CLOSED_STATUS_FILTER)[], label: string }
|
|
242
|
+
|
|
243
|
+
export function getReviewerStatusFilterOptions ({ includeClosed = false }: { includeClosed?: boolean } = {}): StatusFilterOption[] {
|
|
244
|
+
const byLabel = new Map<string, AppRequestStatus[]>()
|
|
245
|
+
for (const [status, config] of Object.entries(REVIEWER_STATUS_CONFIG) as [AppRequestStatus, { label: string }][]) {
|
|
246
|
+
const group = byLabel.get(config.label)
|
|
247
|
+
if (group) group.push(status)
|
|
248
|
+
else byLabel.set(config.label, [status])
|
|
249
|
+
}
|
|
250
|
+
const options: StatusFilterOption[] = Array.from(byLabel, ([label, value]) => ({ value, label }))
|
|
251
|
+
if (includeClosed) options.push({ value: [CLOSED_STATUS_FILTER], label: 'Closed' })
|
|
252
|
+
return options
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function splitStatusFilter (status: unknown): { status?: AppRequestStatus[], closed?: true } {
|
|
256
|
+
if (!Array.isArray(status)) return {}
|
|
257
|
+
const flat = Array.from(new Set(status.flat(2).filter((s): s is string => typeof s === 'string')))
|
|
258
|
+
const statuses = flat.filter(s => s !== CLOSED_STATUS_FILTER) as AppRequestStatus[]
|
|
259
|
+
return { status: statuses.length ? statuses : undefined, closed: flat.includes(CLOSED_STATUS_FILTER) ? true : undefined }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Reviewer status tag, plus a 'Closed' tag when the request was closed after submission
|
|
263
|
+
export function getReviewerStatusTags (status: AppRequestStatus, phase: AppRequestPhase | undefined, closedAt: string | null | undefined): TagItem[] {
|
|
264
|
+
const config = REVIEWER_STATUS_CONFIG[status]
|
|
265
|
+
const tags: TagItem[] = [{ label: config.label, type: config.color }]
|
|
266
|
+
if (closedAt != null && phase !== enumAppRequestPhase.STARTED) tags.push({ label: 'Closed', type: 'yellow' })
|
|
267
|
+
return tags
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Get complete AppRequest status information. Closing does not change the status
|
|
271
|
+
export function getAppRequestStatusInfo (status: AppRequestStatus, phase: AppRequestPhase, closedAt: string | null | undefined): AppStatusConfig {
|
|
272
|
+
const info = APP_REQUEST_STATUS_CONFIG[status]
|
|
273
|
+
const ret = { ...info, tags: [...info.tags] }
|
|
274
|
+
if (closedAt != null && phase !== enumAppRequestPhase.STARTED) ret.tags.push({ label: 'Closed', type: 'yellow' })
|
|
275
|
+
return ret
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Extract status categories
|
|
279
|
+
export function getCurrentStatuses (): AppRequestStatus[] {
|
|
280
|
+
return Object.keys(APP_REQUEST_STATUS_CONFIG).filter(
|
|
281
|
+
status => APP_REQUEST_STATUS_CONFIG[status as AppRequestStatus].category === 'current'
|
|
282
|
+
) as AppRequestStatus[]
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function getPastStatuses (): AppRequestStatus[] {
|
|
286
|
+
return Object.keys(APP_REQUEST_STATUS_CONFIG).filter(
|
|
287
|
+
status => APP_REQUEST_STATUS_CONFIG[status as AppRequestStatus].category === 'past'
|
|
288
|
+
) as AppRequestStatus[]
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Helper for navigation buttons
|
|
292
|
+
export function getNavigationButton (status: string, requestId: string): { label: string, href: string } | null {
|
|
293
|
+
const navigation = APP_REQUEST_STATUS_CONFIG[status]?.navigation
|
|
294
|
+
if (!navigation) return null
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
label: navigation.label,
|
|
298
|
+
href: navigation.href(requestId)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Helper functions for status actions
|
|
303
|
+
export function getSubmitButtonText (status: AppRequestStatus): string {
|
|
304
|
+
return APP_REQUEST_STATUS_CONFIG[status].buttonText
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function getStatusActionType (status: AppRequestStatus): 'navigate' | 'download' | 'export' | 'none' {
|
|
308
|
+
return APP_REQUEST_STATUS_CONFIG[status].actionType
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ========================================
|
|
312
|
+
// === Application Status ===
|
|
313
|
+
// ========================================
|
|
314
|
+
|
|
315
|
+
/** Display labels for the per-program ApplicationStatus values. Shared by status tags and the reviewer dashboard's Program status filter. */
|
|
316
|
+
export const APPLICATION_STATUS_CONFIG: Record<string, ApplicationStatusTagInfo> = {
|
|
317
|
+
ACCEPTED: {
|
|
318
|
+
label: 'Offer accepted',
|
|
319
|
+
description: 'Offer accepted and all requirements met.',
|
|
320
|
+
color: 'green'
|
|
321
|
+
},
|
|
322
|
+
ELIGIBLE: {
|
|
323
|
+
label: 'Approved',
|
|
324
|
+
description: 'All requirements met, acceptance pending.',
|
|
325
|
+
color: 'green'
|
|
326
|
+
},
|
|
327
|
+
INELIGIBLE: {
|
|
328
|
+
label: 'Ineligible',
|
|
329
|
+
description: 'One or more requirements not met.',
|
|
330
|
+
color: 'red'
|
|
331
|
+
},
|
|
332
|
+
PENDING: {
|
|
333
|
+
label: 'Pending',
|
|
334
|
+
description: 'Awaiting further information.',
|
|
335
|
+
color: 'purple'
|
|
336
|
+
},
|
|
337
|
+
REJECTED: {
|
|
338
|
+
label: 'Offer declined',
|
|
339
|
+
description: 'Offer rejected or requirements not met.',
|
|
340
|
+
color: 'red'
|
|
341
|
+
},
|
|
342
|
+
RESCINDED: {
|
|
343
|
+
label: 'Rescinded',
|
|
344
|
+
description: 'Previously approved and has since been rescinded.',
|
|
345
|
+
color: 'red'
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Map ApplicationStatus enum to display info.
|
|
351
|
+
*
|
|
352
|
+
* Returns a list because a restored application carries two tags - the status it was returned to
|
|
353
|
+
* plus a 'Restored' tag noting that it had been rescinded. Every other case returns a single tag.
|
|
354
|
+
*/
|
|
355
|
+
export function getApplicationStatusInfo (status: string, appRequestPhase: string, closedAt: string | null | undefined, rescindedStatus?: string | null): ApplicationStatusTagInfo[] {
|
|
356
|
+
const statusMap: Record<string, ApplicationStatusTagInfo> = {
|
|
357
|
+
ACCEPTED: {
|
|
358
|
+
label: 'Offer accepted',
|
|
359
|
+
description: 'Offer accepted and all requirements met.',
|
|
360
|
+
color: 'green'
|
|
361
|
+
},
|
|
362
|
+
ELIGIBLE: {
|
|
363
|
+
label: 'Approved',
|
|
364
|
+
description: 'All requirements met, acceptance pending.',
|
|
365
|
+
color: 'green'
|
|
366
|
+
},
|
|
367
|
+
INELIGIBLE: {
|
|
368
|
+
label: 'Ineligible',
|
|
369
|
+
description: 'One or more requirements not met.',
|
|
370
|
+
color: 'red'
|
|
371
|
+
},
|
|
372
|
+
PENDING: {
|
|
373
|
+
label: 'Pending',
|
|
374
|
+
description: 'Awaiting further information.',
|
|
375
|
+
color: 'purple'
|
|
376
|
+
},
|
|
377
|
+
REJECTED: {
|
|
378
|
+
label: 'Offer declined',
|
|
379
|
+
description: 'Offer rejected or requirements not met.',
|
|
380
|
+
color: 'red'
|
|
381
|
+
},
|
|
382
|
+
RESCINDED: {
|
|
383
|
+
label: 'Rescinded',
|
|
384
|
+
description: 'Previously approved and has since been rescinded.',
|
|
385
|
+
color: 'red'
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// closing before submission is a cancellation; after submission closing leaves the status as it was
|
|
389
|
+
if (appRequestPhase === enumAppRequestPhase.STARTED && closedAt != null) {
|
|
390
|
+
return [{
|
|
391
|
+
label: 'Cancelled',
|
|
392
|
+
description: `${uiRegistry.getWord('appRequest')} was cancelled before submission.`,
|
|
393
|
+
color: 'gray'
|
|
394
|
+
}]
|
|
395
|
+
}
|
|
396
|
+
// before submission - never submitted, or returned to the applicant
|
|
397
|
+
if (appRequestPhase === enumAppRequestPhase.STARTED && status === enumApplicationStatus.ELIGIBLE) {
|
|
398
|
+
return [{ label: 'Pending', description: 'Awaiting submission.', color: 'purple' }]
|
|
399
|
+
}
|
|
400
|
+
const tags = [APPLICATION_STATUS_CONFIG[status] ?? { label: status, description: 'Unknown status.', color: 'gray' as const }]
|
|
401
|
+
if (rescindedStatus === enumApplicationRescindedStatus.RESTORED) {
|
|
402
|
+
tags.push({
|
|
403
|
+
label: 'Restored',
|
|
404
|
+
description: 'This was rescinded and has since been restored.',
|
|
405
|
+
color: 'teal'
|
|
406
|
+
})
|
|
407
|
+
}
|
|
408
|
+
return tags
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export interface ProgramStatusFilterItem {
|
|
412
|
+
value: { status: string, rescindedStatus?: string }
|
|
413
|
+
label: string
|
|
414
|
+
children?: { value: { status: string, rescindedStatus?: string }, label: string }[]
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function getProgramStatusFilterItems (): ProgramStatusFilterItem[] {
|
|
418
|
+
const rescindable = new Set<string>([enumApplicationStatus.ELIGIBLE, enumApplicationStatus.ACCEPTED])
|
|
419
|
+
return Object.entries(APPLICATION_STATUS_CONFIG)
|
|
420
|
+
.filter(([status]) => status !== enumApplicationStatus.RESCINDED)
|
|
421
|
+
.map(([status, config]) => ({
|
|
422
|
+
value: { status },
|
|
423
|
+
label: config.label,
|
|
424
|
+
children: rescindable.has(status)
|
|
425
|
+
? [
|
|
426
|
+
{ value: { status, rescindedStatus: enumApplicationRescindedStatus.RESCINDED }, label: 'Rescinded' },
|
|
427
|
+
{ value: { status, rescindedStatus: enumApplicationRescindedStatus.RESTORED }, label: 'Restored' }
|
|
428
|
+
]
|
|
429
|
+
: undefined
|
|
430
|
+
}))
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** `getApplicationStatusInfo` as TagSet items. Shared by the applicant program list and the reviewer dashboard's Program column. */
|
|
434
|
+
export function getApplicationStatusTags (status: string, appRequestPhase: string, closedAt: string | null | undefined, rescindedStatus?: string | null): TagItem[] {
|
|
435
|
+
return getApplicationStatusInfo(status, appRequestPhase, closedAt, rescindedStatus).map(info => ({ label: info.label, type: info.color }))
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export const applicantStatuses = new Set<AppRequestStatus>([
|
|
439
|
+
enumAppRequestStatus.STARTED,
|
|
440
|
+
enumAppRequestStatus.READY_TO_SUBMIT
|
|
441
|
+
])
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* The prompt visibilities an applicant may be shown and navigated to. AVAILABLE is the row that owns
|
|
445
|
+
* the key and REQUEST_DUPE is the first appearance within its own application, so is a legitimate
|
|
446
|
+
* destination when the owning row belongs to another program.
|
|
447
|
+
*/
|
|
448
|
+
export const applicantVisiblePromptVisibilities = new Set<PromptVisibility>([
|
|
449
|
+
enumPromptVisibility.AVAILABLE,
|
|
450
|
+
enumPromptVisibility.REQUEST_DUPE
|
|
451
|
+
])
|
|
452
|
+
|
|
453
|
+
export const submissionRequirementTypes = new Set<RequirementType>([
|
|
454
|
+
enumRequirementType.PREQUAL,
|
|
455
|
+
enumRequirementType.QUALIFICATION,
|
|
456
|
+
enumRequirementType.POSTQUAL
|
|
457
|
+
])
|
|
458
|
+
|
|
459
|
+
export const applicantRequirementTypes = new Set<RequirementType>([
|
|
460
|
+
enumRequirementType.PREQUAL,
|
|
461
|
+
enumRequirementType.POSTQUAL,
|
|
462
|
+
enumRequirementType.QUALIFICATION,
|
|
463
|
+
enumRequirementType.ACCEPTANCE
|
|
464
|
+
])
|
|
465
|
+
|
|
466
|
+
export const reviewRequirementTypes = new Set<RequirementType>([
|
|
467
|
+
enumRequirementType.APPROVAL,
|
|
468
|
+
enumRequirementType.PREAPPROVAL
|
|
469
|
+
])
|
|
470
|
+
|
|
471
|
+
export const reviewerRequirementTypes = new Set<RequirementType>([
|
|
472
|
+
enumRequirementType.APPROVAL,
|
|
473
|
+
enumRequirementType.PREAPPROVAL,
|
|
474
|
+
enumRequirementType.WORKFLOW
|
|
475
|
+
])
|
|
476
|
+
|
|
477
|
+
// ========================================
|
|
478
|
+
// === Periods ===
|
|
479
|
+
// ========================================
|
|
480
|
+
const noClosePeriodDate = '9999-12-031T23:59:59.000-06:00'
|
|
481
|
+
|
|
482
|
+
export function getPeriodStatus (period: any) {
|
|
483
|
+
if (!period.openDate) return 'unknown'
|
|
484
|
+
const now = new Date()
|
|
485
|
+
const openDate = new Date(period.openDate)
|
|
486
|
+
const closeDate = (!period.closeDate) ? new Date(noClosePeriodDate) : new Date(period.closeDate)
|
|
487
|
+
if (now < openDate) return 'upcoming'
|
|
488
|
+
if (now > closeDate) return 'closed'
|
|
489
|
+
return 'open'
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export function getPeriodDisplayInfo (period: any) {
|
|
493
|
+
const status = getPeriodStatus(period)
|
|
494
|
+
return {
|
|
495
|
+
status,
|
|
496
|
+
openLabel: status === 'closed' ? 'TBD' : longNumericTime(period.openDate),
|
|
497
|
+
openDateMachineFormat: period.openDate,
|
|
498
|
+
closeLabel: status === 'closed' ? `${uiRegistry.getWord('appRequest')} closed` : `${uiRegistry.getWord('appRequest')} closes`,
|
|
499
|
+
closeDate: (!period.closeDate) ? longNumericTime(noClosePeriodDate) : longNumericTime(period.closeDate),
|
|
500
|
+
closeDateMachineFormat: period.closeDate ?? new Date(noClosePeriodDate),
|
|
501
|
+
canStartNew: status === 'open' && period.reviewed === true
|
|
502
|
+
}
|
|
503
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { DateTime } from 'luxon'
|
|
2
|
+
|
|
3
|
+
// Date Formatting Functions
|
|
4
|
+
|
|
5
|
+
export function longDateTime (dt: DateTime | string) {
|
|
6
|
+
const dateTime = typeof dt === 'string' ? DateTime.fromISO(dt) : dt
|
|
7
|
+
return dateTime.toFormat("MMMM d, yyyy '@' H:mm a")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function longNumericTime (dt: DateTime | string) {
|
|
11
|
+
const dateTime = typeof dt === 'string' ? DateTime.fromISO(dt) : dt
|
|
12
|
+
return dateTime.toFormat('MM/dd/yyyy h:mm a')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function machineDateTime (dt: DateTime | string) {
|
|
16
|
+
const dateTime = typeof dt === 'string' ? DateTime.fromISO(dt) : dt
|
|
17
|
+
return dateTime.toISO({ suppressMilliseconds: true })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function twoLineDateHtml (iso: string) {
|
|
21
|
+
const d = DateTime.fromISO(iso)
|
|
22
|
+
return `<div>${d.toFormat('D')}</div><div>${d.toFormat('t')}</div>`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function booleanToWord (bool?: boolean) {
|
|
26
|
+
return bool ? 'Yes' : 'No'
|
|
27
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { PUBLIC_ENVIRONMENT } from '$env/static/public'
|
|
3
|
+
import { api } from '$internal'
|
|
4
|
+
import { UIShell, ErrorPage } from '@txstate-mws/carbon-svelte'
|
|
5
|
+
import { unifiedAuth } from '@txstate-mws/sveltekit-utils'
|
|
6
|
+
import Dashboard from 'carbon-icons-svelte/lib/Dashboard.svelte'
|
|
7
|
+
import DocumentMultiple_01 from 'carbon-icons-svelte/lib/DocumentMultiple_01.svelte'
|
|
8
|
+
import Settings from 'carbon-icons-svelte/lib/Settings.svelte'
|
|
9
|
+
import Time from 'carbon-icons-svelte/lib/Time.svelte'
|
|
10
|
+
import type { LayoutData } from './$types.js'
|
|
11
|
+
import { uiRegistry } from '$local'
|
|
12
|
+
import { invalidateAll } from '$app/navigation'
|
|
13
|
+
import '../app.css'
|
|
14
|
+
|
|
15
|
+
api.recordNavigations()
|
|
16
|
+
|
|
17
|
+
export let data: LayoutData
|
|
18
|
+
$: ({ access, layoutError, canImpersonate } = data)
|
|
19
|
+
const userProfileName = [access?.user?.fullname.slice(0, access?.user?.fullname.indexOf(' ')), access?.user?.fullname.slice(access?.user?.fullname.indexOf(' ') + 1)]
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<UIShell appName={uiRegistry.config.appName} reverseWeights={!!PUBLIC_ENVIRONMENT} companyName={PUBLIC_ENVIRONMENT || 'TXST'} overlayText={PUBLIC_ENVIRONMENT} navRoot={{
|
|
23
|
+
title: uiRegistry.config.appName,
|
|
24
|
+
hideFromSideNav: true,
|
|
25
|
+
routeId: '/',
|
|
26
|
+
children: [
|
|
27
|
+
{
|
|
28
|
+
title: uiRegistry.config.applicantDashboardTitle ?? 'Applicant Dashboard',
|
|
29
|
+
navTitle: uiRegistry.config.applicantDashboardNavTitle ?? uiRegistry.config.applicantDashboardTitle ?? 'Applicant',
|
|
30
|
+
group: 'Dashboards',
|
|
31
|
+
hideFromSideNav: !access?.viewApplicantDashboard,
|
|
32
|
+
icon: Dashboard,
|
|
33
|
+
routeId: '/dashboards/applicant',
|
|
34
|
+
children: [{
|
|
35
|
+
title: uiRegistry.getWord('appRequest'),
|
|
36
|
+
routeId: '/requests/[id]/apply',
|
|
37
|
+
children: [{
|
|
38
|
+
title: 'Review Your Submission',
|
|
39
|
+
routeId: '/requests/[id]/apply/review'
|
|
40
|
+
}, {
|
|
41
|
+
title: 'Program review',
|
|
42
|
+
routeId: '/requests/[id]/apply/programs'
|
|
43
|
+
}, {
|
|
44
|
+
routeId: '/requests/[id]/apply/[promptId]',
|
|
45
|
+
title: page => page.data.promptsById?.[page.params.promptId ?? '']?.navTitle ?? 'Unknown Prompt'
|
|
46
|
+
}]
|
|
47
|
+
}]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
title: 'Reviewer',
|
|
51
|
+
group: 'Dashboards',
|
|
52
|
+
hideFromSideNav: !access?.viewReviewerInterface,
|
|
53
|
+
icon: Dashboard,
|
|
54
|
+
routeId: '/dashboards/reviewer'
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
title: uiRegistry.getPlural('appRequest'),
|
|
58
|
+
group: 'Administration',
|
|
59
|
+
hideFromSideNav: !access?.viewAppRequestList,
|
|
60
|
+
icon: DocumentMultiple_01,
|
|
61
|
+
routeId: '/requests',
|
|
62
|
+
children: [{
|
|
63
|
+
title: $page => $page.data.basicRequestData ? `${$page.data.basicRequestData.applicant.fullname} (${$page.data.basicRequestData.period.name})` : 'Unknown Request',
|
|
64
|
+
routeId: '/requests/[id]',
|
|
65
|
+
children: [{
|
|
66
|
+
title: `Review ${uiRegistry.getWord('appRequest')}`,
|
|
67
|
+
routeId: '/requests/[id]/approve',
|
|
68
|
+
children: [{
|
|
69
|
+
title: 'Activity Log',
|
|
70
|
+
routeId: '/requests/[id]/approve/activity'
|
|
71
|
+
}]
|
|
72
|
+
}]
|
|
73
|
+
}]
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
title: 'Admin Settings',
|
|
77
|
+
group: 'Administration',
|
|
78
|
+
hideFromSideNav: !access?.viewRoleManagement,
|
|
79
|
+
icon: Settings,
|
|
80
|
+
routeId: '/roles',
|
|
81
|
+
children: [{
|
|
82
|
+
title: $page => `Role: ${$page.data.role.name}`,
|
|
83
|
+
routeId: '/roles/[id]'
|
|
84
|
+
}, {
|
|
85
|
+
title: 'Users',
|
|
86
|
+
routeId: '/roles/users'
|
|
87
|
+
}]
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
title: `Manage ${uiRegistry.getPlural('period')}`,
|
|
91
|
+
group: 'Administration',
|
|
92
|
+
hideFromSideNav: !access?.viewPeriodManagement,
|
|
93
|
+
icon: Time,
|
|
94
|
+
routeId: '/periods',
|
|
95
|
+
children: [
|
|
96
|
+
{
|
|
97
|
+
title: `Configure ${uiRegistry.getWord('period')}`,
|
|
98
|
+
routeId: '/periods/[id]/configure'
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
title: 'Manage Announcement',
|
|
104
|
+
group: 'Administration',
|
|
105
|
+
hideFromSideNav: !access?.viewAnnouncementManagement,
|
|
106
|
+
icon: Time,
|
|
107
|
+
routeId: '/announcement'
|
|
108
|
+
},
|
|
109
|
+
...(uiRegistry.config.extraNavItems ?? [])
|
|
110
|
+
]
|
|
111
|
+
}}
|
|
112
|
+
profilelinks={[...(uiRegistry.config.profileLinks ?? []), { label: 'Logout', onClick: () => unifiedAuth.logout(api) }]}
|
|
113
|
+
userProfile={{firstName: userProfileName?.[0] ?? access?.user?.login ?? '', lastName: userProfileName?.[1] ?? ''}}
|
|
114
|
+
impersonate={{
|
|
115
|
+
canImpersonate: !!canImpersonate && !!uiRegistry.config.userLookup, // only show impersonation option if user has permission and userLookup function is provided
|
|
116
|
+
userLookup: async (login) => (uiRegistry.config.userLookup) ? await uiRegistry.config.userLookup(login) : undefined, // use the userLookup function from the registry to find the user profile based on login
|
|
117
|
+
impersonate: async (login) => {
|
|
118
|
+
if (!login) return
|
|
119
|
+
await unifiedAuth.impersonate(api, login)
|
|
120
|
+
invalidateAll()
|
|
121
|
+
},
|
|
122
|
+
endImpersonate: async () => {
|
|
123
|
+
unifiedAuth.exitImpersonation(api)
|
|
124
|
+
await invalidateAll()
|
|
125
|
+
},
|
|
126
|
+
isImpersonating: unifiedAuth.getImpersonationStatus(api).isImpersonating
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
{#if layoutError}
|
|
130
|
+
<ErrorPage error={layoutError} />
|
|
131
|
+
{:else}
|
|
132
|
+
<slot />
|
|
133
|
+
{/if}
|
|
134
|
+
</UIShell>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { api } from '$internal'
|
|
2
|
+
import type { LayoutLoad } from './$types'
|
|
3
|
+
import { unifiedAuth } from '@txstate-mws/sveltekit-utils'
|
|
4
|
+
|
|
5
|
+
export const load: LayoutLoad = async input => {
|
|
6
|
+
api.fetch = input.fetch
|
|
7
|
+
input.depends('api:getAccess')
|
|
8
|
+
await unifiedAuth.handle(api, input)
|
|
9
|
+
try {
|
|
10
|
+
const canImpersonate = await unifiedAuth.mayImpersonateAny(api)
|
|
11
|
+
const access = await api.getAccess()
|
|
12
|
+
return { access, canImpersonate }
|
|
13
|
+
} catch (e: any) {
|
|
14
|
+
return { layoutError: { status: e.status, message: e.message } }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const ssr = false
|