@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,14 @@
|
|
|
1
|
+
import { redirect } from '@sveltejs/kit'
|
|
2
|
+
import type { PageLoad } from './$types'
|
|
3
|
+
import { resolve } from '$app/paths'
|
|
4
|
+
|
|
5
|
+
export const load: PageLoad = async ({ parent }) => {
|
|
6
|
+
const { access } = await parent()
|
|
7
|
+
// access is missing only when the layout failed; it renders layoutError in place of the page, so don't mask it
|
|
8
|
+
if (!access) return {} as never
|
|
9
|
+
if (access.viewReviewerInterface) throw redirect(303, resolve('/dashboards/reviewer'))
|
|
10
|
+
if (access.viewAppRequestList) throw redirect(303, resolve('/requests'))
|
|
11
|
+
if (access.viewRoleManagement) throw redirect(303, resolve('/roles'))
|
|
12
|
+
if (access.viewPeriodManagement) throw redirect(303, resolve('/periods'))
|
|
13
|
+
throw redirect(303, resolve('/dashboards/applicant'))
|
|
14
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { api, AnnouncementBanner } from "$internal"
|
|
3
|
+
import type { FormStore } from '@txstate-mws/svelte-forms'
|
|
4
|
+
import IntroPanel from "$internal/components/IntroPanel.svelte"
|
|
5
|
+
import { FieldCheckbox, FieldDateTime, FieldRadio, FieldTextArea, FieldTextInput, FieldToggle, Form, TagSet } from "@txstate-mws/carbon-svelte"
|
|
6
|
+
import type { PageData } from "../announcement/$types"
|
|
7
|
+
import { Button } from 'carbon-components-svelte'
|
|
8
|
+
import { invalidateAll } from "$app/navigation"
|
|
9
|
+
import { toasts } from "@txstate-mws/svelte-components"
|
|
10
|
+
import TextClearFormat from "carbon-icons-svelte/lib/TextClearFormat.svelte";
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
export let data: PageData
|
|
14
|
+
$: ({ anouncement, canManage } = data)
|
|
15
|
+
|
|
16
|
+
let store: FormStore | undefined
|
|
17
|
+
|
|
18
|
+
async function submit (data: any) {
|
|
19
|
+
const { id, addLink, isActive, ...rest } = data
|
|
20
|
+
const { success, messages, data: newData } = id ? await api.updateAnnouncement(id, rest, false) : await api.createAnnouncement(rest, false)
|
|
21
|
+
if (success) toasts.add({ message: 'This announcement has been saved.', title: 'Success', type: 'success' })
|
|
22
|
+
return {
|
|
23
|
+
success,
|
|
24
|
+
messages,
|
|
25
|
+
data: newData
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function validate (data: any) {
|
|
30
|
+
const { id, addLink, isActive, ...rest } = data
|
|
31
|
+
const { messages } = id ? await api.updateAnnouncement(id, rest) : await api.createAnnouncement(rest)
|
|
32
|
+
return messages
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function saved () {
|
|
36
|
+
await invalidateAll()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function reset () {
|
|
40
|
+
if (anouncement?.id) await api.deleteAnnouncement(anouncement.id)
|
|
41
|
+
await invalidateAll()
|
|
42
|
+
store?.reset()
|
|
43
|
+
store?.setField('type', 'toggle')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
$: isActive = anouncement?.isActive ?? false
|
|
47
|
+
|
|
48
|
+
</script>
|
|
49
|
+
<IntroPanel
|
|
50
|
+
title="Applicant dashboard message"
|
|
51
|
+
subtitle='This is a time-sensitive message that will automatically display to applicants when enabled or scheduled.'
|
|
52
|
+
>
|
|
53
|
+
<div class="flex items-center gap-2">
|
|
54
|
+
<TagSet
|
|
55
|
+
tags={isActive ? [{ label: 'Active', type: 'green' }] : [{ label: 'Inactive', type: 'purple' }]}
|
|
56
|
+
tagType="status"
|
|
57
|
+
tagSize="sm"
|
|
58
|
+
/>
|
|
59
|
+
<span class="text-sm">{isActive ? 'This message is currently displaying to applicants.' : 'This message is not currently displaying to applicants.'}</span>
|
|
60
|
+
</div>
|
|
61
|
+
</IntroPanel>
|
|
62
|
+
|
|
63
|
+
<Form
|
|
64
|
+
let:data
|
|
65
|
+
submit={canManage ? submit : undefined}
|
|
66
|
+
validate={canManage ? validate : undefined}
|
|
67
|
+
bind:store
|
|
68
|
+
on:saved={saved}
|
|
69
|
+
hideFallbackMessage
|
|
70
|
+
preload={anouncement ? { ...anouncement, addLink: anouncement?.link || anouncement?.linkText } : undefined}
|
|
71
|
+
>
|
|
72
|
+
<FieldRadio
|
|
73
|
+
path='type'
|
|
74
|
+
required
|
|
75
|
+
disabled={!canManage}
|
|
76
|
+
labelText="Display duration"
|
|
77
|
+
defaultValue='toggle'
|
|
78
|
+
class="md:w-[645px]"
|
|
79
|
+
items={[
|
|
80
|
+
{ label: 'Toggle on and off', value: 'toggle' },
|
|
81
|
+
{ label: 'Date range', value: 'date' }
|
|
82
|
+
]} />
|
|
83
|
+
{#if data.type === 'toggle'}
|
|
84
|
+
<FieldToggle path='enabled' disabled={!canManage} hideLabel labelText='Enable/Disable' labelA='Message inactive' labelB='Message active' />
|
|
85
|
+
{:else}
|
|
86
|
+
<div class="md:w-[645px] flow datetime-full">
|
|
87
|
+
<FieldDateTime path='start' disabled={!canManage} labelText='Start date' helperText='Leave blank to start displaying as soon as it is saved.' defaultTime='24:00'/>
|
|
88
|
+
<FieldDateTime path='end' disabled={!canManage} labelText='End date' helperText='How long the message displays. Leave blank to display indefinitely.' defaultTime='24:00' />
|
|
89
|
+
<div class="flex justify-end mt-0">
|
|
90
|
+
<Button size='small' kind='ghost' disabled={!canManage} on:click={() => { store?.setField('start', undefined); store?.setField('end', undefined)}} icon={TextClearFormat}>Clear dates</Button>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
{/if}
|
|
94
|
+
|
|
95
|
+
<div class="md:w-[645px] flow">
|
|
96
|
+
<FieldTextArea rows={1} required disabled={!canManage} path='subject' labelText='Message title' maxCount={40} class="md:w-[645px] textarea"/>
|
|
97
|
+
<FieldTextArea required disabled={!canManage} path='body' labelText='Message text' maxCount={125} class="md:w-[645px]"/>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<FieldCheckbox path='addLink' disabled={!canManage} labelText='Add link' on:change={(e: any) => {
|
|
101
|
+
if (!e.target.checked) {
|
|
102
|
+
store?.setField('linkText', undefined)
|
|
103
|
+
store?.setField('link', undefined)
|
|
104
|
+
}
|
|
105
|
+
}} />
|
|
106
|
+
|
|
107
|
+
{#if data.addLink}
|
|
108
|
+
<div class="flex flex-col sm:flex-row gap-4 md:w-[645px] addLink">
|
|
109
|
+
<FieldTextArea required disabled={!canManage} class='textarea w-full' maxCount={30} rows={1} path='linkText' labelText='Link text' />
|
|
110
|
+
<FieldTextInput required disabled={!canManage} class='w-full' path='link' labelText='URL' />
|
|
111
|
+
</div>
|
|
112
|
+
{/if}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
{#if data.subject && data.body}
|
|
116
|
+
<div class='w-full'>
|
|
117
|
+
<AnnouncementBanner subject={data.subject} body={data.body} link={data.link} linkText={data.linkText} />
|
|
118
|
+
</div>
|
|
119
|
+
{:else}
|
|
120
|
+
<div class="bg-[var(--cds-ui-01)] p-4 gap-8 md:w-[645px]">
|
|
121
|
+
<p>No message to display</p>
|
|
122
|
+
<p class="text-sm text-[--cds-text-02]">Nothing will be shown to applicants until a message is configured.</p>
|
|
123
|
+
</div>
|
|
124
|
+
{/if}
|
|
125
|
+
|
|
126
|
+
<div slot='submit' class="flex gap-4">
|
|
127
|
+
{#if canManage}
|
|
128
|
+
<Button type='submit'>Save</Button>
|
|
129
|
+
<Button kind='ghost' on:click={reset} icon={TextClearFormat}>Reset</Button>
|
|
130
|
+
{/if}
|
|
131
|
+
</div>
|
|
132
|
+
</Form>
|
|
133
|
+
|
|
134
|
+
<style>
|
|
135
|
+
/* Stretch FieldDateTime (date input + time input + am/pm select) across the full form width. */
|
|
136
|
+
.datetime-full :global(.picker-row) {
|
|
137
|
+
gap: .5rem;
|
|
138
|
+
}
|
|
139
|
+
.datetime-full :global(.picker-row > *) {
|
|
140
|
+
flex-grow: 1;
|
|
141
|
+
width: 100%;
|
|
142
|
+
}
|
|
143
|
+
.datetime-full :global(.bx--text-input__field-wrapper) {
|
|
144
|
+
width: 100% !important;
|
|
145
|
+
}
|
|
146
|
+
.datetime-full :global(.bx--date-picker-input__wrapper),
|
|
147
|
+
.datetime-full :global(.bx--date-picker--single),
|
|
148
|
+
.datetime-full :global(.bx--date-picker-container),
|
|
149
|
+
.datetime-full :global(.flatpickr-wrapper),
|
|
150
|
+
.datetime-full :global(input.bx--date-picker__input),
|
|
151
|
+
.datetime-full :global(.bx--time-picker .bx--select-input),
|
|
152
|
+
.datetime-full :global(.bx--time-picker),
|
|
153
|
+
.datetime-full :global(.bx--time-picker__input),
|
|
154
|
+
.datetime-full :global(.bx--time-picker__input-field) {
|
|
155
|
+
width: 100%;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
:global(.bx--text-area.textarea) {
|
|
159
|
+
resize: none;
|
|
160
|
+
}
|
|
161
|
+
.addLink :global(.cs-field) {
|
|
162
|
+
width: 100%;
|
|
163
|
+
}
|
|
164
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { error } from "@sveltejs/kit";
|
|
2
|
+
import type { PageLoad } from "../$types";
|
|
3
|
+
import { api } from "$internal";
|
|
4
|
+
|
|
5
|
+
export const load: PageLoad = async ({ parent }) => {
|
|
6
|
+
const parentData = await parent()
|
|
7
|
+
// access is missing only when the layout failed; it renders layoutError in place of the page, so don't mask it
|
|
8
|
+
if (!parentData.access) return {} as never
|
|
9
|
+
if (!parentData?.access?.viewAnnouncementManagement) throw error(403)
|
|
10
|
+
const canManage = !!parentData?.access?.manageAnnouncements
|
|
11
|
+
|
|
12
|
+
// no filter: the editor needs the announcement whether or not it is currently displaying
|
|
13
|
+
const anouncement = await api.getAnnouncement()
|
|
14
|
+
|
|
15
|
+
return { anouncement, canManage }
|
|
16
|
+
}
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { goto, invalidate } from '$app/navigation'
|
|
3
|
+
import { resolve } from '$app/paths'
|
|
4
|
+
import type { ApplicationForDetails, AppRequestForDetails, Scalars } from '@reqquest/ui'
|
|
5
|
+
import {
|
|
6
|
+
AnnouncementBanner, api, ApplicationDetailsView, AppRequestCard, IntroPanel, type AppRequestForExportResponse, type DashboardAppRequest,
|
|
7
|
+
getPeriodDisplayInfo, getPeriodStatus, getStatusActionType, getSubmitButtonText
|
|
8
|
+
} from '$internal'
|
|
9
|
+
import { CardGrid, FieldMultiselect, FilterUI, Panel, PanelDialog, Toasts } from '@txstate-mws/carbon-svelte'
|
|
10
|
+
import { toasts } from '@txstate-mws/svelte-components'
|
|
11
|
+
import { Button, Dropdown, InlineNotification, Modal, Tooltip } from 'carbon-components-svelte'
|
|
12
|
+
import Close from 'carbon-icons-svelte/lib/Close.svelte'
|
|
13
|
+
import Reset from 'carbon-icons-svelte/lib/Reset.svelte'
|
|
14
|
+
import DocumentExport from 'carbon-icons-svelte/lib/DocumentExport.svelte'
|
|
15
|
+
import { uiRegistry } from '$local'
|
|
16
|
+
import type { PageData } from './$types'
|
|
17
|
+
|
|
18
|
+
interface FilterState {
|
|
19
|
+
tab?: 'recent_applications' | 'past_applications'
|
|
20
|
+
search?: string
|
|
21
|
+
periodIds?: string[]
|
|
22
|
+
status?: string[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export let data: PageData
|
|
26
|
+
$: ({ appRequests, availablePeriods, availableStatuses, access, openPeriods, recentCutoffIso, recentDays, announcement } = data)
|
|
27
|
+
|
|
28
|
+
let sidePanelOpen = false
|
|
29
|
+
let loading = false
|
|
30
|
+
|
|
31
|
+
// Selected Items
|
|
32
|
+
let selectedAppRequest: AppRequestForDetails | undefined = undefined
|
|
33
|
+
let selectedPeriodId: string | undefined = undefined
|
|
34
|
+
let lastInsertedId: string | undefined
|
|
35
|
+
|
|
36
|
+
// Application Details Data (loaded when side panel opens)
|
|
37
|
+
let appData: Scalars['JsonData'] = {}
|
|
38
|
+
let prequalPrompts: AppRequestForExportResponse['prequalPrompts'] | undefined = undefined
|
|
39
|
+
let postqualPrompts: AppRequestForExportResponse['postqualPrompts'] | undefined = undefined
|
|
40
|
+
let applications: ApplicationForDetails[] = []
|
|
41
|
+
let filterDataSearch: FilterState | undefined
|
|
42
|
+
|
|
43
|
+
// Modal State
|
|
44
|
+
let cancelConfirmation: { open: boolean, requestId: string | null, isWithdraw: boolean } = { open: false, requestId: null, isWithdraw: false }
|
|
45
|
+
|
|
46
|
+
// Auto-select first displayable period
|
|
47
|
+
$: displayablePeriods = openPeriods.length > 0 ? getOpenAndUpcomingPeriods(openPeriods, recentCutoffIso) : []
|
|
48
|
+
$: if (displayablePeriods.length > 0 && !selectedPeriodId) {
|
|
49
|
+
selectedPeriodId = displayablePeriods[0].id
|
|
50
|
+
}
|
|
51
|
+
$: currentPeriod = getCurrentPeriod(displayablePeriods, selectedPeriodId)
|
|
52
|
+
$: periodInfo = currentPeriod ? getPeriodDisplayInfo(currentPeriod) : null
|
|
53
|
+
|
|
54
|
+
// ==========================================
|
|
55
|
+
// Period Management Helper Functions
|
|
56
|
+
// ==========================================
|
|
57
|
+
function getCurrentPeriod (periods: typeof openPeriods, selectedId: string | undefined) {
|
|
58
|
+
if (periods.length === 1) return periods[0]
|
|
59
|
+
if (selectedId) return periods.find(p => p.id === selectedId) ?? null
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getOpenAndUpcomingPeriods (periods: typeof openPeriods, recentCutoffIso: string): typeof openPeriods {
|
|
64
|
+
const recentCutoff = new Date(recentCutoffIso)
|
|
65
|
+
|
|
66
|
+
return periods
|
|
67
|
+
.filter(period => {
|
|
68
|
+
const status = getPeriodStatus(period)
|
|
69
|
+
// Show periods that are open/upcoming AND have been reviewed
|
|
70
|
+
if ((status === 'open' || status === 'upcoming') && period.reviewed) return true
|
|
71
|
+
// Also show closed periods that closed within the recent window
|
|
72
|
+
if (status === 'closed' && period.closeDate) {
|
|
73
|
+
const closeDate = new Date(period.closeDate)
|
|
74
|
+
return closeDate >= recentCutoff && period.reviewed
|
|
75
|
+
}
|
|
76
|
+
return false
|
|
77
|
+
})
|
|
78
|
+
.sort((a, b) => new Date(a.openDate).getTime() - new Date(b.openDate).getTime())
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function showError (message: string) {
|
|
82
|
+
toasts.add(message, 'error', 7000)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isSubmitted (status: string): boolean {
|
|
86
|
+
return status !== 'STARTED' && status !== 'READY_TO_SUBMIT'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ==========================================
|
|
90
|
+
// API Action Handlers
|
|
91
|
+
// ==========================================
|
|
92
|
+
|
|
93
|
+
async function handleApiAction (
|
|
94
|
+
action: () => Promise<any>,
|
|
95
|
+
errorMessage: string,
|
|
96
|
+
successMessage?: string
|
|
97
|
+
) {
|
|
98
|
+
try {
|
|
99
|
+
const result = await action()
|
|
100
|
+
if (result.success) {
|
|
101
|
+
await invalidate('api:getApplicantRequests')
|
|
102
|
+
if (successMessage) {
|
|
103
|
+
toasts.add(successMessage, 'success', 5000)
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
const error = result.messages?.find((m: { type: string, message: string }) => m.type === 'error')?.message
|
|
107
|
+
showError(error ?? errorMessage)
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
showError(errorMessage)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ==========================================
|
|
115
|
+
// UI Card Action Builders
|
|
116
|
+
// ==========================================
|
|
117
|
+
|
|
118
|
+
// TODO probably switch this to hide actions instead of disable, but it is nice to see them for now.
|
|
119
|
+
// In general these actions are based off of the text info in https://www.figma.com/design/VXFWNXZ6UNlDxmGNwVoEuu/ReqQuest?node-id=954-101490&m=dev
|
|
120
|
+
function buildCardActions (request: DashboardAppRequest) {
|
|
121
|
+
const submitted = isSubmitted(request.status)
|
|
122
|
+
const cancelWithdrawLabel = submitted ? `Withdraw ${uiRegistry.getWord('appRequest')}` : `Cancel ${uiRegistry.getWord('appRequest')}`
|
|
123
|
+
|
|
124
|
+
return [
|
|
125
|
+
{
|
|
126
|
+
label: `View ${uiRegistry.getWord('appRequest')}`,
|
|
127
|
+
onClick: async () => await openSidePanel(request)
|
|
128
|
+
},
|
|
129
|
+
// {
|
|
130
|
+
// label: 'Download Offer',
|
|
131
|
+
// onClick: async () => await downloadOffer(request.id),
|
|
132
|
+
// icon: Download,
|
|
133
|
+
// disabled: !request.actions.offer
|
|
134
|
+
// },
|
|
135
|
+
// {
|
|
136
|
+
// label: 'Appeal Decision',
|
|
137
|
+
// onClick: async () => await appealDecision(request.id),
|
|
138
|
+
// icon: Warning,
|
|
139
|
+
// disabled: !request.actions.returnToApplicant
|
|
140
|
+
// },
|
|
141
|
+
{
|
|
142
|
+
label: `Export ${uiRegistry.getWord('appRequest')}`,
|
|
143
|
+
onClick: async () => await exportApplication(request.id),
|
|
144
|
+
icon: DocumentExport
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
label: cancelWithdrawLabel,
|
|
148
|
+
onClick: () => { cancelConfirmation = { open: true, requestId: request.id, isWithdraw: submitted } },
|
|
149
|
+
icon: Close,
|
|
150
|
+
disabled: !request.actions.cancel
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
label: 'Reinstate Application',
|
|
154
|
+
onClick: async () => await reinstateApplication(request.id),
|
|
155
|
+
icon: Reset,
|
|
156
|
+
disabled: !request.actions.reopen
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ==========================================
|
|
162
|
+
// Application Actions
|
|
163
|
+
// ==========================================
|
|
164
|
+
// const appealDecision = async (requestId: string) =>
|
|
165
|
+
// await handleApiAction(
|
|
166
|
+
// async () => await api.returnAppRequest(requestId),
|
|
167
|
+
// 'Failed to appeal decision',
|
|
168
|
+
// 'Appeal submitted successfully'
|
|
169
|
+
// )
|
|
170
|
+
|
|
171
|
+
const reinstateApplication = async (requestId: string) =>
|
|
172
|
+
await handleApiAction(
|
|
173
|
+
async () => await api.reopenAppRequest(requestId),
|
|
174
|
+
'Failed to reinstate application',
|
|
175
|
+
'Application reinstated successfully'
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
// async function downloadOffer (requestId: string) {
|
|
179
|
+
// // TODO: Implement actual download logic
|
|
180
|
+
// console.log('Download offer for', requestId)
|
|
181
|
+
// }
|
|
182
|
+
|
|
183
|
+
async function exportApplication (requestId: string) {
|
|
184
|
+
await goto(resolve(`/requests/${requestId}/export`))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ==========================================
|
|
188
|
+
// Panel Dialog Management
|
|
189
|
+
// ==========================================
|
|
190
|
+
|
|
191
|
+
async function openSidePanel (appRequest: DashboardAppRequest) {
|
|
192
|
+
sidePanelOpen = true
|
|
193
|
+
loading = true
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
// Fetch additional application details for the side panel
|
|
197
|
+
const details = await api.getAppRequestForExport(appRequest.id)
|
|
198
|
+
|
|
199
|
+
selectedAppRequest = details.appRequest as AppRequestForDetails | undefined
|
|
200
|
+
|
|
201
|
+
appData = details.appRequest.data
|
|
202
|
+
applications = details.applicationsReview
|
|
203
|
+
prequalPrompts = details.prequalPrompts
|
|
204
|
+
postqualPrompts = details.postqualPrompts
|
|
205
|
+
} catch (error) {
|
|
206
|
+
showError('Failed to load application details')
|
|
207
|
+
console.error('Error loading application details:', error)
|
|
208
|
+
} finally {
|
|
209
|
+
loading = false
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function closeSidePanel () {
|
|
214
|
+
sidePanelOpen = false
|
|
215
|
+
selectedAppRequest = undefined
|
|
216
|
+
appData = {}
|
|
217
|
+
prequalPrompts = undefined
|
|
218
|
+
postqualPrompts = undefined
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function handleCancelConfirm () {
|
|
222
|
+
if (!cancelConfirmation.requestId) return
|
|
223
|
+
|
|
224
|
+
const actionType = cancelConfirmation.isWithdraw ? 'withdraw' : 'cancel'
|
|
225
|
+
await handleApiAction(
|
|
226
|
+
async () => await api.cancelAppRequest(cancelConfirmation.requestId!),
|
|
227
|
+
`Failed to ${actionType} ${uiRegistry.getWord('appRequest').toLowerCase()}`,
|
|
228
|
+
`${uiRegistry.getWord('appRequest')} ${actionType === 'withdraw' ? 'withdrawn' : 'cancelled'} successfully`
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
cancelConfirmation = { open: false, requestId: null, isWithdraw: false }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Handle submit action based on status action type, used for PanelDialog, specifically for the App View-only side panel
|
|
235
|
+
async function handleSubmitAction () {
|
|
236
|
+
if (!selectedAppRequest) return
|
|
237
|
+
|
|
238
|
+
const { status, id } = selectedAppRequest
|
|
239
|
+
const actionType = getStatusActionType(status)
|
|
240
|
+
|
|
241
|
+
switch (actionType) {
|
|
242
|
+
case 'navigate':
|
|
243
|
+
await goto(resolve(`/requests/${id}/apply`))
|
|
244
|
+
break
|
|
245
|
+
case 'export':
|
|
246
|
+
await exportApplication(id)
|
|
247
|
+
break
|
|
248
|
+
// case 'download':
|
|
249
|
+
// await downloadOffer(id)
|
|
250
|
+
// break
|
|
251
|
+
case 'none':
|
|
252
|
+
// No action for completed/final statuses
|
|
253
|
+
break
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ==========================================
|
|
258
|
+
// Form Handlers
|
|
259
|
+
// ==========================================
|
|
260
|
+
|
|
261
|
+
async function clickCreateAppRequest () {
|
|
262
|
+
// Use the currently selected period
|
|
263
|
+
if (!selectedPeriodId) return
|
|
264
|
+
const { success } = await submitAppRequest({ periodId: selectedPeriodId })
|
|
265
|
+
if (success) await onSaved()
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function onSaved () {
|
|
269
|
+
await invalidate('api:getAccess')
|
|
270
|
+
await goto(resolve(`/requests/${lastInsertedId}/apply`))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function submitAppRequest (data: { periodId: string }) {
|
|
274
|
+
const response = await api.createAppRequest(data.periodId, access.user!.login)
|
|
275
|
+
if (response.success) lastInsertedId = response.id
|
|
276
|
+
return {
|
|
277
|
+
...response,
|
|
278
|
+
data
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ==========================================
|
|
283
|
+
// FilterUI Helper Functions
|
|
284
|
+
// ==========================================
|
|
285
|
+
|
|
286
|
+
// Reactive statements for filter options
|
|
287
|
+
$: periodOptions = availablePeriods.map(p => ({ value: p.id, label: p.name }))
|
|
288
|
+
$: statusOptions = availableStatuses.map(label => ({ value: label, label }))
|
|
289
|
+
$: isPastTab = filterDataSearch?.tab === 'past_applications'
|
|
290
|
+
$: pastFiltersReady = isPastTab && (periodOptions.length > 0 || statusOptions.length > 0)
|
|
291
|
+
$: showIntroPanel = !isPastTab
|
|
292
|
+
</script>
|
|
293
|
+
|
|
294
|
+
<div class="flow applicant-dashboard" class:past-tab={isPastTab}>
|
|
295
|
+
<!-- Filter UI for recent/past applications -->
|
|
296
|
+
<FilterUI
|
|
297
|
+
search={isPastTab}
|
|
298
|
+
tabs={[
|
|
299
|
+
{ label: `Recent ${uiRegistry.getPlural('appRequest')}`, value: 'recent_applications' },
|
|
300
|
+
{ label: `Past ${uiRegistry.getPlural('appRequest')}`, value: 'past_applications' }
|
|
301
|
+
]}
|
|
302
|
+
on:apply={e => { filterDataSearch = e.detail }}
|
|
303
|
+
on:mount={e => { filterDataSearch = e.detail }}>
|
|
304
|
+
<svelte:fragment slot="quickfilters">
|
|
305
|
+
{#if pastFiltersReady}
|
|
306
|
+
<FieldMultiselect path="periodIds" label={uiRegistry.getWord('period')} items={periodOptions} />
|
|
307
|
+
<FieldMultiselect path="status" label="Status" items={statusOptions} />
|
|
308
|
+
{/if}
|
|
309
|
+
</svelte:fragment>
|
|
310
|
+
</FilterUI>
|
|
311
|
+
|
|
312
|
+
<!-- Time sensitive banner -->
|
|
313
|
+
{#if announcement}
|
|
314
|
+
<AnnouncementBanner subject={announcement.subject} body={announcement.body} link={announcement.link} linkText={announcement.linkText} />
|
|
315
|
+
{/if}
|
|
316
|
+
|
|
317
|
+
{#if showIntroPanel}
|
|
318
|
+
<IntroPanel
|
|
319
|
+
title={uiRegistry.config.applicantDashboardIntroHeader}
|
|
320
|
+
subtitle={uiRegistry.config.applicantDashboardIntroDetail}
|
|
321
|
+
|
|
322
|
+
>
|
|
323
|
+
<svelte:fragment slot="block-end">
|
|
324
|
+
{#if displayablePeriods.length > 0}
|
|
325
|
+
<!-- Show period info and action for selected period -->
|
|
326
|
+
{#if currentPeriod && periodInfo}
|
|
327
|
+
<div class="flex flex-wrap gap-4 items-center">
|
|
328
|
+
<!-- Period Information Section -->
|
|
329
|
+
<section class="text-base text-center flex-col gap-2">
|
|
330
|
+
<dl class="flex gap-4 text-sm">
|
|
331
|
+
<div class="flex flex-col bg-[var(--cds-ui-03,#d9d9d9)] py-4 px-4 justify-center">
|
|
332
|
+
<dt class="font-bold">{uiRegistry.getWord('appRequest')} opens:</dt>
|
|
333
|
+
<dd>
|
|
334
|
+
<time datetime={periodInfo.openDateMachineFormat}>
|
|
335
|
+
{periodInfo.openLabel}
|
|
336
|
+
</time>
|
|
337
|
+
</dd>
|
|
338
|
+
</div>
|
|
339
|
+
{#if currentPeriod.closeDate}
|
|
340
|
+
<div class="flex flex-col bg-[var(--cds-ui-03,#d9d9d9)] py-4 px-4 justify-center">
|
|
341
|
+
<dt class="font-bold">{uiRegistry.getWord('appRequest')} closes:</dt>
|
|
342
|
+
<dd>
|
|
343
|
+
<time datetime={periodInfo.closeDateMachineFormat}>
|
|
344
|
+
{periodInfo.closeDate}
|
|
345
|
+
</time>
|
|
346
|
+
</dd>
|
|
347
|
+
</div>
|
|
348
|
+
{/if}
|
|
349
|
+
</dl>
|
|
350
|
+
</section>
|
|
351
|
+
<section class="flex flex-wrap gap-4 items-end intro-actions">
|
|
352
|
+
<!-- Period selector (if multiple) -->
|
|
353
|
+
{#if displayablePeriods.length > 1}
|
|
354
|
+
<div class="min-w-[250px] intro-dropdown-wrapper">
|
|
355
|
+
<Dropdown
|
|
356
|
+
size="lg"
|
|
357
|
+
labelText="Select a {uiRegistry.getWord('period').toLowerCase()}"
|
|
358
|
+
hideLabel
|
|
359
|
+
placeholder="Choose a {uiRegistry.getWord('period').toLowerCase()}"
|
|
360
|
+
bind:selectedId={selectedPeriodId}
|
|
361
|
+
items={displayablePeriods.map(p => ({
|
|
362
|
+
id: p.id,
|
|
363
|
+
text: p.name
|
|
364
|
+
}))}
|
|
365
|
+
/>
|
|
366
|
+
</div>
|
|
367
|
+
{/if}
|
|
368
|
+
|
|
369
|
+
<!-- Action Button. Only show start button if no existing application and user can act -->
|
|
370
|
+
{#if periodInfo.canStartNew && access.createAppRequestSelf}
|
|
371
|
+
<Button
|
|
372
|
+
on:click={clickCreateAppRequest}
|
|
373
|
+
kind="primary"
|
|
374
|
+
size="field"
|
|
375
|
+
>
|
|
376
|
+
Start {uiRegistry.getWord('appRequest').toLowerCase()}
|
|
377
|
+
</Button>
|
|
378
|
+
{:else if (!periodInfo.canStartNew || !access.createAppRequestSelf) && appRequests.length > 0}
|
|
379
|
+
<Tooltip>
|
|
380
|
+
<p>{uiRegistry.getWord('appRequest')} started, see dashboard card for details.</p>
|
|
381
|
+
</Tooltip>
|
|
382
|
+
{/if}
|
|
383
|
+
</section>
|
|
384
|
+
|
|
385
|
+
</div>
|
|
386
|
+
{/if}
|
|
387
|
+
{:else}
|
|
388
|
+
<p>No {uiRegistry.getWord('appRequest').toLowerCase()} {uiRegistry.getPlural('period').toLowerCase()} are currently available.</p>
|
|
389
|
+
{/if}
|
|
390
|
+
</svelte:fragment>
|
|
391
|
+
<svelte:fragment>
|
|
392
|
+
{#if uiRegistry.config.slots?.applicantDashboardIntroSlot}
|
|
393
|
+
<svelte:component this={uiRegistry.config.slots?.applicantDashboardIntroSlot} {appRequests} {api} />
|
|
394
|
+
{/if}
|
|
395
|
+
</svelte:fragment>
|
|
396
|
+
</IntroPanel>
|
|
397
|
+
{/if}
|
|
398
|
+
<!-- Application Cards -->
|
|
399
|
+
<Panel title={filterDataSearch?.tab === 'past_applications' ? `All past ${uiRegistry.getPlural('appRequest').toLowerCase()}` : `All recent ${uiRegistry.getPlural('appRequest').toLowerCase()}`}>
|
|
400
|
+
{#if appRequests.length === 0}
|
|
401
|
+
{#if filterDataSearch?.tab === 'past_applications'}
|
|
402
|
+
<InlineNotification
|
|
403
|
+
kind="info"
|
|
404
|
+
title="No results found."
|
|
405
|
+
subtitle={pastFiltersReady ? 'You may need to refine your searched terms, filters or try again.' : undefined}
|
|
406
|
+
lowContrast
|
|
407
|
+
hideCloseButton
|
|
408
|
+
/>
|
|
409
|
+
{:else}
|
|
410
|
+
<InlineNotification
|
|
411
|
+
kind="info"
|
|
412
|
+
title="No recent {uiRegistry.getPlural('appRequest').toLowerCase()} found."
|
|
413
|
+
subtitle="You have no {uiRegistry.getPlural('appRequest').toLowerCase()} created or modified in the last {recentDays} days. Check under Past {uiRegistry.getPlural('appRequest').toLowerCase()} tab."
|
|
414
|
+
lowContrast
|
|
415
|
+
hideCloseButton
|
|
416
|
+
/>
|
|
417
|
+
{/if}
|
|
418
|
+
{:else}
|
|
419
|
+
<CardGrid cardSize="500px">
|
|
420
|
+
{#each appRequests as request (request.id)}
|
|
421
|
+
<div class="max-w-[680px]">
|
|
422
|
+
<AppRequestCard
|
|
423
|
+
{request}
|
|
424
|
+
actions={buildCardActions(request)}
|
|
425
|
+
showAcceptanceButtons={true}
|
|
426
|
+
/>
|
|
427
|
+
</div>
|
|
428
|
+
{/each}
|
|
429
|
+
</CardGrid>
|
|
430
|
+
{/if}
|
|
431
|
+
</Panel>
|
|
432
|
+
|
|
433
|
+
<!-- Inlined Application Details Panel -->
|
|
434
|
+
<PanelDialog
|
|
435
|
+
size="large"
|
|
436
|
+
open={sidePanelOpen}
|
|
437
|
+
title={selectedAppRequest?.period?.name ?? `${uiRegistry.getWord('appRequest')} Details`}
|
|
438
|
+
cancelText="Close"
|
|
439
|
+
submitText={selectedAppRequest?.status ? getSubmitButtonText(selectedAppRequest.status) : ''}
|
|
440
|
+
on:cancel={closeSidePanel}
|
|
441
|
+
on:submit={handleSubmitAction}>
|
|
442
|
+
{#if selectedAppRequest}
|
|
443
|
+
<ApplicationDetailsView
|
|
444
|
+
appRequest={selectedAppRequest}
|
|
445
|
+
{applications}
|
|
446
|
+
{appData}
|
|
447
|
+
{prequalPrompts}
|
|
448
|
+
{postqualPrompts}
|
|
449
|
+
{loading}
|
|
450
|
+
{uiRegistry}
|
|
451
|
+
title={selectedAppRequest.period?.name ? `View your ${selectedAppRequest.period.name} ${uiRegistry.getWord('appRequest').toLowerCase()}` : `View your ${uiRegistry.getWord('appRequest').toLowerCase()}`}
|
|
452
|
+
showCorrectionsInline
|
|
453
|
+
/>
|
|
454
|
+
{/if}
|
|
455
|
+
</PanelDialog>
|
|
456
|
+
|
|
457
|
+
<Modal
|
|
458
|
+
bind:open={cancelConfirmation.open}
|
|
459
|
+
modalHeading={cancelConfirmation.isWithdraw ? `Withdraw ${uiRegistry.getWord('appRequest')}` : `Cancel ${uiRegistry.getWord('appRequest')}`}
|
|
460
|
+
primaryButtonText={cancelConfirmation.isWithdraw ? `Yes, Withdraw ${uiRegistry.getWord('appRequest')}` : `Yes, Cancel ${uiRegistry.getWord('appRequest')}`}
|
|
461
|
+
secondaryButtonText={`Keep ${uiRegistry.getWord('appRequest')}`}
|
|
462
|
+
on:click:button--secondary={() => { cancelConfirmation = { open: false, requestId: null, isWithdraw: false } }}
|
|
463
|
+
on:submit={handleCancelConfirm}
|
|
464
|
+
on:close={() => { cancelConfirmation = { open: false, requestId: null, isWithdraw: false } }}
|
|
465
|
+
>
|
|
466
|
+
<p>
|
|
467
|
+
{#if cancelConfirmation.isWithdraw}
|
|
468
|
+
Withdrawing your {uiRegistry.getWord('appRequest').toLowerCase()} will remove it from consideration and any approved benefits will be lost. You can reopen and edit this {uiRegistry.getWord('appRequest').toLowerCase()} to resubmit it, as long as the {uiRegistry.getWord('appRequest').toLowerCase()} window is still open.
|
|
469
|
+
{:else}
|
|
470
|
+
Canceling your {uiRegistry.getWord('appRequest').toLowerCase()} will remove it from consideration. You can reinstate and submit it later, as long as the {uiRegistry.getWord('appRequest').toLowerCase()} window is still open.
|
|
471
|
+
{/if}
|
|
472
|
+
</p>
|
|
473
|
+
</Modal>
|
|
474
|
+
|
|
475
|
+
<Toasts />
|
|
476
|
+
</div>
|
|
477
|
+
|
|
478
|
+
<style>
|
|
479
|
+
.applicant-dashboard {
|
|
480
|
+
--repel-vertical-alignment:top;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.intro-dropdown-wrapper :global(.bx--dropdown__wrapper.bx--list-box__wrapper) {
|
|
484
|
+
display: flex;
|
|
485
|
+
flex-direction: column;
|
|
486
|
+
margin-top:-5px;
|
|
487
|
+
}
|
|
488
|
+
.intro-actions :global(.bx--list-box__field) {
|
|
489
|
+
background: var(--cds-field-02,#ffffff);
|
|
490
|
+
}
|
|
491
|
+
.applicant-dashboard.past-tab :global(.quickfilters-form) {
|
|
492
|
+
min-height: 2.5rem;
|
|
493
|
+
}
|
|
494
|
+
</style>
|