@reqquest/ui 1.17.0 → 1.19.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 +508 -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 +5 -4
- package/dist/registry.d.ts +17 -0
- package/dist/registry.js +3 -0
- package/package.json +29 -15
- package/dist/components/DelayedSkeleton.svelte.d.ts +0 -21
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { api, getApplicationStatusInfo } from '$internal'
|
|
3
|
+
import type { LayoutData } from '../$types.js'
|
|
4
|
+
import { uiRegistry } from '$local'
|
|
5
|
+
import { enumApplicationStatus, enumRequirementStatus, InfoCard } from '@reqquest/ui'
|
|
6
|
+
import { Loading, Modal } from 'carbon-components-svelte'
|
|
7
|
+
import { Information } from 'carbon-icons-svelte'
|
|
8
|
+
import StatusMessageList from '$internal/components/StatusMessageList.svelte'
|
|
9
|
+
import { getContext } from 'svelte'
|
|
10
|
+
import { page } from '$app/stores'
|
|
11
|
+
import { UISHELL_STICKY_CONTEXT, type UIShellStickyStore } from '@txstate-mws/carbon-svelte'
|
|
12
|
+
|
|
13
|
+
export let basicRequestData: LayoutData['basicRequestData']
|
|
14
|
+
let open = false
|
|
15
|
+
|
|
16
|
+
const stickyShellHeader = getContext<UIShellStickyStore>(UISHELL_STICKY_CONTEXT)
|
|
17
|
+
|
|
18
|
+
// the review page only loads the displayed program, so the dialog fetches every program's outcome when it opens
|
|
19
|
+
type IneligibilityInfo = Awaited<ReturnType<typeof api.getIneligibilityInfo>>
|
|
20
|
+
let ineligibilityInfo: IneligibilityInfo | undefined
|
|
21
|
+
let loadingInfo = false
|
|
22
|
+
async function openIneligibilityInfo () {
|
|
23
|
+
open = true
|
|
24
|
+
loadingInfo = true
|
|
25
|
+
try {
|
|
26
|
+
ineligibilityInfo = await api.getIneligibilityInfo($page.params.id!)
|
|
27
|
+
} finally {
|
|
28
|
+
loadingInfo = false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
$: optedOutPrograms = ineligibilityInfo?.applications
|
|
33
|
+
.filter(curr => curr.requirements.flatMap(r => r.prompts).find(r => r.optOut))
|
|
34
|
+
.reduce((acc, c) => {
|
|
35
|
+
const optOutRequirement = c.requirements.find(r => r.prompts.some(p => p.optOut))
|
|
36
|
+
return {
|
|
37
|
+
...acc,
|
|
38
|
+
[c.id]: optOutRequirement?.status === enumRequirementStatus.DISQUALIFYING
|
|
39
|
+
}
|
|
40
|
+
}, {} as Record<string, boolean>) ?? {}
|
|
41
|
+
$: ineligiblePrograms = ineligibilityInfo?.applications.filter(app => app.status === enumApplicationStatus.INELIGIBLE) ?? []
|
|
42
|
+
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<div class="review-container">
|
|
46
|
+
<div class="review-sidebar flow" style="--sticky-top: {$stickyShellHeader.height}px">
|
|
47
|
+
<InfoCard
|
|
48
|
+
noPrimaryAction
|
|
49
|
+
title={basicRequestData.applicant.fullname}
|
|
50
|
+
actions={[
|
|
51
|
+
{ label: 'Ineligibility Information', icon: Information, onClick: openIneligibilityInfo }
|
|
52
|
+
]}>
|
|
53
|
+
<dl class="identifier">
|
|
54
|
+
<dt>{uiRegistry.getWord('login')}</dt>
|
|
55
|
+
<dd>{basicRequestData.applicant.login}</dd>
|
|
56
|
+
{#each basicRequestData.applicant.otherIdentifiers as identifier (identifier.id)}
|
|
57
|
+
<dt>{identifier.label}:</dt>
|
|
58
|
+
<dd>{identifier.id}</dd>
|
|
59
|
+
{/each}
|
|
60
|
+
</dl>
|
|
61
|
+
{#if uiRegistry.config.slots?.reviewerSidebarCard}
|
|
62
|
+
<svelte:component this={uiRegistry.config.slots.reviewerSidebarCard} appRequest={basicRequestData} applicant={basicRequestData.applicant} {api} />
|
|
63
|
+
{/if}
|
|
64
|
+
</InfoCard>
|
|
65
|
+
|
|
66
|
+
<slot name="sidebar" />
|
|
67
|
+
{#if uiRegistry.config.slots?.reviewerSidebar}
|
|
68
|
+
<svelte:component this={uiRegistry.config.slots.reviewerSidebar} appRequest={basicRequestData} applicant={basicRequestData.applicant} {api} />
|
|
69
|
+
{/if}
|
|
70
|
+
</div>
|
|
71
|
+
<div class="review-content">
|
|
72
|
+
<slot />
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<Modal passiveModal bind:open modalHeading="Ineligible programs">
|
|
77
|
+
<div class='flex flex-col gap-4 text-base'>
|
|
78
|
+
{#if loadingInfo}
|
|
79
|
+
<Loading withOverlay={false} small />
|
|
80
|
+
{:else if ineligiblePrograms.length && ineligibilityInfo}
|
|
81
|
+
{#each ineligiblePrograms as application}
|
|
82
|
+
{@const warningReqs = application.requirements.filter(r => r.status === 'WARNING' && r.statusReason)}
|
|
83
|
+
{@const message = `${getApplicationStatusInfo(application.status, ineligibilityInfo.phase, ineligibilityInfo.closedAt, application.rescindedStatus)[0].label}${application.statusReason ?? optedOutPrograms[application.id] ? ':' : ''} ${optedOutPrograms[application.id] ? 'Opted out' : application.statusReason}`}
|
|
84
|
+
<span class='py-2'>{application.title}</span>
|
|
85
|
+
<StatusMessageList
|
|
86
|
+
icon
|
|
87
|
+
items={[{ id: '', message }]}
|
|
88
|
+
variant="error"
|
|
89
|
+
accordionTitle="Multiple warnings" />
|
|
90
|
+
<StatusMessageList
|
|
91
|
+
icon
|
|
92
|
+
items={warningReqs.map(r => ({ id: r.id, message: r.statusReason! }))}
|
|
93
|
+
variant="warning"
|
|
94
|
+
accordionTitle="Multiple warnings" />
|
|
95
|
+
{/each}
|
|
96
|
+
{:else}
|
|
97
|
+
<p class="text-center">No ineligible programs</p>
|
|
98
|
+
{/if}
|
|
99
|
+
</div>
|
|
100
|
+
</Modal>
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
<style>
|
|
104
|
+
.review-container {
|
|
105
|
+
display: flex;
|
|
106
|
+
flex-direction: row-reverse;
|
|
107
|
+
gap: 15px;
|
|
108
|
+
padding: 15px;
|
|
109
|
+
align-items: flex-start;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.review-sidebar {
|
|
113
|
+
container-type: inline-size;
|
|
114
|
+
width: min(20rem, 33%);
|
|
115
|
+
position: sticky;
|
|
116
|
+
top: calc(var(--sticky-top, 0px) + 15px);
|
|
117
|
+
/* Cards taller than the viewport scroll within rather than being cut off. */
|
|
118
|
+
max-height: calc(100dvh - var(--sticky-top, 0px) - 30px);
|
|
119
|
+
overflow-y: auto;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.review-content {
|
|
123
|
+
flex-grow: 1;
|
|
124
|
+
container-type: inline-size;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@media (max-width: 800px) {
|
|
128
|
+
.review-container {
|
|
129
|
+
flex-direction: column-reverse;
|
|
130
|
+
align-items: stretch;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.review-sidebar {
|
|
134
|
+
width: 100%;
|
|
135
|
+
position: static;
|
|
136
|
+
max-height: none;
|
|
137
|
+
overflow-y: visible;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.identifier {
|
|
142
|
+
width: 100%;
|
|
143
|
+
display: grid;
|
|
144
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
145
|
+
gap: 8px;
|
|
146
|
+
container-type: inline-size;
|
|
147
|
+
}
|
|
148
|
+
.identifier dt {
|
|
149
|
+
font-weight: bold;
|
|
150
|
+
}
|
|
151
|
+
.identifier dd, .identifier dt {
|
|
152
|
+
margin: 0;
|
|
153
|
+
padding: 0;
|
|
154
|
+
}
|
|
155
|
+
@container (width < 18rem) {
|
|
156
|
+
.identifier {
|
|
157
|
+
grid-template-columns: 1fr;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.ineligible-warning {
|
|
162
|
+
padding: .625rem 0;
|
|
163
|
+
}
|
|
164
|
+
</style>
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { BadgeNumber, FieldTextArea, PanelDialog, PanelFormDialog } from '@txstate-mws/carbon-svelte'
|
|
3
|
+
import { toasts } from '@txstate-mws/svelte-components'
|
|
4
|
+
import { ReviewerQuestions, AppRequestActions, SystemDeterminationNotice } from '$internal/components'
|
|
5
|
+
import { Button, InlineNotification, Select, SelectItem } from 'carbon-components-svelte'
|
|
6
|
+
import Edit from 'carbon-icons-svelte/lib/Edit.svelte'
|
|
7
|
+
import Pen from 'carbon-icons-svelte/lib/Pen.svelte'
|
|
8
|
+
import TrashCan from 'carbon-icons-svelte/lib/TrashCan.svelte'
|
|
9
|
+
import View from 'carbon-icons-svelte/lib/View.svelte'
|
|
10
|
+
import { invalidateAll } from '$app/navigation'
|
|
11
|
+
import { api, getApplicationStatusInfo, hasDisplayablePrompts } from '$internal'
|
|
12
|
+
import { CommentCard, enumPromptVisibility, enumRequirementStatus, enumRequirementType, InfoCard, PromptIndicators, type StatusReason } from '@reqquest/ui'
|
|
13
|
+
import type { PageData } from './$types'
|
|
14
|
+
import { uiRegistry } from '$local'
|
|
15
|
+
import ApproveLayout from '../ApproveLayout.svelte'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* This page is the primary reviewer screen, it will show all the prompt
|
|
20
|
+
* data so far collected and allow the reviewer to navigate to (modal popup?)
|
|
21
|
+
* any available prompts to fill in more data.
|
|
22
|
+
*/
|
|
23
|
+
export let data: PageData
|
|
24
|
+
$: ({ basicRequestData, appRequest, programKey, requestId } = data)
|
|
25
|
+
$: application = appRequest.applications.find(a => a.programKey === programKey)!
|
|
26
|
+
// mirrors the API's "no trailing work" rule
|
|
27
|
+
$: nothingToReview = !application.requirements.some(r => r.workflowStage || r.type === enumRequirementType.PREAPPROVAL || r.type === enumRequirementType.APPROVAL || r.type === enumRequirementType.ACCEPTANCE)
|
|
28
|
+
const NOTE_MAX_LENGTH = 350
|
|
29
|
+
$: notes = appRequest?.notes ?? []
|
|
30
|
+
$: latestNote = notes[0]
|
|
31
|
+
|
|
32
|
+
let showNotesDialog = false
|
|
33
|
+
let showAddNoteDialog = false
|
|
34
|
+
let showUpdateNoteDialog = false
|
|
35
|
+
let noteBeingEdited: { id: string, content: string, persistent: boolean } | undefined = undefined
|
|
36
|
+
|
|
37
|
+
async function onAddNoteValidate (data: { content: string, persistent?: boolean }) {
|
|
38
|
+
const response = await api.addNote(appRequest.id, data.content, data.persistent, true)
|
|
39
|
+
return response.messages
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function onAddNoteSubmit (data: { content: string, persistent?: boolean }) {
|
|
43
|
+
const response = await api.addNote(appRequest.id, data.content, data.persistent, false)
|
|
44
|
+
return { ...response, data }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function onAddNoteSaved () {
|
|
48
|
+
showAddNoteDialog = false
|
|
49
|
+
await invalidateAll()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// NOTE: addNote and updateNote should always share validation rules.
|
|
53
|
+
// As long as we do not set persistent flag, then we can reuse addNote
|
|
54
|
+
// to validate.
|
|
55
|
+
async function onUpdateNoteValidate (data: { content: string }) {
|
|
56
|
+
const response = await api.addNote(appRequest.id, data.content, undefined, true)
|
|
57
|
+
return response.messages
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function onUpdateNoteSubmit (data: { content: string, persistent: boolean }) {
|
|
61
|
+
if (!noteBeingEdited) return { success: false, messages: [], data }
|
|
62
|
+
if (data.persistent !== noteBeingEdited.persistent) {
|
|
63
|
+
const response = await api.togglePersistence(noteBeingEdited.id)
|
|
64
|
+
if (!response.success) return { ...response, data }
|
|
65
|
+
}
|
|
66
|
+
if (data.content !== noteBeingEdited.content) {
|
|
67
|
+
const response = await api.updateNote(noteBeingEdited.id, data.content)
|
|
68
|
+
return { ...response, data }
|
|
69
|
+
}
|
|
70
|
+
return { success: true, messages: [], data }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function onUpdateNoteSaved () {
|
|
74
|
+
showUpdateNoteDialog = false
|
|
75
|
+
noteBeingEdited = undefined
|
|
76
|
+
await invalidateAll()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function editNote (note: { id: string, content: string, persistent?: boolean | null }) {
|
|
80
|
+
noteBeingEdited = {
|
|
81
|
+
id: note.id,
|
|
82
|
+
content: note.content,
|
|
83
|
+
persistent: note.persistent ?? false
|
|
84
|
+
}
|
|
85
|
+
showNotesDialog = false
|
|
86
|
+
showUpdateNoteDialog = true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function deleteNote (noteId: string) {
|
|
90
|
+
const success = await api.deleteNote(noteId)
|
|
91
|
+
if (success) {
|
|
92
|
+
toasts.add({ type: 'success', message: 'Note deleted.' })
|
|
93
|
+
await invalidateAll()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type ApplicationRequirement = (typeof appRequest)['applications'][number]['requirements'][number]
|
|
98
|
+
type Section = { key: string, title: string, requirements: ApplicationRequirement[] }
|
|
99
|
+
|
|
100
|
+
// the screen is split into panels by requirement type, in lifecycle order, unless the program's
|
|
101
|
+
// reviewSections (on its UIConfig entry) says otherwise. A custom panel takes the requirements it names, in the order it names
|
|
102
|
+
// them; the default panels keep whatever is left, in requirementKeys order. Panels the layout does not
|
|
103
|
+
// mention trail it in the default order, so a layout may be partial.
|
|
104
|
+
let sections: Section[]
|
|
105
|
+
// the stage panel the "Send to" button lands on once the application is ready for workflow - the last
|
|
106
|
+
// stage in lifecycle order, whatever order the layout displays the panels in
|
|
107
|
+
let lastStageKey: string | undefined
|
|
108
|
+
// it's not enough to show an indicator for the prompt's requirement status; since we only show the prompt
|
|
109
|
+
// once, we need to show the highest indicator for any requirement that uses this prompt. So if two requirements
|
|
110
|
+
// share a prompt and one requirement is disqualifying while the other is just a warning, we need to show the
|
|
111
|
+
// disqualifying indicator.
|
|
112
|
+
let promptIndicator: Record<string, { indicator: typeof PromptIndicators[keyof typeof PromptIndicators], reason: string | undefined } | undefined>
|
|
113
|
+
// every requirement status reason aimed at each prompt key (all statuses, blamed or fallback), for the
|
|
114
|
+
// inline notifications under the reviewer's edit forms and the `statusReasons` prop of every prompt component
|
|
115
|
+
let promptStatusReasons: Record<string, StatusReason[]>
|
|
116
|
+
$: {
|
|
117
|
+
promptIndicator = {}
|
|
118
|
+
promptStatusReasons = {}
|
|
119
|
+
const general: Section = { key: 'general', title: 'General Questions', requirements: [] }
|
|
120
|
+
const program: Section = { key: 'program', title: application.title, requirements: [] }
|
|
121
|
+
const reviewer: Section = { key: 'reviewer', title: 'Reviewer Questions', requirements: [] }
|
|
122
|
+
const acceptance: Section = { key: 'acceptance', title: 'Acceptance', requirements: [] }
|
|
123
|
+
const defaults: Record<string, Section> = { GENERAL: general, PROGRAM: program, REVIEWER: reviewer, ACCEPTANCE: acceptance }
|
|
124
|
+
const blockingStages: Section[] = []
|
|
125
|
+
const nonBlockingStages: Section[] = []
|
|
126
|
+
const stages: Record<string, Section> = {}
|
|
127
|
+
const layout = uiRegistry.getProgram(programKey)?.reviewSections ?? []
|
|
128
|
+
const custom = layout.map((entry, i) => 'requirementKeys' in entry
|
|
129
|
+
? { key: `custom:${i}`, title: entry.title, requirementKeys: entry.requirementKeys, requirements: [] as ApplicationRequirement[] }
|
|
130
|
+
: undefined)
|
|
131
|
+
const claimedKeys = new Set<string>(custom.flatMap(c => c?.requirementKeys ?? []))
|
|
132
|
+
const claimed = new Map<string, ApplicationRequirement>()
|
|
133
|
+
for (const req of application.requirements) {
|
|
134
|
+
// automation indicator is about the prompt itself not requirement's status, so only applies to the requirements own prompts
|
|
135
|
+
for (const prompt of req.prompts) {
|
|
136
|
+
if (prompt.visibility === enumPromptVisibility.UNREACHABLE) continue
|
|
137
|
+
if (uiRegistry.getPrompt(prompt.key)?.automation && (promptIndicator[prompt.key]?.indicator ?? 0) < PromptIndicators.AUTOMATION) {
|
|
138
|
+
promptIndicator[prompt.key] = { indicator: PromptIndicators.AUTOMATION, reason: 'This answer will be filled in by an automation.' }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const faulted = req.blame?.length
|
|
142
|
+
? req.blame
|
|
143
|
+
: req.prompts.filter(p => p.visibility !== enumPromptVisibility.UNREACHABLE).map(p => p.key)
|
|
144
|
+
for (const key of faulted) {
|
|
145
|
+
(promptStatusReasons[key] ??= []).push({ status: req.status, statusReason: req.statusReason, programName: application.title, blamed: !!req.blame?.length })
|
|
146
|
+
if (req.status === enumRequirementStatus.DISQUALIFYING && (promptIndicator[key]?.indicator ?? 0) < PromptIndicators.DISQUALIFYING) {
|
|
147
|
+
promptIndicator[key] = { indicator: PromptIndicators.DISQUALIFYING, reason: req.statusReason ?? undefined }
|
|
148
|
+
} else if (req.status === enumRequirementStatus.WARNING && (promptIndicator[key]?.indicator ?? 0) < PromptIndicators.WARNING) {
|
|
149
|
+
promptIndicator[key] = { indicator: PromptIndicators.WARNING, reason: req.statusReason ?? undefined }
|
|
150
|
+
} else if (req.status === enumRequirementStatus.PENDING && req.statusReason && (promptIndicator[key]?.indicator ?? 0) < PromptIndicators.PENDING) {
|
|
151
|
+
promptIndicator[key] = { indicator: PromptIndicators.PENDING, reason: req.statusReason }
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (req.workflowStage) {
|
|
155
|
+
if (!stages[req.workflowStage.key]) {
|
|
156
|
+
stages[req.workflowStage.key] = { key: `stage:${req.workflowStage.key}`, title: req.workflowStage.title, requirements: [] }
|
|
157
|
+
;(req.workflowStage.blocking ? blockingStages : nonBlockingStages).push(stages[req.workflowStage.key])
|
|
158
|
+
}
|
|
159
|
+
stages[req.workflowStage.key].requirements.push(req)
|
|
160
|
+
} else if (claimedKeys.has(req.key)) claimed.set(req.key, req)
|
|
161
|
+
else if (req.type === enumRequirementType.ACCEPTANCE) acceptance.requirements.push(req)
|
|
162
|
+
else if (req.type === enumRequirementType.APPROVAL || req.type === enumRequirementType.PREAPPROVAL) reviewer.requirements.push(req)
|
|
163
|
+
else if (req.type === enumRequirementType.QUALIFICATION || req.type === enumRequirementType.POSTQUAL) program.requirements.push(req)
|
|
164
|
+
else general.requirements.push(req) // PREQUAL
|
|
165
|
+
}
|
|
166
|
+
// a requirement the layout names but the period disabled has no record here, so it is simply skipped
|
|
167
|
+
for (const c of custom) {
|
|
168
|
+
if (c) c.requirements = c.requirementKeys.map(k => claimed.get(k)).filter((r): r is ApplicationRequirement => r != null)
|
|
169
|
+
}
|
|
170
|
+
const ordered: Section[] = []
|
|
171
|
+
for (const [i, entry] of layout.entries()) {
|
|
172
|
+
const section = 'requirementKeys' in entry
|
|
173
|
+
? custom[i]
|
|
174
|
+
: 'workflowStage' in entry
|
|
175
|
+
? stages[entry.workflowStage]
|
|
176
|
+
: defaults[entry.section]
|
|
177
|
+
if (section) ordered.push(section)
|
|
178
|
+
}
|
|
179
|
+
for (const section of [general, program, reviewer, acceptance, ...blockingStages, ...nonBlockingStages]) {
|
|
180
|
+
if (!ordered.includes(section)) ordered.push(section)
|
|
181
|
+
}
|
|
182
|
+
// a panel with nothing to show is dropped rather than rendered as an empty header. Stage panels are the
|
|
183
|
+
// exception: they carry the advance/return controls and a deliberate "nothing to answer, you may advance" state
|
|
184
|
+
sections = ordered.filter(s => (!!s.requirements[0]?.workflowStage) || s.requirements.some(hasDisplayablePrompts))
|
|
185
|
+
lastStageKey = [...blockingStages, ...nonBlockingStages].pop()?.key
|
|
186
|
+
}
|
|
187
|
+
$: applicationStatusTags = getApplicationStatusInfo(application.status, appRequest.phase, appRequest.closedAt, application.rescindedStatus).map(info => ({ label: info.label, type: info.color }))
|
|
188
|
+
$: loading = false
|
|
189
|
+
let showLoading = false
|
|
190
|
+
let loadingTimer: NodeJS.Timeout | undefined
|
|
191
|
+
$: {
|
|
192
|
+
if (loading) {
|
|
193
|
+
if (!loadingTimer) {
|
|
194
|
+
loadingTimer = setTimeout(() => {
|
|
195
|
+
showLoading = true
|
|
196
|
+
}, 500)
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
clearTimeout(loadingTimer)
|
|
200
|
+
loadingTimer = undefined
|
|
201
|
+
showLoading = false
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// open rescind and restore panelformdialog
|
|
205
|
+
let reasonAction: 'rescind' | 'restore' | undefined = undefined
|
|
206
|
+
$: reasonActionInfo = reasonAction === 'restore'
|
|
207
|
+
? { title: `Restore ${application.navTitle} benefit`, instructions: 'You will need to provide a reason why this benefit should be restored. The benefit will be restored to the state it was in before it was rescinded.', submitText: 'Restore benefit', successMessage: 'Benefit restored.' }
|
|
208
|
+
: { title: `Rescind ${application.navTitle} benefit`, instructions: 'You will need to provide a reason why this benefit should be rescinded. The applicant will no longer be able to use this benefit after rescinding.', submitText: 'Rescind benefit', successMessage: 'Benefit rescinded.' }
|
|
209
|
+
|
|
210
|
+
async function onReasonValidate (data: { reason: string }) {
|
|
211
|
+
const response = reasonAction === 'restore'
|
|
212
|
+
? await api.restore(application.id, data.reason, true)
|
|
213
|
+
: await api.rescind(application.id, data.reason, true)
|
|
214
|
+
return response.messages
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function onReasonSubmit (data: { reason: string }) {
|
|
218
|
+
const response = reasonAction === 'restore'
|
|
219
|
+
? await api.restore(application.id, data.reason, false)
|
|
220
|
+
: await api.rescind(application.id, data.reason, false)
|
|
221
|
+
return { ...response, data }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function onReasonSaved () {
|
|
225
|
+
const message = reasonActionInfo.successMessage
|
|
226
|
+
onReasonCancel()
|
|
227
|
+
await invalidateAll()
|
|
228
|
+
toasts.add({ type: 'success', message })
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function onReasonCancel () {
|
|
232
|
+
reasonAction = undefined
|
|
233
|
+
}
|
|
234
|
+
</script>
|
|
235
|
+
|
|
236
|
+
{#if nothingToReview}
|
|
237
|
+
<SystemDeterminationNotice programTitle={application.title} />
|
|
238
|
+
{/if}
|
|
239
|
+
<ApproveLayout {basicRequestData}>
|
|
240
|
+
<svelte:fragment slot="sidebar">
|
|
241
|
+
<InfoCard title={application.title} tags={applicationStatusTags} tagsInBody />
|
|
242
|
+
<AppRequestActions {application} {basicRequestData} {requestId} />
|
|
243
|
+
<!-- <InfoCard title={application.title} tags={applicationStatusTags} tagsInBody> -->
|
|
244
|
+
<!--
|
|
245
|
+
<dl class="card">
|
|
246
|
+
<dt>Status</dt>
|
|
247
|
+
<dd><TagSet tags={applicationStatusTags} /></dd>
|
|
248
|
+
</dl>
|
|
249
|
+
-->
|
|
250
|
+
<!-- </InfoCard> -->
|
|
251
|
+
<InfoCard
|
|
252
|
+
title="Application Notes"
|
|
253
|
+
actions={[
|
|
254
|
+
...(appRequest.actions.createNote ? [{ label: 'Add Note', icon: Pen, onClick: () => { showAddNoteDialog = true } }] : []),
|
|
255
|
+
...(notes.length > 0 ? [{ label: 'See All Notes', icon: View, onClick: () => { showNotesDialog = true } }] : [])
|
|
256
|
+
]}
|
|
257
|
+
>
|
|
258
|
+
<BadgeNumber slot="header-right" value={notes.length} style="--badge-bg: var(--cds-ui-04)" />
|
|
259
|
+
<div class="active-note">
|
|
260
|
+
{#if latestNote}
|
|
261
|
+
<CommentCard
|
|
262
|
+
authorName={latestNote.author.fullname}
|
|
263
|
+
authorLogin={latestNote.author.login}
|
|
264
|
+
content={latestNote.content}
|
|
265
|
+
createdAt={latestNote.createdAt}
|
|
266
|
+
noborder
|
|
267
|
+
/>
|
|
268
|
+
{:else}
|
|
269
|
+
<InlineNotification kind="info" lowContrast hideCloseButton title="No application notes." subtitle="Add a note to see it here."></InlineNotification>
|
|
270
|
+
{/if}
|
|
271
|
+
</div>
|
|
272
|
+
</InfoCard>
|
|
273
|
+
|
|
274
|
+
{#if application.actions.rescindApplication || application.actions.restoreApplication}
|
|
275
|
+
<InfoCard title="Rescind / Restore Benefit">
|
|
276
|
+
<div class="flow">
|
|
277
|
+
{#if application.actions.rescindApplication}
|
|
278
|
+
<Button size="small" class="[ w-full ]" kind="danger" on:click={() => { reasonAction = 'rescind' }}>Rescind benefit</Button>
|
|
279
|
+
{/if}
|
|
280
|
+
{#if application.actions.restoreApplication}
|
|
281
|
+
<Button size="small" class="[ w-full ]" kind="danger-ghost" on:click={() => { reasonAction = 'restore' }}>Restore benefit</Button>
|
|
282
|
+
{/if}
|
|
283
|
+
</div>
|
|
284
|
+
</InfoCard>
|
|
285
|
+
{/if}
|
|
286
|
+
</svelte:fragment>
|
|
287
|
+
<ReviewerQuestions {sections} {lastStageKey} {appRequest} {application} {promptIndicator} {promptStatusReasons} {basicRequestData} bind:loading/>
|
|
288
|
+
</ApproveLayout>
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
{#if reasonAction}
|
|
292
|
+
<PanelFormDialog
|
|
293
|
+
title={reasonActionInfo.title}
|
|
294
|
+
open
|
|
295
|
+
on:cancel={onReasonCancel}
|
|
296
|
+
on:saved={onReasonSaved}
|
|
297
|
+
validate={onReasonValidate}
|
|
298
|
+
submit={onReasonSubmit}
|
|
299
|
+
submitText={reasonActionInfo.submitText}
|
|
300
|
+
centered
|
|
301
|
+
>
|
|
302
|
+
<div>{reasonActionInfo.instructions}</div>
|
|
303
|
+
<FieldTextArea path="reason" labelText={`Reason to ${reasonAction} this benefit.`} required notNull rows={4} />
|
|
304
|
+
<div class="bx--form__helper-text">This reason will be recorded in the request activity and included in the email sent to the applicant.</div>
|
|
305
|
+
</PanelFormDialog>
|
|
306
|
+
{/if}
|
|
307
|
+
|
|
308
|
+
{#if showUpdateNoteDialog && noteBeingEdited}
|
|
309
|
+
<PanelFormDialog
|
|
310
|
+
title="Update Note"
|
|
311
|
+
bind:open={showUpdateNoteDialog}
|
|
312
|
+
on:cancel={() => { showUpdateNoteDialog = false; noteBeingEdited = undefined }}
|
|
313
|
+
on:saved={onUpdateNoteSaved}
|
|
314
|
+
validate={onUpdateNoteValidate}
|
|
315
|
+
submit={onUpdateNoteSubmit}
|
|
316
|
+
submitText="Save"
|
|
317
|
+
disableSaveUntilChanged={true}
|
|
318
|
+
centered
|
|
319
|
+
preload={{
|
|
320
|
+
content: noteBeingEdited.content,
|
|
321
|
+
persistent: noteBeingEdited.persistent
|
|
322
|
+
}}
|
|
323
|
+
>
|
|
324
|
+
<FieldTextArea path="content" labelText="Note" required notNull rows={6} maxCount={NOTE_MAX_LENGTH} />
|
|
325
|
+
<!--
|
|
326
|
+
{#if appRequest.actions.createPersistentNote}
|
|
327
|
+
<FieldCheckbox path="persistent" labelText="Persistent (show on the applicant's profile)" />
|
|
328
|
+
{/if}
|
|
329
|
+
-->
|
|
330
|
+
</PanelFormDialog>
|
|
331
|
+
{/if}
|
|
332
|
+
|
|
333
|
+
<PanelFormDialog
|
|
334
|
+
title="New application note"
|
|
335
|
+
bind:open={showAddNoteDialog}
|
|
336
|
+
on:cancel={() => { showAddNoteDialog = false }}
|
|
337
|
+
on:saved={onAddNoteSaved}
|
|
338
|
+
validate={onAddNoteValidate}
|
|
339
|
+
submit={onAddNoteSubmit}
|
|
340
|
+
submitText="Save note"
|
|
341
|
+
centered
|
|
342
|
+
>
|
|
343
|
+
<div>Write a note for this application.</div>
|
|
344
|
+
<FieldTextArea path="content" labelText="Note" required notNull rows={6} maxCount={NOTE_MAX_LENGTH} placeholder="Enter your note here." />
|
|
345
|
+
<div class="bx--form__helper-text">This note will only be visible to other reviewers who access this application.</div>
|
|
346
|
+
<!-- {#if appRequest.actions.createPersistentNote}
|
|
347
|
+
<FieldCheckbox path="persistent" labelText="Persistent (show on the applicant's profile)" />
|
|
348
|
+
{/if}
|
|
349
|
+
-->
|
|
350
|
+
</PanelFormDialog>
|
|
351
|
+
|
|
352
|
+
<PanelDialog
|
|
353
|
+
title="Application Notes"
|
|
354
|
+
bind:open={showNotesDialog}
|
|
355
|
+
on:cancel={() => { showNotesDialog = false }}
|
|
356
|
+
cancelText="Close"
|
|
357
|
+
on:submit={() => { showNotesDialog = false; showAddNoteDialog = true }}
|
|
358
|
+
submitText="Add a note"
|
|
359
|
+
centered
|
|
360
|
+
size="large"
|
|
361
|
+
>
|
|
362
|
+
<div class="notes-list flow">
|
|
363
|
+
{#each notes as note (note.id)}
|
|
364
|
+
<CommentCard
|
|
365
|
+
content={note.content}
|
|
366
|
+
authorName={note.author.fullname}
|
|
367
|
+
authorLogin={note.author.login}
|
|
368
|
+
createdAt={note.createdAt}
|
|
369
|
+
actions={[
|
|
370
|
+
{ label: 'Edit', icon: Edit, onClick: () => editNote(note) },
|
|
371
|
+
{ label: 'Delete', icon: TrashCan, onClick: () => deleteNote(note.id) }
|
|
372
|
+
]}
|
|
373
|
+
/>
|
|
374
|
+
{:else}
|
|
375
|
+
<p>No application notes. Add note to see it here.</p>
|
|
376
|
+
{/each}
|
|
377
|
+
</div>
|
|
378
|
+
</PanelDialog>
|
|
379
|
+
|
|
380
|
+
<style>
|
|
381
|
+
.active-note {
|
|
382
|
+
padding: 4px 8px;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
:global(.content):has(.notes-list) {
|
|
386
|
+
max-height: calc(90vh - 1.5rem - 64px);
|
|
387
|
+
background-color: var(--cds-ui-01);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.app-actions {
|
|
391
|
+
width: fit-content;
|
|
392
|
+
}
|
|
393
|
+
.app-actions :global(.bx--select) {
|
|
394
|
+
width: auto;
|
|
395
|
+
}
|
|
396
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { api } from '$internal'
|
|
2
|
+
import { error } from '@sveltejs/kit'
|
|
3
|
+
import { uiRegistry } from '$local'
|
|
4
|
+
import type { PageLoad } from './$types'
|
|
5
|
+
import { getInlineReviewerEditPrompts, coalesceAppRequestPrompts } from '$internal'
|
|
6
|
+
|
|
7
|
+
export const load: PageLoad = async ({ params, depends }) => {
|
|
8
|
+
const appRequest = await api.getReviewData(params.id)
|
|
9
|
+
if (!appRequest) throw error(404, `${uiRegistry.getWord('appRequest')} not found`)
|
|
10
|
+
const inlinePrompts = getInlineReviewerEditPrompts(appRequest)
|
|
11
|
+
const inlinePromptsWithData = await api.getPromptDataLegion(params.id, (inlinePrompts ?? []).map(prompt => prompt.id))
|
|
12
|
+
const coalescedAppRequest = coalesceAppRequestPrompts(appRequest, inlinePromptsWithData)
|
|
13
|
+
depends('request:approve')
|
|
14
|
+
return { appRequest: coalescedAppRequest, programKey: params.programKey, requestId: params.id }
|
|
15
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ColumnList, FieldDate, FieldMultiselect, FilterUI, Pagination } from '@txstate-mws/carbon-svelte'
|
|
3
|
+
import { htmlEncode, sortby } from 'txstate-utils'
|
|
4
|
+
import { longDateTime, downloadCsv } from '$internal'
|
|
5
|
+
import { uiRegistry } from '$local'
|
|
6
|
+
import type { PageData } from './$types'
|
|
7
|
+
import ApproveLayout from '../ApproveLayout.svelte'
|
|
8
|
+
import DocExport from 'carbon-icons-svelte/lib/DocumentExport.svelte'
|
|
9
|
+
|
|
10
|
+
export let data: PageData
|
|
11
|
+
$: ({ activity, actions, people, basicRequestData } = data)
|
|
12
|
+
$: activityActions = actions.map(value => ({ value }))
|
|
13
|
+
$: activityPeople = sortby(people.map(p => ({ value: p.login, label: `${p.fullname} (${p.login})` })), 'label')
|
|
14
|
+
|
|
15
|
+
function formatCSVData (d: any[]) {
|
|
16
|
+
return d.map(row => ({
|
|
17
|
+
User: `${row.user.fullname} (${row.user.login})` + (row.impersonatedBy ? `<div class="impersonator">${htmlEncode(row.impersonatedBy.fullname)} (${row.impersonatedBy.login})</div>` : ''),
|
|
18
|
+
'Created At': longDateTime(row.createdAt),
|
|
19
|
+
Action: row.action,
|
|
20
|
+
Description: row.description
|
|
21
|
+
}))
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<ApproveLayout {basicRequestData}>
|
|
26
|
+
<div>
|
|
27
|
+
<FilterUI>
|
|
28
|
+
<svelte:fragment slot="quickfilters">
|
|
29
|
+
<FieldMultiselect path="actions" label="Activity Type" placeholder="Filter by activity type" items={activityActions} />
|
|
30
|
+
<FieldMultiselect path="users" label="People" placeholder="Filter by person" items={activityPeople} />
|
|
31
|
+
</svelte:fragment>
|
|
32
|
+
<FieldDate path="happenedAfter" labelText="From" placeholder="Activities After" />
|
|
33
|
+
<FieldDate path="happenedBefore" labelText="To" placeholder="Activities Before" />
|
|
34
|
+
</FilterUI>
|
|
35
|
+
|
|
36
|
+
<ColumnList
|
|
37
|
+
searchable
|
|
38
|
+
title='Activity Log'
|
|
39
|
+
rows={activity}
|
|
40
|
+
columns={[
|
|
41
|
+
{ id: 'user', label: 'Person', render: row => htmlEncode(`${row.user.fullname} (${row.user.login})`) + (row.impersonatedBy ? `<div class="impersonator">${htmlEncode(row.impersonatedBy.fullname)} (${row.impersonatedBy.login})</div>` : '') },
|
|
42
|
+
{ id: 'createdAt', label: 'Date', render: row => longDateTime(row.createdAt) },
|
|
43
|
+
{ id: 'action', label: 'Activity', get: 'action' },
|
|
44
|
+
{ id: 'description', label: 'Detail', get: 'description' }
|
|
45
|
+
]}
|
|
46
|
+
listActions={[
|
|
47
|
+
{ label: 'Download', icon: DocExport, onClick: () => { downloadCsv(formatCSVData(data.activity), 'app_requests_activity_log') } }
|
|
48
|
+
]}
|
|
49
|
+
noItemsTitle="No activity."
|
|
50
|
+
noItemsSubtitle="No activity has been recorded for this {uiRegistry.getWord('appRequest').toLocaleLowerCase()}." />
|
|
51
|
+
</div>
|
|
52
|
+
<Pagination
|
|
53
|
+
page={data.pageInfo?.currentPage}
|
|
54
|
+
totalItems={data.pageInfo?.totalItems}
|
|
55
|
+
pageSize={25}
|
|
56
|
+
chooseSize
|
|
57
|
+
/>
|
|
58
|
+
|
|
59
|
+
</ApproveLayout>
|
|
60
|
+
|
|
61
|
+
<style>
|
|
62
|
+
div :global(.impersonator) {
|
|
63
|
+
font-size: 0.8em;
|
|
64
|
+
color: var(--cds-text-02);
|
|
65
|
+
}
|
|
66
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { api } from '$internal'
|
|
2
|
+
import { error } from '@sveltejs/kit'
|
|
3
|
+
import type { PageLoad } from './$types'
|
|
4
|
+
import { extractMergedFilters, extractPaginationParams } from '@txstate-mws/carbon-svelte'
|
|
5
|
+
|
|
6
|
+
export const load: PageLoad = async ({ params, url, depends }) => {
|
|
7
|
+
const filters = extractMergedFilters(url)
|
|
8
|
+
const { page, pagesize } = extractPaginationParams(url)
|
|
9
|
+
const { activity, actions, people, pageInfo } = await api.getRequestActivity(params.id, filters, { page, perPage: pagesize })
|
|
10
|
+
depends('request:approve:activity')
|
|
11
|
+
return { activity, actions, people, pageInfo }
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { api } from '$internal'
|
|
2
|
+
import type { PageLoad } from './$types'
|
|
3
|
+
|
|
4
|
+
export const load: PageLoad = async ({ params, depends }) => {
|
|
5
|
+
const details = await api.getAppRequestForExport(params.id)
|
|
6
|
+
|
|
7
|
+
depends('request:reviewer-export')
|
|
8
|
+
return {
|
|
9
|
+
...details,
|
|
10
|
+
appData: details.appRequest.data
|
|
11
|
+
}
|
|
12
|
+
}
|