@reqquest/ui 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +8 -0
  2. package/dist/api.js +656 -111
  3. package/dist/components/AppRequestCard.svelte +172 -0
  4. package/dist/components/ApplicantProgramList.svelte +184 -0
  5. package/dist/components/ApplicantProgramListTooltip.svelte +22 -0
  6. package/dist/components/ApplicantPromptPage.svelte +88 -0
  7. package/dist/components/ApplicationDetailsView.svelte +307 -0
  8. package/dist/components/ButtonLoadingIcon.svelte +2 -1
  9. package/dist/components/FieldCardCheckbox.svelte +328 -0
  10. package/dist/components/FieldCardRadio.svelte +320 -0
  11. package/dist/components/IntroPanel.svelte +41 -0
  12. package/dist/components/PeriodPanel.svelte +100 -0
  13. package/dist/components/QuestionnairePrompt.svelte +36 -0
  14. package/dist/components/RenderDisplayComponent.svelte +38 -0
  15. package/dist/components/ReviewerList.svelte +93 -0
  16. package/dist/components/StatusMessageList.svelte +35 -0
  17. package/dist/components/WarningIconYellow.svelte +20 -0
  18. package/dist/components/index.js +11 -0
  19. package/dist/components/types.js +1 -0
  20. package/dist/csv.js +21 -0
  21. package/dist/index.js +2 -0
  22. package/dist/status-utils.js +343 -0
  23. package/dist/stores/IStateStore.js +0 -1
  24. package/dist/typed-client/schema.graphql +564 -124
  25. package/dist/typed-client/schema.js +87 -23
  26. package/dist/typed-client/types.js +919 -454
  27. package/dist/util.js +12 -1
  28. package/package.json +39 -40
  29. package/dist/api.d.ts +0 -595
  30. package/dist/components/ButtonLoadingIcon.svelte.d.ts +0 -18
  31. package/dist/components/index.d.ts +0 -1
  32. package/dist/index.d.ts +0 -4
  33. package/dist/registry.d.ts +0 -138
  34. package/dist/stores/IStateStore.d.ts +0 -5
  35. package/dist/typed-client/index.d.ts +0 -25
  36. package/dist/typed-client/runtime/batcher.d.ts +0 -105
  37. package/dist/typed-client/runtime/createClient.d.ts +0 -17
  38. package/dist/typed-client/runtime/error.d.ts +0 -18
  39. package/dist/typed-client/runtime/fetcher.d.ts +0 -10
  40. package/dist/typed-client/runtime/generateGraphqlOperation.d.ts +0 -30
  41. package/dist/typed-client/runtime/index.d.ts +0 -11
  42. package/dist/typed-client/runtime/linkTypeMap.d.ts +0 -9
  43. package/dist/typed-client/runtime/typeSelection.d.ts +0 -28
  44. package/dist/typed-client/runtime/types.d.ts +0 -55
  45. package/dist/typed-client/schema.d.ts +0 -1483
  46. package/dist/typed-client/types.d.ts +0 -540
  47. package/dist/util.d.ts +0 -2
