@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,33 @@
|
|
|
1
|
+
import { enumIneligiblePhases, enumRequirementStatus, type IneligiblePhases } from '@reqquest/ui'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* True when this application is ineligible because of an applicant requirement (PREQUAL, QUALIFICATION
|
|
5
|
+
* or POSTQUAL). This looks at the type of the failing requirement, not at when it failed - a reviewer
|
|
6
|
+
* changing an applicant answer after submission also makes this true. For "was it ineligible before
|
|
7
|
+
* submission" use the API's `hiddenIneligiblePreSubmit` instead.
|
|
8
|
+
*
|
|
9
|
+
* These are the applications that land in the "Ineligible benefits" panel on
|
|
10
|
+
* the applicant screens, and the ones whose program `eligibilityDescription` should be shown.
|
|
11
|
+
* Anywhere that cares about membership in that panel should go through this function so the
|
|
12
|
+
* panel and the extra messaging can't drift out of sync.
|
|
13
|
+
*/
|
|
14
|
+
export function isIneligibleByApplicantRequirement (application: { ineligiblePhase?: IneligiblePhases | null }) {
|
|
15
|
+
return application.ineligiblePhase === enumIneligiblePhases.PREQUAL || application.ineligiblePhase === enumIneligiblePhases.QUALIFICATION
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
// Return the appRequests with any application that is ineligible because of an applicant requirement stripped out of each one. See `isIneligibleByApplicantRequirement`.
|
|
20
|
+
export function excludeApplicantRequirementIneligibleApps<T extends { applications: { ineligiblePhase?: IneligiblePhases | null }[] }> (appRequests: T[]): T[] {
|
|
21
|
+
return appRequests.map(ar => ({
|
|
22
|
+
...ar,
|
|
23
|
+
applications: ar.applications.filter(app => !isIneligibleByApplicantRequirement(app))
|
|
24
|
+
}))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isHiddenFromApplicant (application: { hiddenIneligiblePreSubmit?: boolean | null, requirements?: { status: string, prompts: { optOut?: boolean | null }[] }[] }) {
|
|
28
|
+
if (!application.hiddenIneligiblePreSubmit) return false
|
|
29
|
+
const optedOut = application.requirements?.some(r => r.status === enumRequirementStatus.DISQUALIFYING && r.prompts.some(p => p.optOut))
|
|
30
|
+
return !optedOut
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const DEFAULT_APPLICANT_NO_PROGRAMS_MESSAGE = 'Based on your responses, you don\'t currently qualify for any available benefits. If you believe this is incorrect, review your answers.'
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { InlineNotification, NotificationActionButton } from 'carbon-components-svelte'
|
|
3
|
+
|
|
4
|
+
/** Short headline shown in bold. */
|
|
5
|
+
export let subject: string
|
|
6
|
+
/** Body text shown under the subject. */
|
|
7
|
+
export let body: string
|
|
8
|
+
/** Optional URL rendered as a ghost action button after the text. */
|
|
9
|
+
export let link: string | null | undefined = undefined
|
|
10
|
+
/** Visible text for the link. Required by the API whenever a link is given. */
|
|
11
|
+
export let linkText: string | null | undefined = undefined
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<!--
|
|
15
|
+
Deliberately NOT carbon's default role="alert". The banner is ordinary page content that is
|
|
16
|
+
already there when the applicant arrives, so it must be a labelled landmark that screen readers
|
|
17
|
+
meet once in reading order (and can jump to), not a live region that announces itself and its
|
|
18
|
+
link on insertion. NVDA was reading the link three times under role="alert". The prop type only
|
|
19
|
+
admits alert|log|status, but the component forwards the string verbatim to the root div, hence
|
|
20
|
+
the cast. The fixed label keeps the subject from being spoken twice (landmark name + title).
|
|
21
|
+
-->
|
|
22
|
+
<InlineNotification
|
|
23
|
+
role={'region' as any}
|
|
24
|
+
aria-label="Announcement"
|
|
25
|
+
kind="warning"
|
|
26
|
+
title={subject}
|
|
27
|
+
subtitle={body}
|
|
28
|
+
lowContrast
|
|
29
|
+
hideCloseButton
|
|
30
|
+
class="time-sensitive-banner"
|
|
31
|
+
>
|
|
32
|
+
<svelte:fragment slot="actions">
|
|
33
|
+
{#if link}
|
|
34
|
+
<NotificationActionButton href={link}>{linkText}</NotificationActionButton>
|
|
35
|
+
{/if}
|
|
36
|
+
</svelte:fragment>
|
|
37
|
+
</InlineNotification>
|
|
38
|
+
|
|
39
|
+
<style>
|
|
40
|
+
:global(div.time-sensitive-banner.bx--inline-notification) {
|
|
41
|
+
min-width: unset;
|
|
42
|
+
max-width: fit-content;
|
|
43
|
+
flex-wrap: wrap;
|
|
44
|
+
width: auto;
|
|
45
|
+
align-items: center;
|
|
46
|
+
}
|
|
47
|
+
</style>
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ActionItem } from '@txstate-mws/carbon-svelte'
|
|
3
|
+
import { Card, TagSet } from '@txstate-mws/carbon-svelte'
|
|
4
|
+
import { Button } from 'carbon-components-svelte'
|
|
5
|
+
import { goto } from '$app/navigation'
|
|
6
|
+
import { resolve } from '$app/paths'
|
|
7
|
+
import { enumPromptVisibility } from '@reqquest/ui'
|
|
8
|
+
import { getApplicationStatusInfo, getAppRequestStatusInfo, getNavigationButton } from '../status-utils.js'
|
|
9
|
+
import { longNumericTime } from '../util.js'
|
|
10
|
+
import { uiRegistry } from '$local'
|
|
11
|
+
import type { PageData } from '../../routes/dashboards/applicant/$types'
|
|
12
|
+
import StatusMessageList from './StatusMessageList.svelte'
|
|
13
|
+
import WarningIconYellow from './WarningIconYellow.svelte'
|
|
14
|
+
|
|
15
|
+
// Type for the partial AppRequest data passed from dashboard
|
|
16
|
+
type DashboardAppRequest = PageData['appRequests'][number]
|
|
17
|
+
|
|
18
|
+
export let request: DashboardAppRequest
|
|
19
|
+
export let actions: ActionItem[] = []
|
|
20
|
+
export let showAcceptanceButtons = true
|
|
21
|
+
export let onAcceptanceNavigate: ((requestId: string) => void) | undefined = undefined
|
|
22
|
+
|
|
23
|
+
$: statusInfo = getAppRequestStatusInfo(request.status, request.phase, request.closedAt)
|
|
24
|
+
$: firstInvalidatedPrompt = request.applications
|
|
25
|
+
.flatMap(app => app.requirements)
|
|
26
|
+
.flatMap(req => req.prompts)
|
|
27
|
+
.find(p => p.visibility === enumPromptVisibility.AVAILABLE && !p.moot && p.invalidated && p.invalidatedReason)
|
|
28
|
+
$: navButton = firstInvalidatedPrompt
|
|
29
|
+
? { label: 'Make corrections', href: `/requests/${request.id}/apply/${firstInvalidatedPrompt.id}` }
|
|
30
|
+
: getNavigationButton(request.status, request.id)
|
|
31
|
+
|
|
32
|
+
async function handleAcceptanceClick () {
|
|
33
|
+
if (onAcceptanceNavigate) {
|
|
34
|
+
onAcceptanceNavigate(request.id)
|
|
35
|
+
} else {
|
|
36
|
+
await goto(resolve(`/requests/${request.id}/apply`))
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<Card
|
|
42
|
+
title={request.period.name}
|
|
43
|
+
subhead={`Started: ${longNumericTime(request.createdAt)}`}
|
|
44
|
+
tags={statusInfo.tags}
|
|
45
|
+
tagsInBody
|
|
46
|
+
{actions}
|
|
47
|
+
forceOverflow={true}
|
|
48
|
+
navigations={navButton ? [navButton] : []}>
|
|
49
|
+
|
|
50
|
+
<!-- Status description -->
|
|
51
|
+
{#if statusInfo.description}
|
|
52
|
+
<p class="pl-1.5 mb-4 text-sm">{statusInfo.description}</p>
|
|
53
|
+
{/if}
|
|
54
|
+
|
|
55
|
+
<!-- Benefits section -->
|
|
56
|
+
<div class="rounded">
|
|
57
|
+
<h4 class="m-0 mb-2 text-lg benefits-title">Potential benefits</h4>
|
|
58
|
+
<p class="m-0 mb-3 benefits-subtitle text-sm">Benefit results will be finalized once the {uiRegistry.getWord('appRequest').toLowerCase()} submission and review is completed.</p>
|
|
59
|
+
|
|
60
|
+
{#if request.applications.length > 0}
|
|
61
|
+
{#each request.applications as application (application.id)}
|
|
62
|
+
{@const invalidatedPrompts = application.requirements
|
|
63
|
+
.flatMap(req => req.prompts)
|
|
64
|
+
.filter(p => p.visibility === enumPromptVisibility.AVAILABLE && !p.moot && p.invalidated && p.invalidatedReason)
|
|
65
|
+
}
|
|
66
|
+
{@const warningReqs = application.requirements.filter(r => r.status === 'WARNING' && r.statusReason)}
|
|
67
|
+
{@const appStatusTags = invalidatedPrompts.length > 0
|
|
68
|
+
? [{ label: 'Needs corrections', type: 'magenta' as const }]
|
|
69
|
+
: getApplicationStatusInfo(application.status, request.phase, request.closedAt, application.rescindedStatus).map(info => ({ label: info.label, type: info.color }))}
|
|
70
|
+
<div class="program-status py-2 px-4 mb-4">
|
|
71
|
+
<div class="flex items-center">
|
|
72
|
+
<span class="font-medium">{application.title}</span>
|
|
73
|
+
<div class="tagwrap">
|
|
74
|
+
<TagSet tags={appStatusTags} />
|
|
75
|
+
</div>
|
|
76
|
+
{#if (application.status === 'PENDING' || application.status === 'ELIGIBLE') && warningReqs.length > 0}
|
|
77
|
+
<WarningIconYellow size={20} />
|
|
78
|
+
{/if}
|
|
79
|
+
|
|
80
|
+
<!-- Acceptance buttons -->
|
|
81
|
+
{#if showAcceptanceButtons && (request.status === 'ACCEPTANCE' || request.status === 'READY_TO_ACCEPT') && application.status === 'ELIGIBLE'}
|
|
82
|
+
<Button kind="primary" size="small" class="ml-auto"
|
|
83
|
+
on:click={handleAcceptanceClick}>
|
|
84
|
+
{request.status === 'READY_TO_ACCEPT' ? 'Accept Offer' : 'Review Offer'}
|
|
85
|
+
</Button>
|
|
86
|
+
{/if}
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<!-- Status reason -->
|
|
90
|
+
{#if application.statusReason && application.status !== 'INELIGIBLE' && invalidatedPrompts.length === 0}
|
|
91
|
+
<p class="status-reason mt-2 mb-0 text-sm">{application.statusReason}</p>
|
|
92
|
+
{/if}
|
|
93
|
+
|
|
94
|
+
<!-- Warnings for PENDING/ELIGIBLE applications -->
|
|
95
|
+
{#if (application.status === 'PENDING' || application.status === 'ELIGIBLE') && warningReqs.length > 0}
|
|
96
|
+
<StatusMessageList
|
|
97
|
+
items={warningReqs.map(r => ({ id: r.id, message: r.statusReason! }))}
|
|
98
|
+
variant="warning"
|
|
99
|
+
accordionTitle="Multiple warnings" />
|
|
100
|
+
{/if}
|
|
101
|
+
|
|
102
|
+
<!-- Failed requirements for INELIGIBLE applications -->
|
|
103
|
+
{#if application.status === 'INELIGIBLE' && application.requirements}
|
|
104
|
+
{@const failedRequirements = application.requirements.filter(req => req.status === 'DISQUALIFYING' && req.statusReason)}
|
|
105
|
+
<StatusMessageList
|
|
106
|
+
items={failedRequirements.map(r => ({ id: r.id, message: r.statusReason! }))}
|
|
107
|
+
accordionTitle="Multiple eligibility issues" />
|
|
108
|
+
{/if}
|
|
109
|
+
|
|
110
|
+
<!-- Corrections needed for non-INELIGIBLE applications -->
|
|
111
|
+
{#if invalidatedPrompts.length > 0}
|
|
112
|
+
<StatusMessageList
|
|
113
|
+
items={invalidatedPrompts.map(p => ({ id: p.id, message: p.invalidatedReason! }))}
|
|
114
|
+
accordionTitle="Multiple corrections needed" />
|
|
115
|
+
{/if}
|
|
116
|
+
</div>
|
|
117
|
+
{/each}
|
|
118
|
+
{:else}
|
|
119
|
+
<p>No benefits associated with this {uiRegistry.getWord('appRequest').toLowerCase()}.</p>
|
|
120
|
+
{/if}
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<!-- Footer info grid -->
|
|
124
|
+
<div class="mt-4 grid grid-cols-3 footer-grid">
|
|
125
|
+
<div class="text-center py-4 px-2 footer-section">
|
|
126
|
+
<span class="block text-sm font-semibold footer-label mb-1">Request number:</span>
|
|
127
|
+
<span class="block text-sm footer-value">{request.id.slice(0, 6).toUpperCase()}</span>
|
|
128
|
+
</div>
|
|
129
|
+
<div class="text-center py-4 px-2 footer-section">
|
|
130
|
+
<span class="block text-sm font-semibold footer-label mb-1">Last updated:</span>
|
|
131
|
+
<span class="block text-sm footer-value">{longNumericTime(request.updatedAt)}</span>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="text-center py-4 px-2 footer-section">
|
|
134
|
+
<span class="block text-sm font-semibold footer-label mb-1">Waiting on:</span>
|
|
135
|
+
<span class="block text-sm footer-value">{statusInfo.waitingOn}</span>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</Card>
|
|
139
|
+
|
|
140
|
+
<style>
|
|
141
|
+
/* Global styles for external component overrides */
|
|
142
|
+
:global(.tagwrap .tag-set > *) {
|
|
143
|
+
max-width: 100% !important;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.benefits-subtitle {
|
|
147
|
+
color: var(--cds-text-02);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.program-status {
|
|
151
|
+
background-color: var(--cds-ui-01);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.footer-grid {
|
|
155
|
+
background-color: var(--cds-ui-01);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.footer-section {
|
|
159
|
+
border-right: 1px solid var(--cds-ui-05);
|
|
160
|
+
|
|
161
|
+
}
|
|
162
|
+
.footer-section:last-child {
|
|
163
|
+
border-right: none;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.footer-label {
|
|
167
|
+
color: var(--cds-text-01);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.footer-value {
|
|
171
|
+
color: var(--cds-text-01);
|
|
172
|
+
}
|
|
173
|
+
</style>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Form, PanelFormDialog } from '@txstate-mws/carbon-svelte'
|
|
3
|
+
import type { FormStore } from '@txstate-mws/svelte-forms'
|
|
4
|
+
import { afterNavigate, invalidate, invalidateAll } from '$app/navigation'
|
|
5
|
+
import { uiRegistry } from '$local'
|
|
6
|
+
import { api, type PromptForEditing } from '../api.js'
|
|
7
|
+
import { Loading } from "carbon-components-svelte";
|
|
8
|
+
import MissingDefinitionNotification from './MissingDefinitionNotification.svelte'
|
|
9
|
+
import type { AppRequestForDetails, OptOutApplication } from '@reqquest/ui'
|
|
10
|
+
|
|
11
|
+
export let open = false
|
|
12
|
+
export let optIn = false
|
|
13
|
+
export let prompt: PromptForEditing
|
|
14
|
+
export let appRequest: AppRequestForDetails
|
|
15
|
+
export let optOutSelected: OptOutApplication | undefined
|
|
16
|
+
|
|
17
|
+
$: def = uiRegistry.getPrompt(prompt.key)
|
|
18
|
+
let loading = false
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
let store: FormStore | undefined
|
|
22
|
+
|
|
23
|
+
async function submit (data: any) {
|
|
24
|
+
loading = true
|
|
25
|
+
try {
|
|
26
|
+
const { success, messages, data: newData } = await api.updatePrompt(prompt.id, data, false)
|
|
27
|
+
if (!success) loading = false
|
|
28
|
+
return { success, messages, data: newData?.[prompt.key] }
|
|
29
|
+
} catch (e) {
|
|
30
|
+
loading = false
|
|
31
|
+
throw e
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function saved () {
|
|
36
|
+
open = false
|
|
37
|
+
loading = false
|
|
38
|
+
invalidateAll()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function onValidate (data: any) {
|
|
42
|
+
const { messages } = await api.updatePrompt(prompt.id, data, true)
|
|
43
|
+
return messages
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let lastPromptId: string | undefined
|
|
47
|
+
$: if (prompt.id !== lastPromptId) {
|
|
48
|
+
lastPromptId = prompt.id
|
|
49
|
+
store = undefined
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
{#if loading}
|
|
54
|
+
<Loading />
|
|
55
|
+
{/if}
|
|
56
|
+
|
|
57
|
+
<PanelFormDialog
|
|
58
|
+
let:data
|
|
59
|
+
bind:store
|
|
60
|
+
centered
|
|
61
|
+
open={open}
|
|
62
|
+
on:cancel={() => { open = false }}
|
|
63
|
+
validate={onValidate}
|
|
64
|
+
on:saved={saved}
|
|
65
|
+
disableSaveUntilChanged={true}
|
|
66
|
+
{submit}
|
|
67
|
+
title={`${optIn ? 'Opt in to' : 'Opt out of'} ${optOutSelected?.title}?`}
|
|
68
|
+
submitText={optIn ? 'Opt in' : 'Opt out'}
|
|
69
|
+
cancelText="Cancel"
|
|
70
|
+
preload={prompt.preloadData}
|
|
71
|
+
preloadAsDraft={!prompt.hasSavedData}
|
|
72
|
+
>
|
|
73
|
+
{#if def?.formComponent == null}
|
|
74
|
+
<MissingDefinitionNotification kind="prompt" definitionKey={prompt.key} />
|
|
75
|
+
{:else}
|
|
76
|
+
<svelte:component this={def.formComponent} {data} appRequestId={appRequest.id} appRequestData={appRequest.data} prestageData={{latest: prompt.prestageData, current: appRequest.data[prompt.key]?.__prestage}} fetched={prompt.fetchedData} configData={prompt.configurationData} gatheredConfigData={prompt.gatheredConfigData} invalidated={prompt.invalidated} invalidatedReason={prompt.invalidatedReason} statusReasons={optOutSelected?.prompt.statusReasons ?? []} />
|
|
77
|
+
{/if}
|
|
78
|
+
</PanelFormDialog>
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { TagSet } from '@txstate-mws/carbon-svelte'
|
|
3
|
+
import { Button } from 'carbon-components-svelte'
|
|
4
|
+
import { Close, InProgress, CheckmarkFilled, Information, SubtractAlt } from 'carbon-icons-svelte'
|
|
5
|
+
import { isNotBlank } from 'txstate-utils'
|
|
6
|
+
import { type ApplicationForDetails, type AppRequestForDetails, enumApplicationStatus, enumIneligiblePhases, enumPromptVisibility, enumRequirementStatus, enumRequirementType, type OptOutApplication } from '@reqquest/ui'
|
|
7
|
+
import { getApplicationStatusTags } from '../status-utils.js'
|
|
8
|
+
import { isIneligibleByApplicantRequirement } from '../appreq-utils.js'
|
|
9
|
+
import ApplicantProgramListTooltip from './ApplicantProgramListTooltip.svelte'
|
|
10
|
+
import WarningIconYellow from './WarningIconYellow.svelte'
|
|
11
|
+
import { api, type PromptForEditing } from '$internal/api.js'
|
|
12
|
+
import ApplicantOptOutModal from './ApplicantOptOutModal.svelte'
|
|
13
|
+
|
|
14
|
+
export let appRequest: AppRequestForDetails
|
|
15
|
+
export let applications: ApplicationForDetails[]
|
|
16
|
+
export let viewMode = false
|
|
17
|
+
export let showTooltipsAsText = false
|
|
18
|
+
export let promptsById: Record<string, any> = {}
|
|
19
|
+
|
|
20
|
+
let open = false
|
|
21
|
+
let optIn = false
|
|
22
|
+
let optOutPrompt: PromptForEditing | undefined
|
|
23
|
+
let optOutSelected: OptOutApplication | undefined
|
|
24
|
+
|
|
25
|
+
async function openOptOutModal (programId: string, openOptIn = false) {
|
|
26
|
+
optOutSelected = optOutPrograms[programId]
|
|
27
|
+
await loadOptOutPrompt()
|
|
28
|
+
optIn = openOptIn
|
|
29
|
+
open = true
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
$: promptsByApplicationId = applications.reduce<Record<string, typeof applications[0]['requirements'][0]['prompts'] | undefined>>((acc, curr) => ({
|
|
33
|
+
...acc,
|
|
34
|
+
[curr.id]: curr.requirements
|
|
35
|
+
.filter(r => r.type === enumRequirementType.QUALIFICATION)
|
|
36
|
+
.flatMap(r => r.prompts)
|
|
37
|
+
}), {})
|
|
38
|
+
|
|
39
|
+
$: programButtonStatus = applications.reduce((acc, curr) => {
|
|
40
|
+
let status: string
|
|
41
|
+
if (curr.completionStatus === enumApplicationStatus.PENDING) {
|
|
42
|
+
const unanswered = curr.requirements
|
|
43
|
+
.filter(r => r.type === enumRequirementType.QUALIFICATION)
|
|
44
|
+
.flatMap(r => r.prompts)
|
|
45
|
+
.filter(p => (!p.answered || p.invalidated) && !p.optOut)
|
|
46
|
+
if (!curr.requirements.some(r => r.prompts.some(p => p.answered && !p.invalidated && !p.optOut))) status = 'start'
|
|
47
|
+
else if (unanswered.length === 0) status = 'complete'
|
|
48
|
+
else if (unanswered.some(p => p.visibility === enumPromptVisibility.AVAILABLE)) status = 'continue'
|
|
49
|
+
// every remaining unanswered prompt is owned by an earlier program, so continuing here shows nothing new
|
|
50
|
+
else status = 'revisitPending'
|
|
51
|
+
} else if (curr.completionStatus === enumApplicationStatus.INELIGIBLE) {
|
|
52
|
+
status = curr.ineligiblePhase === enumIneligiblePhases.PREQUAL ? 'ineligible' : 'revisit'
|
|
53
|
+
} else status = 'complete'
|
|
54
|
+
return { ...acc, [curr.id]: status }
|
|
55
|
+
}, {} as Record<string, string>)
|
|
56
|
+
|
|
57
|
+
const programButtonLabels: Record<string, string> = {
|
|
58
|
+
start: 'Start',
|
|
59
|
+
continue: 'Continue',
|
|
60
|
+
complete: 'Revisit',
|
|
61
|
+
revisit: 'Revisit',
|
|
62
|
+
revisitPending: 'Revisit'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
$: programFirstPromptId = applications.reduce((acc, curr) => ({
|
|
66
|
+
...acc,
|
|
67
|
+
[curr.id]: (promptsByApplicationId[curr.id]?.find(p => (!p.answered || p.invalidated) && !p.optOut) ?? (promptsByApplicationId[curr.id]?.[0]?.optOut ? promptsByApplicationId[curr.id]?.[1] : promptsByApplicationId[curr.id]?.[0]))?.id
|
|
68
|
+
}), {} as Record<string, string | undefined>)
|
|
69
|
+
|
|
70
|
+
$: optOutPrograms = applications.reduce((acc, curr) => {
|
|
71
|
+
const optOut = curr.requirements.flatMap(r => r.prompts).find(({ optOut: optOutPrompt }) => optOutPrompt)
|
|
72
|
+
|
|
73
|
+
if (!optOut) return acc
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
...acc,
|
|
77
|
+
[curr.id]: {
|
|
78
|
+
...curr,
|
|
79
|
+
prompt: optOut
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}, {} as Record<string, OptOutApplication | undefined>)
|
|
83
|
+
|
|
84
|
+
$: optedOutPrograms = applications
|
|
85
|
+
.filter(curr => curr.requirements.flatMap(r => r.prompts).find(r => r.optOut))
|
|
86
|
+
.reduce((acc, c) => {
|
|
87
|
+
const optOutRequirement = c.requirements.find(r => r.prompts.some(p => p.optOut))
|
|
88
|
+
return {
|
|
89
|
+
...acc,
|
|
90
|
+
[c.id]: optOutRequirement?.status === enumRequirementStatus.DISQUALIFYING
|
|
91
|
+
}
|
|
92
|
+
}, {} as Record<string, boolean>)
|
|
93
|
+
|
|
94
|
+
async function loadOptOutPrompt () {
|
|
95
|
+
const promptId = optOutSelected?.prompt?.id
|
|
96
|
+
if (!promptId) return
|
|
97
|
+
const { prompt } = await api.getApplicantPrompt(appRequest.id, promptId)
|
|
98
|
+
optOutPrompt = prompt
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
</script>
|
|
102
|
+
|
|
103
|
+
<section class="programs-container">
|
|
104
|
+
<header>
|
|
105
|
+
<div class="program column">Program</div>
|
|
106
|
+
<div class="status column">Status</div>
|
|
107
|
+
</header>
|
|
108
|
+
{#each applications as application (application.id)}
|
|
109
|
+
{@const programStatus = programButtonStatus[application.id]}
|
|
110
|
+
{@const programFirstPrompt = programFirstPromptId[application.id]}
|
|
111
|
+
{@const programDescriptions = isIneligibleByApplicantRequirement(application) && !optedOutPrograms[application.id] ? [application.applicantDescription, application.eligibilityDescription].filter(isNotBlank) : []}
|
|
112
|
+
<div class="program column [ flex-col ]" style='align-items: start;'>
|
|
113
|
+
<span>{application.title}</span>
|
|
114
|
+
{#if !viewMode}
|
|
115
|
+
{#if optedOutPrograms[application.id]}
|
|
116
|
+
<Button on:click={() => openOptOutModal(application.id, true)} kind='ghost' style='padding: 0; min-height: 0;' class='[ p-0 justify-start ]'>Opt In</Button>
|
|
117
|
+
{:else if optOutPrograms[application.id] && application.completionStatus !== enumApplicationStatus.INELIGIBLE}
|
|
118
|
+
<Button on:click={() => openOptOutModal(application.id)} kind='ghost' style='padding: 0; min-height: 0;' class='[ p-0 justify-start ]'>Opt out</Button>
|
|
119
|
+
{/if}
|
|
120
|
+
{/if}
|
|
121
|
+
</div>
|
|
122
|
+
<div class="status column" class:no-tooltip={!application.statusReason?.length && !programDescriptions.length}>
|
|
123
|
+
{#if !viewMode}
|
|
124
|
+
<div class="icon-and-tooltip" class:wide-icon={application.completionStatus === enumApplicationStatus.INELIGIBLE}>
|
|
125
|
+
{#if optedOutPrograms[application.id]}
|
|
126
|
+
<SubtractAlt size={24} fill='#dd3b46'/>
|
|
127
|
+
{:else if application.completionStatus === enumApplicationStatus.INELIGIBLE}
|
|
128
|
+
<Close size={32} class="status-icon-ineligible" />
|
|
129
|
+
{:else if ['start', 'continue', 'revisitPending'].includes(programStatus)}
|
|
130
|
+
<InProgress size={24} class="status-icon-pending" />
|
|
131
|
+
{:else if application.hasWarning}
|
|
132
|
+
<WarningIconYellow size={24} class="status-icon-warning" />
|
|
133
|
+
{:else}
|
|
134
|
+
<CheckmarkFilled size={24} class="status-icon-complete" />
|
|
135
|
+
{/if}
|
|
136
|
+
<ApplicantProgramListTooltip {optedOutPrograms} {application} />
|
|
137
|
+
</div>
|
|
138
|
+
{#if optedOutPrograms[application.id]}
|
|
139
|
+
<p>Opted out</p>
|
|
140
|
+
{:else if programFirstPrompt && programStatus !== 'ineligible'}
|
|
141
|
+
<Button size="small" kind={programStatus === 'complete' ? 'ghost' : ['revisit', 'revisitPending'].includes(programStatus) ? 'secondary' : 'primary'} href={programFirstPrompt}>{programButtonLabels[programStatus]}</Button>
|
|
142
|
+
{/if}
|
|
143
|
+
{:else}
|
|
144
|
+
{#if ['start', 'continue', 'complete', 'revisitPending'].includes(programStatus) && !optedOutPrograms[application.id]}
|
|
145
|
+
<TagSet tags={getApplicationStatusTags(application.status, appRequest.phase, appRequest.closedAt, application.rescindedStatus)} />
|
|
146
|
+
{:else if optedOutPrograms[application.id]}
|
|
147
|
+
<SubtractAlt size={24} fill='#dd3b46'/><p>Opted out</p>
|
|
148
|
+
{:else}
|
|
149
|
+
<Close size={32} class="status-icon-ineligible" />
|
|
150
|
+
{/if}
|
|
151
|
+
<ApplicantProgramListTooltip {optedOutPrograms} {application} />
|
|
152
|
+
{/if}
|
|
153
|
+
</div>
|
|
154
|
+
{#if application.warningReasons.length || application.ineligibleReasons.length || programDescriptions.length}
|
|
155
|
+
<div class="tooltip-text-row" class:visible={showTooltipsAsText}>
|
|
156
|
+
{#each application.ineligibleReasons as reason (reason)}
|
|
157
|
+
<div class="tooltip-text-item">
|
|
158
|
+
<Information size={16} /> {reason}
|
|
159
|
+
</div>
|
|
160
|
+
{/each}
|
|
161
|
+
{#each programDescriptions as description (description)}
|
|
162
|
+
<div class="tooltip-text-item">
|
|
163
|
+
<Information size={16} /> {description}
|
|
164
|
+
</div>
|
|
165
|
+
{/each}
|
|
166
|
+
{#each application.warningReasons as reason (reason)}
|
|
167
|
+
<div class="tooltip-text-item">
|
|
168
|
+
<Information size={16} /> {reason}
|
|
169
|
+
</div>
|
|
170
|
+
{/each}
|
|
171
|
+
</div>
|
|
172
|
+
{/if}
|
|
173
|
+
{/each}
|
|
174
|
+
</section>
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
{#if optOutPrompt?.key}
|
|
178
|
+
<ApplicantOptOutModal bind:open bind:optIn bind:prompt={optOutPrompt} {optOutSelected} {appRequest} />
|
|
179
|
+
{/if}
|
|
180
|
+
|
|
181
|
+
<style>
|
|
182
|
+
.programs-container {
|
|
183
|
+
display: grid;
|
|
184
|
+
grid-template-columns: 2fr 1fr;
|
|
185
|
+
margin-bottom: 1.5rem;
|
|
186
|
+
background-color: #F2F2F2;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.programs-container header {
|
|
190
|
+
display: contents;
|
|
191
|
+
font-weight: bold;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.programs-container header .column {
|
|
195
|
+
padding: 0.75rem 10px;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.programs-container .column {
|
|
199
|
+
border-bottom: 1px solid var(--cds-ui-03, #e0e0e0);
|
|
200
|
+
padding: .5rem 10px;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.programs-container :global(.status-column) {
|
|
204
|
+
flex-grow: 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
:not(.header) .program.column {
|
|
208
|
+
display: flex;
|
|
209
|
+
align-items: center;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
:not(.header) .status.column {
|
|
213
|
+
display: flex;
|
|
214
|
+
flex-wrap: wrap;
|
|
215
|
+
align-items: center;
|
|
216
|
+
justify-content: flex-start;
|
|
217
|
+
gap: 12px;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.status.column :global(.reason-tooltip) {
|
|
221
|
+
height: 16px;
|
|
222
|
+
}
|
|
223
|
+
:not(.header) .status.column.no-tooltip {
|
|
224
|
+
flex-wrap: nowrap
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.programs-container :global(.status-icon-pending) {
|
|
228
|
+
fill: var(--cds-support-04);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.programs-container :global(.status-icon-complete) {
|
|
232
|
+
fill: var(--cds-support-04);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.programs-container :global(.status-icon-ineligible) {
|
|
236
|
+
margin-left: -4px;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.icon-and-tooltip {
|
|
240
|
+
display: flex;
|
|
241
|
+
align-items: center;
|
|
242
|
+
gap: 8px;
|
|
243
|
+
}
|
|
244
|
+
.icon-and-tooltip.wide-icon {
|
|
245
|
+
gap: 4px;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.tooltip-text-row {
|
|
249
|
+
grid-column: span 2;
|
|
250
|
+
background-color: #FBF1DA;
|
|
251
|
+
padding: 0.5rem 1rem;
|
|
252
|
+
display: none;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.tooltip-text-row.visible {
|
|
256
|
+
display: block;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.tooltip-text-item {
|
|
260
|
+
display: flex;
|
|
261
|
+
align-items: center;
|
|
262
|
+
gap: 0.5rem;
|
|
263
|
+
padding: 0.125rem 16px;
|
|
264
|
+
color: var(--cds-text-01);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
@media print {
|
|
268
|
+
.tooltip-text-row {
|
|
269
|
+
display: block !important;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
:global(.opt-out-modal .bx--modal-content) {
|
|
274
|
+
margin-bottom: 1rem;
|
|
275
|
+
}
|
|
276
|
+
</style>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Tooltip } from 'carbon-components-svelte'
|
|
3
|
+
import { isNotBlank } from 'txstate-utils'
|
|
4
|
+
import type { ApplicationForDetails } from '@reqquest/ui'
|
|
5
|
+
import { isIneligibleByApplicantRequirement } from '../appreq-utils.js'
|
|
6
|
+
|
|
7
|
+
export let application: ApplicationForDetails
|
|
8
|
+
export let optedOutPrograms: Record<string, boolean>
|
|
9
|
+
|
|
10
|
+
// Programs that died before submission get extra context above the failing reason: what the
|
|
11
|
+
// program is and a summary of what it would have required. An applicant disqualified early may
|
|
12
|
+
// never have seen the program's prompts, so the statusReason alone lacks context.
|
|
13
|
+
$: programDescriptions = isIneligibleByApplicantRequirement(application) && !optedOutPrograms[application.id]
|
|
14
|
+
? [application.applicantDescription, application.eligibilityDescription].filter(isNotBlank)
|
|
15
|
+
: []
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
{#if application.warningReasons.length || application.ineligibleReasons.length || application.metReasons.length || programDescriptions.length}
|
|
19
|
+
<Tooltip align="end" direction="bottom" triggerText="" class="reason-tooltip">
|
|
20
|
+
{#if application.ineligibleReasons.length || programDescriptions.length}
|
|
21
|
+
{#each application.ineligibleReasons as reason (reason)}
|
|
22
|
+
<p>{reason}</p>
|
|
23
|
+
{/each}
|
|
24
|
+
{#each programDescriptions as description (description)}
|
|
25
|
+
<p class="program-description">{description}</p>
|
|
26
|
+
{/each}
|
|
27
|
+
{:else if application.warningReasons.length}
|
|
28
|
+
<p><strong>Warnings:</strong></p>
|
|
29
|
+
{#each application.warningReasons as reason (reason)}
|
|
30
|
+
<p>{reason}</p>
|
|
31
|
+
{/each}
|
|
32
|
+
{:else if application.metReasons.length}
|
|
33
|
+
{#each application.metReasons as reason (reason)}
|
|
34
|
+
<p>{reason}</p>
|
|
35
|
+
{/each}
|
|
36
|
+
{/if}
|
|
37
|
+
</Tooltip>
|
|
38
|
+
{/if}
|
|
39
|
+
|
|
40
|
+
<style>
|
|
41
|
+
.program-description {
|
|
42
|
+
margin-top: 0.75rem;
|
|
43
|
+
}
|
|
44
|
+
</style>
|