@@ -0,0 +1,172 @@
1
+ <script lang="ts">
2
+ import { goto } from '$app/navigation'
3
+ import { resolve } from '$app/paths'
4
+ import { getApplicationStatusInfo, getAppRequestStatusInfo, getNavigationButton } from '../status-utils.js'
5
+ import { longNumericTime } from '../util.js'
6
+ import type { ActionItem } from '@txstate-mws/carbon-svelte'
7
+ import { Card, TagSet } from '@txstate-mws/carbon-svelte'
8
+ import Button from "carbon-components-svelte/src/Button/Button.svelte";
9
+ import type { PageData } from '../../routes/dashboards/applicant/$types'
10
+ import StatusMessageList from './StatusMessageList.svelte'
11
+ import WarningIconYellow from './WarningIconYellow.svelte'
12
+ import { enumPromptVisibility } from '../typed-client'
13
+
14
+ // Type for the partial AppRequest data passed from dashboard
15
+ type DashboardAppRequest = PageData['appRequests'][number]
16
+
17
+ export let request: DashboardAppRequest
18
+ export let actions: ActionItem[] = []
19
+ export let showAcceptanceButtons = true
20
+ export let onAcceptanceNavigate: ((requestId: string) => void) | undefined = undefined
21
+
22
+ $: statusInfo = getAppRequestStatusInfo(request.status)
23
+ $: firstInvalidatedPrompt = request.applications
24
+ .flatMap(app => app.requirements)
25
+ .flatMap(req => req.prompts)
26
+ .find(p => p.visibility === enumPromptVisibility.AVAILABLE && p.invalidated && p.invalidatedReason)
27
+ $: navButton = firstInvalidatedPrompt
28
+ ? { label: 'Make corrections', href: `/requests/${request.id}/apply/${firstInvalidatedPrompt.id}` }
29
+ : getNavigationButton(request.status, request.id)
30
+
31
+ async function handleAcceptanceClick () {
32
+ if (onAcceptanceNavigate) {
33
+ onAcceptanceNavigate(request.id)
34
+ } else {
35
+ await goto(resolve(`/requests/${request.id}/apply`))
36
+ }
37
+ }
38
+ </script>
39
+
40
+ <Card
41
+ title={request.period.name}
42
+ subhead={`Started: ${longNumericTime(request.createdAt)}`}
43
+ tags={[{ label: statusInfo.label, type: statusInfo.color }]}
44
+ tagsInBody
45
+ {actions}
46
+ forceOverflow={true}
47
+ navigations={navButton ? [navButton] : []}>
48
+
49
+ <!-- Status description -->
50
+ {#if statusInfo.description}
51
+ <p class="pl-1.5 mb-4 text-sm">{statusInfo.description}</p>
52
+ {/if}
53
+
54
+ <!-- Benefits section -->
55
+ <div class="rounded">
56
+ <h4 class="m-0 mb-2 text-lg benefits-title">Potential benefits</h4>
57
+ <p class="m-0 mb-3 benefits-subtitle text-sm">Benefit results will be finalized once the application submission and review is completed.</p>
58
+
59
+ {#if request.applications.length > 0}
60
+ {#each request.applications as application (application.id)}
61
+ {@const invalidatedPrompts = application.requirements
62
+ .flatMap(req => req.prompts)
63
+ .filter(p => p.visibility === enumPromptVisibility.AVAILABLE && p.invalidated && p.invalidatedReason)
64
+ }
65
+ {@const warningReqs = application.requirements.filter(r => r.status === 'WARNING' && r.statusReason)}
66
+ {@const appStatusTag = invalidatedPrompts.length > 0
67
+ ? { label: 'Needs corrections', color: 'magenta' as const }
68
+ : getApplicationStatusInfo(application.status)}
69
+ <div class="program-status py-2 px-4 mb-4">
70
+ <div class="flex items-center">
71
+ <span class="font-medium">{application.title}</span>
72
+ <div class="tagwrap">
73
+ <TagSet tags={[{ label: appStatusTag.label, type: appStatusTag.color }]} />
74
+ </div>
75
+ {#if (application.status === 'PENDING' || application.status === 'ELIGIBLE') && warningReqs.length > 0}
76
+ <WarningIconYellow size={20} />
77
+ {/if}
78
+
79
+ <!-- Acceptance buttons -->
80
+ {#if showAcceptanceButtons && (request.status === 'ACCEPTANCE' || request.status === 'READY_TO_ACCEPT') && application.status === 'ELIGIBLE'}
81
+ <Button kind="primary" size="small" class="ml-auto"
82
+ on:click={handleAcceptanceClick}>
83
+ {request.status === 'READY_TO_ACCEPT' ? 'Accept Offer' : 'Review Offer'}
84
+ </Button>
85
+ {/if}
86
+ </div>
87
+
88
+ <!-- Status reason -->
89
+ {#if application.statusReason && application.status !== 'INELIGIBLE' && invalidatedPrompts.length === 0}
90
+ <p class="status-reason mt-2 mb-0 text-sm">{application.statusReason}</p>
91
+ {/if}
92
+
93
+ <!-- Warnings for PENDING/ELIGIBLE applications -->
94
+ {#if (application.status === 'PENDING' || application.status === 'ELIGIBLE') && warningReqs.length > 0}
95
+ <StatusMessageList
96
+ items={warningReqs.map(r => ({ id: r.id, message: r.statusReason! }))}
97
+ variant="warning"
98
+ accordionTitle="Multiple warnings" />
99
+ {/if}
100
+
101
+ <!-- Failed requirements for INELIGIBLE applications -->
102
+ {#if application.status === 'INELIGIBLE' && application.requirements}
103
+ {@const failedRequirements = application.requirements.filter(req => req.status === 'DISQUALIFYING' && req.statusReason)}
104
+ <StatusMessageList
105
+ items={failedRequirements.map(r => ({ id: r.id, message: r.statusReason! }))}
106
+ accordionTitle="Multiple eligibility issues" />
107
+ {/if}
108
+
109
+ <!-- Corrections needed for non-INELIGIBLE applications -->
110
+ {#if invalidatedPrompts.length > 0}
111
+ <StatusMessageList
112
+ items={invalidatedPrompts.map(p => ({ id: p.id, message: p.invalidatedReason! }))}
113
+ accordionTitle="Multiple corrections needed" />
114
+ {/if}
115
+ </div>
116
+ {/each}
117
+ {:else}
118
+ <p>No benefits associated with this application.</p>
119
+ {/if}
120
+ </div>
121
+
122
+ <!-- Footer info grid -->
123
+ <div class="mt-4 grid grid-cols-3 footer-grid">
124
+ <div class="text-center py-4 px-2 footer-section">
125
+ <span class="block text-sm font-semibold footer-label mb-1">Request number:</span>
126
+ <span class="block text-sm footer-value">{request.id.slice(0, 6).toUpperCase()}</span>
127
+ </div>
128
+ <div class="text-center py-4 px-2 footer-section">
129
+ <span class="block text-sm font-semibold footer-label mb-1">Last updated:</span>
130
+ <span class="block text-sm footer-value">{longNumericTime(request.updatedAt)}</span>
131
+ </div>
132
+ <div class="text-center py-4 px-2 footer-section">
133
+ <span class="block text-sm font-semibold footer-label mb-1">Waiting on:</span>
134
+ <span class="block text-sm footer-value">{statusInfo.waitingOn}</span>
135
+ </div>
136
+ </div>
137
+ </Card>
138
+
139
+ <style>
140
+ /* Global styles for external component overrides */
141
+ :global(.tagwrap .tag-set > *) {
142
+ max-width: 100% !important;
143
+ }
144
+
145
+ .benefits-subtitle {
146
+ color: var(--cds-text-02);
147
+ }
148
+
149
+ .program-status {
150
+ background-color: var(--cds-ui-01);
151
+ }
152
+
153
+ .footer-grid {
154
+ background-color: var(--cds-ui-01);
155
+ }
156
+
157
+ .footer-section {
158
+ border-right: 1px solid var(--cds-ui-05);
159
+
160
+ }
161
+ .footer-section:last-child {
162
+ border-right: none;
163
+ }
164
+
165
+ .footer-label {
166
+ color: var(--cds-text-01);
167
+ }
168
+
169
+ .footer-value {
170
+ color: var(--cds-text-01);
171
+ }
172
+ </style>
@@ -0,0 +1,184 @@
1
+ <script lang="ts">
2
+ import { TagSet } from '@txstate-mws/carbon-svelte'
3
+ import Button from "carbon-components-svelte/src/Button/Button.svelte";
4
+ import Close from "carbon-icons-svelte/lib/Close.svelte";
5
+ import InProgress from "carbon-icons-svelte/lib/InProgress.svelte";
6
+ import CheckmarkFilled from "carbon-icons-svelte/lib/CheckmarkFilled.svelte";
7
+ import Information from "carbon-icons-svelte/lib/Information.svelte";
8
+ import { ucfirst } from 'txstate-utils'
9
+ import type { ApplicationForDetails } from './types'
10
+ import { enumApplicationStatus, enumIneligiblePhases, enumRequirementType, getApplicationStatusInfo } from '..'
11
+ import ApplicantProgramListTooltip from './ApplicantProgramListTooltip.svelte'
12
+ import WarningIconYellow from './WarningIconYellow.svelte'
13
+
14
+ export let applications: ApplicationForDetails[]
15
+ export let viewMode = false
16
+ export let showTooltipsAsText = false
17
+
18
+ $: promptsByApplicationId = applications.reduce<Record<string, typeof applications[0]['requirements'][0]['prompts'] | undefined>>((acc, curr) => ({
19
+ ...acc,
20
+ [curr.id]: curr.requirements
21
+ .filter(r => r.type === enumRequirementType.QUALIFICATION)
22
+ .flatMap(r => r.prompts)
23
+ }), {})
24
+
25
+ $: programButtonStatus = applications.reduce((acc, curr) => ({
26
+ ...acc,
27
+ [curr.id]: curr.completionStatus === enumApplicationStatus.PENDING
28
+ ? curr.requirements.some(r => r.prompts.some(p => p.answered && !p.invalidated))
29
+ ? curr.requirements.filter(r => r.type === enumRequirementType.QUALIFICATION).every(r => r.prompts.every(p => p.answered && !p.invalidated))
30
+ ? 'complete'
31
+ : 'continue'
32
+ : 'start'
33
+ : curr.completionStatus === enumApplicationStatus.INELIGIBLE
34
+ ? curr.ineligiblePhase === enumIneligiblePhases.PREQUAL
35
+ ? 'ineligible'
36
+ : 'revisit'
37
+ : 'complete'
38
+ }), {})
39
+ $: programFirstPromptId = applications.reduce((acc, curr) => ({
40
+ ...acc,
41
+ [curr.id]: (promptsByApplicationId[curr.id]?.find(p => !p.answered || p.invalidated) ?? promptsByApplicationId[curr.id]?.[0])?.id
42
+ }), {})
43
+ </script>
44
+
45
+ <section class="programs-container">
46
+ <header>
47
+ <div class="program column">Program</div>
48
+ <div class="status column">Eligibility</div>
49
+ </header>
50
+ {#each applications as application (application.id)}
51
+ {@const programStatus = programButtonStatus[application.id]}
52
+ {@const programFirstPrompt = programFirstPromptId[application.id]}
53
+ <div class="program column">{application.title}</div>
54
+ <div class="status column" class:no-tooltip={!application.statusReason?.length}>
55
+ {#if !viewMode}
56
+ <div class="icon-and-tooltip" class:wide-icon={application.completionStatus === enumApplicationStatus.INELIGIBLE}>
57
+ {#if application.completionStatus === enumApplicationStatus.INELIGIBLE}
58
+ <Close size={32} class="status-icon-ineligible" />
59
+ {:else if ['start', 'continue'].includes(programStatus)}
60
+ <InProgress size={24} class="status-icon-pending" />
61
+ {:else if application.hasWarning}
62
+ <WarningIconYellow size={24} class="status-icon-warning" />
63
+ {:else}
64
+ <CheckmarkFilled size={24} class="status-icon-complete" />
65
+ {/if}
66
+ <ApplicantProgramListTooltip {application} />
67
+ </div>
68
+ {#if programFirstPrompt && programStatus !== 'ineligible'}
69
+ <Button size="small" kind={programStatus === 'complete' ? 'ghost' : programStatus === 'revisit' ? 'secondary' : 'primary'} href={programFirstPrompt}>{ucfirst(programStatus)}</Button>
70
+ {/if}
71
+ {:else}
72
+ {@const statusInfo = getApplicationStatusInfo(application.status)}
73
+ <TagSet tags={[{ type: statusInfo.color, label: statusInfo.label }]} />
74
+ <ApplicantProgramListTooltip {application} />
75
+ {/if}
76
+ </div>
77
+ {#if application.warningReasons.length || application.ineligibleReasons.length}
78
+ <div class="tooltip-text-row" class:visible={showTooltipsAsText}>
79
+ {#each application.ineligibleReasons as reason (reason)}
80
+ <div class="tooltip-text-item">
81
+ <Information size={16} /> {reason}
82
+ </div>
83
+ {/each}
84
+ {#each application.warningReasons as reason (reason)}
85
+ <div class="tooltip-text-item">
86
+ <Information size={16} /> {reason}
87
+ </div>
88
+ {/each}
89
+ </div>
90
+ {/if}
91
+ {/each}
92
+ </section>
93
+
94
+ <style>
95
+ .programs-container {
96
+ display: grid;
97
+ grid-template-columns: 2fr 1fr;
98
+ margin-bottom: 1.5rem;
99
+ }
100
+
101
+ .programs-container header {
102
+ display: contents;
103
+ font-weight: bold;
104
+ }
105
+
106
+ .programs-container header .column {
107
+ padding: 0.5rem 8px;
108
+ }
109
+
110
+ .programs-container .column {
111
+ border-bottom: 1px solid var(--cds-ui-03, #e0e0e0);
112
+ padding: 1rem 8px;
113
+ }
114
+
115
+ .programs-container :global(.status-column) {
116
+ flex-grow: 0;
117
+ }
118
+
119
+ :not(.header) .program.column {
120
+ display: flex;
121
+ align-items: center;
122
+ }
123
+
124
+ :not(.header) .status.column {
125
+ display: flex;
126
+ flex-wrap: wrap;
127
+ align-items: center;
128
+ justify-content: flex-start;
129
+ gap: 12px;
130
+ }
131
+
132
+ .status.column :global(.reason-tooltip) {
133
+ height: 16px;
134
+ }
135
+ :not(.header) .status.column.no-tooltip {
136
+ flex-wrap: nowrap
137
+ }
138
+
139
+ .programs-container :global(.status-icon-pending) {
140
+ fill: var(--cds-support-04);
141
+ }
142
+
143
+ .programs-container :global(.status-icon-complete) {
144
+ fill: var(--cds-support-04);
145
+ }
146
+
147
+ .programs-container :global(.status-icon-ineligible) {
148
+ margin-left: -4px;
149
+ }
150
+
151
+ .icon-and-tooltip {
152
+ display: flex;
153
+ align-items: center;
154
+ gap: 8px;
155
+ }
156
+ .icon-and-tooltip.wide-icon {
157
+ gap: 4px;
158
+ }
159
+
160
+ .tooltip-text-row {
161
+ grid-column: span 2;
162
+ background-color: #FBF1DA;
163
+ padding: 0.5rem 1rem;
164
+ display: none;
165
+ }
166
+
167
+ .tooltip-text-row.visible {
168
+ display: block;
169
+ }
170
+
171
+ .tooltip-text-item {
172
+ display: flex;
173
+ align-items: center;
174
+ gap: 0.5rem;
175
+ padding: 0.125rem 16px;
176
+ color: var(--cds-text-01);
177
+ }
178
+
179
+ @media print {
180
+ .tooltip-text-row {
181
+ display: block !important;
182
+ }
183
+ }
184
+ </style>
@@ -0,0 +1,22 @@
1
+ <script lang="ts">
2
+ import Tooltip from "carbon-components-svelte/src/Tooltip/Tooltip.svelte";
3
+ import type { ApplicationForDetails } from './types'
4
+
5
+ export let application: ApplicationForDetails
6
+ </script>
7
+
8
+ {#if application.warningReasons.length || application.ineligibleReasons.length}
9
+ <Tooltip align="end" direction="bottom" triggerText="" class="reason-tooltip">
10
+ {#if application.ineligibleReasons.length}
11
+ <p><strong>Ineligible Because:</strong></p>
12
+ {#each application.ineligibleReasons as reason (reason)}
13
+ <p>{reason}</p>
14
+ {/each}
15
+ {:else if application.warningReasons.length}
16
+ <p><strong>Warnings:</strong></p>
17
+ {#each application.warningReasons as reason (reason)}
18
+ <p>{reason}</p>
19
+ {/each}
20
+ {/if}
21
+ </Tooltip>
22
+ {/if}
@@ -0,0 +1,88 @@
1
+ <script lang="ts">
2
+ /**
3
+ * This page collects prompt data from the applicant. Only one route exists per
4
+ * prompt, even if the prompt is shared between multiple applications. We should
5
+ * be able to figure out which application we are in based on the full state of
6
+ * the app request since the prompt should only be visible in a single spot.
7
+ */
8
+
9
+ import { Form } from '@txstate-mws/carbon-svelte'
10
+ import type { FormStore } from '@txstate-mws/svelte-forms'
11
+ import Button from "carbon-components-svelte/src/Button/Button.svelte";
12
+ import { getContext } from 'svelte'
13
+ import type { Writable } from 'svelte/store'
14
+ import { afterNavigate, beforeNavigate, goto, invalidate } from '$app/navigation'
15
+ import type { ResolvedPathname } from '$app/types'
16
+ import { api, ButtonLoadingIcon } from '..'
17
+ import { uiRegistry } from '../../local/index.js'
18
+ import type { PageData } from '../../routes/requests/[id]/apply/[promptId]/$types.js'
19
+
20
+ export let data: PageData
21
+ $: ({ prompt, appRequestForExport } = data)
22
+ $: def = uiRegistry.getPrompt(prompt.key)
23
+ const nextHref = getContext<Writable<{ nextHref: ResolvedPathname, prevHref: ResolvedPathname | undefined }>>('nextHref')
24
+
25
+ let store: FormStore | undefined
26
+ let continueAfterSave = false
27
+ $: hasPreviousPrompt = $nextHref.prevHref != null
28
+
29
+ async function handleBack () {
30
+ const previousHref = $nextHref.prevHref
31
+ if (previousHref) {
32
+ // eslint-disable-next-line svelte/no-navigation-without-resolve -- already resolved
33
+ await goto(previousHref)
34
+ }
35
+ }
36
+
37
+ async function onSubmit (data: any) {
38
+ const { success, messages } = await api.updatePrompt(prompt.id, data, false, appRequestForExport.dataVersion)
39
+ return {
40
+ success,
41
+ messages,
42
+ data
43
+ }
44
+ }
45
+
46
+ async function onValidate (data: any) {
47
+ const { messages } = await api.updatePrompt(prompt.id, data, true, appRequestForExport.dataVersion)
48
+ return messages
49
+ }
50
+
51
+ async function onSaved () {
52
+ await invalidate('request:apply')
53
+ if (continueAfterSave && prompt.answered) {
54
+ // eslint-disable-next-line svelte/no-navigation-without-resolve -- already resolved
55
+ await goto($nextHref.nextHref)
56
+ } else await store?.setData(appRequestForExport.data[prompt.key] as object)
57
+ }
58
+
59
+ // Remove the form from the DOM when navigating between prompts
60
+ // to make sure state is reset
61
+ let hideForm = false
62
+ beforeNavigate(() => {
63
+ hideForm = true
64
+ })
65
+ afterNavigate(() => {
66
+ hideForm = false
67
+ })
68
+ </script>
69
+
70
+ {#if !hideForm}
71
+ <div class="prompt-intro flow max-w-screen-md mx-auto pt-10 px-6">
72
+ <!-- svelte-ignore a11y_autofocus -->
73
+ <h2 id="prompt-title" tabindex="-1" autofocus class="font-medium text-xl text-center">{prompt.title}</h2>
74
+ <p class="text-center"> {prompt.description}</p>
75
+ </div>
76
+ <Form bind:store hideFallbackMessage submit={onSubmit} validate={onValidate} preload={prompt.preloadData} on:saved={onSaved} let:data>
77
+ <svelte:component this={def!.formComponent} {data} appRequestId={appRequestForExport.id} appRequestData={appRequestForExport.data} fetched={prompt.fetchedData} configData={prompt.configurationData} gatheredConfigData={prompt.gatheredConfigData} />
78
+ <svelte:fragment slot="submit" let:submitting>
79
+ <div class='form-submit flex gap-12 justify-center mt-16'>
80
+ {#if hasPreviousPrompt}
81
+ <Button kind="ghost" on:click={handleBack}>Back</Button>
82
+ {/if}
83
+ <Button icon={submitting && !continueAfterSave ? ButtonLoadingIcon : null} type="submit" kind="secondary" disabled={submitting} on:click={() => { continueAfterSave = false }}>Save</Button>
84
+ <Button icon={submitting && !continueAfterSave ? ButtonLoadingIcon : null} type="submit" disabled={submitting} on:click={() => { continueAfterSave = true }}>Continue</Button>
85
+ </div>
86
+ </svelte:fragment>
87
+ </Form>
88
+ {/if}