@reqquest/ui 1.0.1 → 1.1.1

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 (46) hide show
  1. package/README.md +8 -0
  2. package/dist/api.js +670 -129
  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 -1
  22. package/dist/status-utils.js +343 -0
  23. package/dist/typed-client/schema.graphql +619 -134
  24. package/dist/typed-client/schema.js +87 -23
  25. package/dist/typed-client/types.js +993 -482
  26. package/dist/util.js +12 -1
  27. package/package.json +39 -40
  28. package/dist/api.d.ts +0 -595
  29. package/dist/components/ButtonLoadingIcon.svelte.d.ts +0 -18
  30. package/dist/components/index.d.ts +0 -1
  31. package/dist/index.d.ts +0 -5
  32. package/dist/registry.d.ts +0 -138
  33. package/dist/stores/IStateStore.d.ts +0 -5
  34. package/dist/typed-client/index.d.ts +0 -25
  35. package/dist/typed-client/runtime/batcher.d.ts +0 -105
  36. package/dist/typed-client/runtime/createClient.d.ts +0 -17
  37. package/dist/typed-client/runtime/error.d.ts +0 -18
  38. package/dist/typed-client/runtime/fetcher.d.ts +0 -10
  39. package/dist/typed-client/runtime/generateGraphqlOperation.d.ts +0 -30
  40. package/dist/typed-client/runtime/index.d.ts +0 -11
  41. package/dist/typed-client/runtime/linkTypeMap.d.ts +0 -9
  42. package/dist/typed-client/runtime/typeSelection.d.ts +0 -28
  43. package/dist/typed-client/runtime/types.d.ts +0 -55
  44. package/dist/typed-client/schema.d.ts +0 -1483
  45. package/dist/typed-client/types.d.ts +0 -540
  46. package/dist/util.d.ts +0 -2
@@ -0,0 +1,320 @@
1
+ <script context="module" lang="ts">
2
+ import type { CardImage, TagItem } from '@txstate-mws/carbon-svelte'
3
+
4
+ export interface CardSelectItem {
5
+ value: any
6
+ title: string
7
+ subhead?: string
8
+ image?: CardImage
9
+ tags?: TagItem[]
10
+ disabled?: boolean
11
+ selectionLabel?: string
12
+ }
13
+ </script>
14
+
15
+ <script lang="ts">
16
+ import { FORM_CONTEXT, FORM_INHERITED_PATH, Field, type FormStore } from '@txstate-mws/svelte-forms'
17
+ import { Store } from '@txstate-mws/svelte-store'
18
+ import { createEventDispatcher, getContext } from 'svelte'
19
+ import { get, isNotBlank, equal } from 'txstate-utils'
20
+ import { Card, CardGrid, FormInlineNotification } from '@txstate-mws/carbon-svelte'
21
+
22
+ const dispatch = createEventDispatcher()
23
+
24
+ /**
25
+ * Path to the form field in the data structure
26
+ * @type {string}
27
+ */
28
+ export let path: string
29
+
30
+ /**
31
+ * Array of card options
32
+ * @type {CardSelectItem[]}
33
+ */
34
+ export let items: CardSelectItem[]
35
+
36
+ /**
37
+ * Set to true to treat the value as a number instead of string
38
+ * @type {boolean}
39
+ * @default false
40
+ */
41
+ export let number = false
42
+
43
+ /**
44
+ * Set to true to treat the value as a boolean
45
+ * @type {boolean}
46
+ * @default false
47
+ */
48
+ export let boolean = false
49
+
50
+ /**
51
+ * Set to true to prevent null values
52
+ * @type {boolean}
53
+ * @default false
54
+ */
55
+ export let notNull = false
56
+
57
+ /**
58
+ * Whether the field group is conditional
59
+ * @type {boolean}
60
+ * @default true
61
+ */
62
+ export let conditional = true
63
+
64
+ /**
65
+ * Default selected value
66
+ * @type {any}
67
+ * @default undefined
68
+ */
69
+ export let defaultValue: any = undefined
70
+
71
+ /**
72
+ * Text to display in the fieldset legend
73
+ * @type {string | undefined}
74
+ * @default undefined
75
+ */
76
+ export let legendText: string | undefined = undefined
77
+
78
+ /**
79
+ * When true, the field is marked as required in the UI. This is only for visual indication
80
+ * and does not affect validation. Validation is handled by the API that receives the data.
81
+ * @type {boolean}
82
+ * @default false
83
+ */
84
+ export let required = false
85
+
86
+ /**
87
+ * CSS size for the grid minimum item width (like CardGrid)
88
+ * @type {string}
89
+ * @default '20rem'
90
+ */
91
+ export let cardSize = '20rem'
92
+
93
+ /**
94
+ * Gap between cards in the grid
95
+ * @type {string}
96
+ * @default '16px'
97
+ */
98
+ export let gap = '16px'
99
+
100
+ /**
101
+ * Custom function to serialize values for form submission
102
+ * @type {((value: any) => string) | undefined}
103
+ * @default undefined
104
+ */
105
+ export let serialize: ((value: any) => string) | undefined = undefined
106
+
107
+ /**
108
+ * Custom function to deserialize values from form data
109
+ * @type {((value: string) => any) | undefined}
110
+ * @default undefined
111
+ */
112
+ export let deserialize: ((value: string) => any) | undefined = undefined
113
+
114
+ const store = getContext<FormStore>(FORM_CONTEXT)
115
+ const inheritedPath = getContext<string>(FORM_INHERITED_PATH)
116
+ const finalPath = [inheritedPath, path].filter(isNotBlank).join('.')
117
+
118
+ const itemStore = new Store(items)
119
+ $: itemStore.set(items)
120
+
121
+ let finalSerialize: ((value: any) => string) | undefined
122
+ let finalDeserialize: ((value: string) => any) | undefined
123
+
124
+ function onSelect (item: CardSelectItem) {
125
+ return () => {
126
+ if (item.disabled) return
127
+ const deserialized = item.value
128
+ void store.setField(finalPath, deserialized)
129
+ dispatch('update', deserialized)
130
+ }
131
+ }
132
+
133
+ async function reactToItems (..._: any[]) {
134
+ if (!finalDeserialize) return
135
+ if (!items.length) {
136
+ return await store.setField(finalPath, finalDeserialize(''), { notDirty: true })
137
+ }
138
+ const val = get($store.data, finalPath)
139
+ if (!items.some(o => equal(o.value, val))) {
140
+ await store.setField(finalPath, notNull && items.some(o => equal(o.value, defaultValue)) ? defaultValue : finalDeserialize(''), { notDirty: true })
141
+ }
142
+ }
143
+ $: reactToItems($itemStore, finalDeserialize).catch(console.error)
144
+
145
+ // Keyboard navigation
146
+ let activeIdx = 0
147
+ const cardElements: HTMLDivElement[] = []
148
+
149
+ function findNextEnabled (startIdx: number, direction: 1 | -1): number {
150
+ let idx = startIdx
151
+ let attempts = 0
152
+ while (attempts < items.length) {
153
+ idx = (idx + direction + items.length) % items.length
154
+ if (!items[idx]?.disabled) return idx
155
+ attempts++
156
+ }
157
+ return startIdx // All disabled, stay put
158
+ }
159
+
160
+ function focusCard (idx: number) {
161
+ if (idx >= 0 && idx < items.length && !items[idx]?.disabled) {
162
+ activeIdx = idx
163
+ cardElements[idx]?.focus()
164
+ }
165
+ }
166
+
167
+ function handleKeyDown (e: KeyboardEvent, idx: number) {
168
+ switch (e.key) {
169
+ case 'ArrowRight':
170
+ case 'ArrowDown':
171
+ e.preventDefault()
172
+ focusCard(findNextEnabled(idx, 1))
173
+ break
174
+ case 'ArrowLeft':
175
+ case 'ArrowUp':
176
+ e.preventDefault()
177
+ focusCard(findNextEnabled(idx, -1))
178
+ break
179
+ case ' ':
180
+ case 'Enter':
181
+ e.preventDefault()
182
+ if (!items[idx]?.disabled) {
183
+ onSelect(items[idx])()
184
+ }
185
+ break
186
+ }
187
+ }
188
+ </script>
189
+ <!--
190
+ @component
191
+
192
+ A form field component that displays a grid of selectable cards (single selection).
193
+ Similar to FieldRadioTile but with richer card-based visuals including images and tags.
194
+
195
+ By default this component expects values to be strings. If you wish to use other types of values, you
196
+ must provide a `serialize` and `deserialize` function. If you set the `number` prop, an appropriate
197
+ serializer and deserializer will be provided by default.
198
+
199
+ If your `items` are not ready on first load (e.g. they're being loaded from an API fetch), you must
200
+ place this field inside an `{#if}` block until they are ready.
201
+ -->
202
+ <Field {path} {notNull} {conditional} {defaultValue} {boolean} {number} {serialize} {deserialize} bind:finalSerialize bind:finalDeserialize let:messages let:value let:invalid let:onBlur>
203
+ <div on:focusin={() => dispatch('focus')} on:focusout={() => { onBlur(); dispatch('blur') }}>
204
+ <fieldset class="card-select-fieldset">
205
+ {#if legendText}
206
+ <legend class="bx--label">{legendText}{#if required} <span aria-hidden="true"> *</span>{/if}</legend>
207
+ {/if}
208
+ <CardGrid {cardSize} {gap}>
209
+ {#each $itemStore as item, index (item.value)}
210
+ {@const selected = finalSerialize ? value === finalSerialize(item.value) : equal(value, item.value)}
211
+ <div
212
+ bind:this={cardElements[index]}
213
+ class="selectable-card"
214
+ class:selected
215
+ class:disabled={item.disabled}
216
+ role="radio"
217
+ aria-checked={selected}
218
+ aria-disabled={item.disabled}
219
+ tabindex={item.disabled ? -1 : (index === activeIdx ? 0 : -1)}
220
+ on:click={onSelect(item)}
221
+ on:keydown={e => handleKeyDown(e, index)}
222
+ >
223
+ <Card
224
+ title={item.title}
225
+ subhead={item.subhead}
226
+ image={item.image}
227
+ tags={item.tags ?? []}
228
+ actions={[]}
229
+ navigations={[]}
230
+ >
231
+ <slot {item} {selected} {index} />
232
+ </Card>
233
+ <div class="selection-indicator justify-center">
234
+ <span class="radio-circle" class:checked={selected}></span>
235
+ <span class="selection-label">{item.selectionLabel ?? 'Choose this option'}</span>
236
+ </div>
237
+ </div>
238
+ {/each}
239
+ </CardGrid>
240
+ </fieldset>
241
+ </div>
242
+ {#each messages as message (message)}
243
+ <FormInlineNotification {message} />
244
+ {/each}
245
+ </Field>
246
+
247
+ <style>
248
+ .card-select-fieldset {
249
+ border: none;
250
+ padding: 0;
251
+ margin: 0;
252
+ }
253
+
254
+ .selectable-card {
255
+ cursor: pointer;
256
+ border: 2px solid transparent;
257
+ border-radius: 4px;
258
+ outline: none;
259
+ transition: border-color 0.15s ease;
260
+ }
261
+
262
+ .selectable-card:focus-visible {
263
+ outline: 2px solid var(--cds-focus, #0f62fe);
264
+ outline-offset: 2px;
265
+ }
266
+
267
+ .selectable-card:hover:not(.disabled) {
268
+ border-color: var(--cds-interactive-03, #0353e9);
269
+ }
270
+
271
+ .selectable-card.selected {
272
+ border-color: var(--cds-interactive-01, #0f62fe);
273
+ }
274
+
275
+ .selectable-card.disabled {
276
+ cursor: not-allowed;
277
+ opacity: 0.5;
278
+ }
279
+
280
+ .selection-indicator {
281
+ display: flex;
282
+ align-items: center;
283
+ gap: 0.5rem;
284
+ padding: 0.75rem 1rem;
285
+ background-color: var(--cds-ui-01, #f4f4f4);
286
+ border-top: 1px solid var(--cds-ui-03, #e0e0e0);
287
+ }
288
+
289
+ .radio-circle {
290
+ width: 1.125rem;
291
+ height: 1.125rem;
292
+ border: 2px solid var(--cds-icon-01, #161616);
293
+ border-radius: 50%;
294
+ flex-shrink: 0;
295
+ position: relative;
296
+ }
297
+
298
+ .radio-circle.checked::after {
299
+ content: '';
300
+ position: absolute;
301
+ top: 50%;
302
+ left: 50%;
303
+ transform: translate(-50%, -50%);
304
+ width: 0.5rem;
305
+ height: 0.5rem;
306
+ background-color: var(--cds-interactive-01, #0f62fe);
307
+ border-radius: 50%;
308
+ }
309
+
310
+ .selection-label {
311
+ font-size: 0.875rem;
312
+ color: var(--cds-text-01, #161616);
313
+ }
314
+
315
+ /* Remove Card's default border when inside selectable-card */
316
+ .selectable-card :global(.card) {
317
+ border: none;
318
+ box-shadow: none;
319
+ }
320
+ </style>
@@ -0,0 +1,41 @@
1
+ <script lang="ts">
2
+ import { TagSet } from '@txstate-mws/carbon-svelte'
3
+ import type { TagItem } from '@txstate-mws/carbon-svelte'
4
+
5
+ export let title = ''
6
+ export let subtitle = ''
7
+ export let tags: TagItem[] | undefined = undefined
8
+ </script>
9
+
10
+ <section class="bg-[var(--cds-ui-01)] py-4 px-[16px] text-2xl mb-2">
11
+ <div class="repel">
12
+ <div class="max-w-lg flex flex-col flex-wrap justify-start">
13
+ {#if title}
14
+ <div class="title-block [ flex gap-[12px] ] ">
15
+ <h2 class="text-[1.25rem] text-[var(--cds-text-01,#1A1A1A)] font-normal leading-7">
16
+ {title}
17
+ </h2>
18
+ {#if tags}
19
+ <TagSet tags={tags} />
20
+ {/if}
21
+ </div>
22
+ {#if subtitle}
23
+ <p class="text-sm text-[var(--cds-text-02)] leading-[18px] tracking-[0.16px] pt-2">
24
+ {subtitle}
25
+ </p>
26
+ {/if}
27
+ {/if}
28
+ <!-- Optional content slot below title/subtitle -->
29
+ {#if $$slots.default}
30
+ <div class="mt-2">
31
+ <slot />
32
+ </div>
33
+ {/if}
34
+ </div>
35
+ {#if $$slots['block-end']}
36
+ <div class="block-end-slot">
37
+ <slot name="block-end" />
38
+ </div>
39
+ {/if}
40
+ </div>
41
+ </section>
@@ -0,0 +1,100 @@
1
+ <script lang="ts">
2
+ import { ActionSet, Panel, TagSet } from '@txstate-mws/carbon-svelte'
3
+ import Tab from "carbon-components-svelte/src/Tabs/Tab.svelte";
4
+ import TabContent from "carbon-components-svelte/src/Tabs/TabContent.svelte";
5
+ import Tabs from "carbon-components-svelte/src/Tabs/Tabs.svelte";
6
+ import Tag from "carbon-components-svelte/src/Tag/Tag.svelte";
7
+ import SettingsEdit from "carbon-icons-svelte/lib/SettingsEdit.svelte";
8
+ import View from "carbon-icons-svelte/lib/View.svelte";
9
+ import { invalidate } from '$app/navigation'
10
+ import { api } from '../api'
11
+ import { page } from '$app/stores'
12
+ import type { UIRegistry } from '../registry'
13
+ import { groupby, pluralize } from 'txstate-utils'
14
+
15
+ export let program: any
16
+ export let sharedProgramRequirements: any
17
+ export let openModal: any
18
+ export let onClick: any
19
+ export let uiRegistry: UIRegistry
20
+
21
+ const disablePeriodProgram = (requirementKey: string) => async () => {
22
+ const res = await api.disablePeriodProgramRequirements($page.params.id!, requirementKey, true)
23
+ await invalidate('api:getPeriodConfigurations')
24
+ }
25
+
26
+ const enablePeriodProgram = (requirementKey: string) => async () => {
27
+ const res = await api.disablePeriodProgramRequirements($page.params.id!, requirementKey, false)
28
+ await invalidate('api:getPeriodConfigurations')
29
+ }
30
+
31
+ $: enabledRequirements = Object.entries(groupby(program.requirements.filter(r => r.enabled), 'type'))
32
+ $: disabledRequirements = program.requirements.filter(r => !r.enabled)
33
+
34
+ </script>
35
+ <Panel title={program.title} expandable expanded>
36
+ {#each enabledRequirements as requrementEntries}
37
+ {@const type = requrementEntries[0]}
38
+ {@const requirements = requrementEntries[1]}
39
+ <Panel title='' expandable expanded>
40
+ <div style="display: content" slot="headerLeft">
41
+ <TagSet tags={[{ label: `Applicant: ${type}`, type: 'purple' }]} />
42
+ </div>
43
+ <div style="display: content" slot="headerRight">
44
+ <TagSet tags={[{ label: `${requirements.length} ${pluralize('requirement', requirements.length)}`, type: 'yellow' }]} />
45
+ </div>
46
+ <Tabs autoWidth>
47
+ <Tab label={`Enabled Requirements (${enabledRequirements.length})`} />
48
+ <Tab label={`Disabled Requirements (${disabledRequirements.length})`} />
49
+ <svelte:fragment slot='content'>
50
+ <TabContent>
51
+ {#each requirements as requirement (requirement.key)}
52
+ {@const reqDef = uiRegistry.getRequirement(requirement.key)}
53
+ <Panel title={requirement.title} expandable noPrimaryAction actions={[{ label: 'Configure requirement', onClick: onClick('requirement', requirement), disabled: reqDef?.configureComponent == null || !requirement.configuration.actions.update }, { label: 'Disable Requirement', onClick: disablePeriodProgram(requirement.key) }]}>
54
+ <div style="display: content" slot="headerLeft">
55
+ <TagSet tags={[{ label: 'Requirement', type: 'yellow' }]} />
56
+ </div>
57
+ <!-- <Button on:click={onClick('requirement', requirement)} type="primary" size="small" icon={SettingsEdit} iconDescription="Edit Configuration" disabled={reqDef.configureComponent == null || !requirement.configuration.actions.update} /> -->
58
+ <div style="display: content" slot="headerRight">
59
+ {@const tags = sharedProgramRequirements[requirement.key]?.length > 1 ? [{ label: 'Shared', onClick: openModal(requirement.key) }] : []}
60
+ <TagSet tags={tags} />
61
+ </div>
62
+
63
+ <ul class="prompts">
64
+ {#each requirement.prompts as prompt (prompt.key)}
65
+ {@const promptDef = uiRegistry.getPrompt(prompt.key)}
66
+ <li class="prompt justify-between">
67
+ <span>
68
+ <Tag type='green'>Prompt</Tag>{prompt.title}
69
+ </span>
70
+ <ActionSet
71
+ actions={[
72
+ // { label: 'View', icon: View },
73
+ { label: 'settings', icon: SettingsEdit, disabled: promptDef?.configureComponent == null || !prompt.configuration.actions.update, onClick: onClick('prompt', prompt) }
74
+ ]}
75
+ />
76
+ </li>
77
+ {/each}
78
+ </ul>
79
+ </Panel>
80
+ {/each}
81
+ </TabContent>
82
+ <TabContent>
83
+ {#each disabledRequirements as requirement (requirement.key)}
84
+ <Panel title={requirement.title} actions={[{ label: 'Enable Requirement', onClick: enablePeriodProgram(requirement.key) }]}>
85
+ Requirement: {requirement.title}
86
+ <ul class="prompts">
87
+ {#each requirement.prompts as prompt (prompt.key)}
88
+ <li class="prompt">
89
+ Prompt: {prompt.title}
90
+ </li>
91
+ {/each}
92
+ </ul>
93
+ </Panel>
94
+ {/each}
95
+ </TabContent>
96
+ </svelte:fragment>
97
+ </Tabs>
98
+ </Panel>
99
+ {/each}
100
+ </Panel>
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ import Button from "carbon-components-svelte/src/Button/Button.svelte";
3
+ import Launch from 'carbon-icons-svelte/lib/Launch.svelte'
4
+
5
+ /**
6
+ * Array of external links to show above the form.
7
+ * There can be up to 3 external links to help applicants if there are things they need to do externally to the app request.
8
+ * @type {{ url: string, label: string }[]}
9
+ */
10
+ export let externalLinks: { url: string, label: string }[] = []
11
+
12
+ /**
13
+ * If true, the form will take the full width of its container. If false, it will be constrained to max-w-screen-md.
14
+ * @type {boolean}
15
+ */
16
+ export let fullWidth = false
17
+
18
+ /**
19
+ * Alignment of the form container. 'center' will center it and 'left' will align it to the left.
20
+ * @type {'left' | 'center'}
21
+ */
22
+ export let align: 'left' | 'center' = 'center'
23
+ </script>
24
+ {#if externalLinks.length > 0}
25
+ <div class="prompt-intro-links flow max-w-screen-md mx-auto px-6">
26
+ <ul class="flex gap-4 flex-wrap mb-4 justify-center">
27
+ {#each externalLinks.slice(0, 3) as link (link.url)}
28
+ <li><Button kind="ghost" icon={Launch} href={link.url}>{link.label}</Button></li>
29
+ {/each}
30
+ </ul>
31
+ </div>
32
+ {/if}
33
+
34
+ <div class="px-6 prompt-form flow" class:max-w-screen-md={!fullWidth} class:w-full={fullWidth} class:mx-auto={align === 'center'}>
35
+ <slot />
36
+ </div>
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ import InlineNotification from "carbon-components-svelte/src/Notification/InlineNotification.svelte";
3
+ import type { PromptDefinition } from '../registry'
4
+
5
+ export let def: PromptDefinition | undefined
6
+ export let appRequestId: string
7
+ export let appData: Record<string, any>
8
+ export let prompt: { key: string, answered: boolean, moot: boolean | null, invalidated: boolean | null, invalidatedReason: string | null }
9
+ export let configData: Record<string, any>
10
+ export let gatheredConfigData: Record<string, any>
11
+ export let showMoot = false
12
+ </script>
13
+
14
+ <svelte:boundary onerror={e => console.error(e)}>
15
+ {#if showMoot && prompt.moot}
16
+ <em>Already disqualified.</em>
17
+ {:else if !prompt.answered}
18
+ <em>Incomplete</em>
19
+ {:else if !def?.displayComponent}
20
+ <em>No display component registered.</em>
21
+ <pre>{JSON.stringify(appData[prompt.key] ?? {}, null, 2)}</pre>
22
+ {:else}
23
+ <svelte:component this={def.displayComponent} {appRequestId} data={appData[prompt.key]} appRequestData={appData} {configData} {gatheredConfigData} />
24
+ {/if}
25
+ {#if prompt.invalidated && (showMoot || !prompt.moot)}
26
+ <InlineNotification kind="warning" title="Correction Needed" subtitle={prompt.invalidatedReason ?? undefined} class="mt-2" lowContrast hideCloseButton />
27
+ {/if}
28
+ {#snippet failed()}
29
+ <div class="error">
30
+ <div>Error Loading Component</div>
31
+ <p>There was an error loading the display component for this prompt.</p>
32
+ </div>
33
+ {/snippet}
34
+ </svelte:boundary>
35
+
36
+ <style>
37
+ .error div { font-weight: bold; }
38
+ </style>
@@ -0,0 +1,93 @@
1
+ <script lang="ts">
2
+ import { base } from "$app/paths"
3
+ import { ColumnList, FieldDate, FieldMultiselect, Pagination, type ActionItem } from "@txstate-mws/carbon-svelte"
4
+ import { DateTime } from "luxon"
5
+ import View from 'carbon-icons-svelte/lib/View.svelte'
6
+ import DocExport from 'carbon-icons-svelte/lib/DocumentExport.svelte'
7
+ import { downloadCsv } from "../csv"
8
+ import type { AppRequest } from "../typed-client"
9
+ import { pluralize } from "txstate-utils"
10
+
11
+ export let data: AppRequest[]
12
+ export let title: string
13
+ export let subtitle: string
14
+
15
+ const selectedActions = (rows: AppRequest[]): ActionItem[] => [
16
+ {
17
+ label: `Download ${rows.length} ${pluralize('application', rows.length)}`,
18
+ // icon: TrashCan,
19
+ onClick: () => { console.log(rows); downloadCsv(formatCSVData(rows)) }
20
+ }
21
+ ]
22
+
23
+ function formatCSVData (d: AppRequest[]) {
24
+ return d.map(d => ({
25
+ Id: d.id,
26
+ Period: d.period.name,
27
+ 'TXST ID': d.applicant.otherInfo,
28
+ Name: d.applicant.fullname,
29
+ 'Date Submitted': DateTime.fromISO(d.createdAt).toFormat('f').replace(',', ''),
30
+ Benefit: `"${d.applications.map(a => a.title).join(', ')}"`,
31
+ 'Last Submitted': DateTime.fromISO(d.updatedAt).toFormat('f').replace(',', '')
32
+ }))
33
+ }
34
+ </script>
35
+
36
+ <div class="flow [ p-4 bg-gray-100 ]">
37
+ <h2 class="[ text-lg ]">{title}</h2>
38
+ <p class="[ text-gray-600 ]">{subtitle}</p>
39
+ </div>
40
+
41
+ <ColumnList
42
+ searchable
43
+ filterTitle='Request Filters'
44
+ {selectedActions}
45
+ listActions={[
46
+ { label: 'Download', icon: DocExport, onClick: () => { console.log(data); downloadCsv(formatCSVData(data)) } }
47
+ ]}
48
+ columns={[
49
+ { id: 'request', label: 'Request #', tags: (row) => [{ label: String(row.id), }] },
50
+ { id: 'period', label: 'Period', render: r => r.period.name },
51
+ { id: 'aNumber', label: 'TXST ID' },
52
+ { id: 'name', label: 'Name', get: 'applicant.fullname' },
53
+ { id: 'dateSubmitted', label: 'Date Submitted', render: r => DateTime.fromISO(r.createdAt).toFormat('f') },
54
+ { id: 'benefit', label: 'Benefit', render: r => r.applications.map(a => a.title).join(', ') },
55
+ { id: 'lastUpdated', label: 'Last Updated', render: r => DateTime.fromISO(r.updatedAt).toFormat('f') },
56
+ ]}
57
+ rows={data}
58
+ title="App Requests"
59
+
60
+ actions={r => [
61
+ {
62
+ label: 'View',
63
+ icon: View,
64
+ // href: `${base}/requests/${r.id}/apply`
65
+ href: `${base}/requests/${r.id}/approve`
66
+ }
67
+ ]}
68
+ >
69
+ <svelte:fragment slot="filters">
70
+ <FieldMultiselect
71
+ path="period"
72
+ labelText="Period"
73
+ items={[]}
74
+ placeholder="Choose one or more"
75
+ />
76
+ <FieldDate
77
+ path='dateSubmitted'
78
+ label='Date Submitted'
79
+ />
80
+ <FieldMultiselect
81
+ path="test"
82
+ labelText="Period"
83
+ items={[]}
84
+ placeholder="Choose one or more"
85
+ />
86
+ </svelte:fragment>
87
+ </ColumnList>
88
+
89
+ <Pagination
90
+ totalItems={data.length}
91
+ pageSize={25}
92
+ chooseSize
93
+ />
@@ -0,0 +1,35 @@
1
+ <script lang="ts">
2
+ import { BadgeNumber } from '@txstate-mws/carbon-svelte'
3
+ import Accordion from "carbon-components-svelte/src/Accordion/Accordion.svelte";
4
+ import AccordionItem from "carbon-components-svelte/src/Accordion/AccordionItem.svelte";
5
+
6
+ export let items: { id: string, message: string }[] = []
7
+ export let variant: 'warning' | 'error' = 'error'
8
+ export let accordionTitle = 'Multiple items'
9
+
10
+ $: badgeStyle = variant === 'warning'
11
+ ? '--badge-bg: var(--yellow-01, #F3D690); --badge-text: #6F510C'
12
+ : '--badge-bg: #FBE9EA; --badge-text: #a11c25'
13
+ </script>
14
+
15
+ {#if items.length === 1}
16
+ <div class="flex items-center">
17
+ <BadgeNumber value={1} class="mt-2 mr-2" style={badgeStyle} />
18
+ <p class="mt-2 mb-0 text-sm">{items[0].message}</p>
19
+ </div>
20
+ {:else if items.length > 1}
21
+ <div class="flex">
22
+ <BadgeNumber value={items.length} class="mt-5 mr-2" style={badgeStyle} />
23
+ <div class="mt-2 w-full">
24
+ <Accordion align="start">
25
+ <AccordionItem title={accordionTitle}>
26
+ <ol class="list-decimal">
27
+ {#each items as item (item.id)}
28
+ <li>{item.message}</li>
29
+ {/each}
30
+ </ol>
31
+ </AccordionItem>
32
+ </Accordion>
33
+ </div>
34
+ </div>
35
+ {/if}
@@ -0,0 +1,20 @@
1
+ <script lang="ts">
2
+ import WarningAltFilled from "carbon-icons-svelte/lib/WarningAltFilled.svelte";
3
+ import type { ComponentProps } from 'svelte'
4
+
5
+ interface $$Props extends ComponentProps<typeof WarningAltFilled> {}
6
+ </script>
7
+
8
+ <div>
9
+ <WarningAltFilled {...$$restProps} />
10
+ </div>
11
+
12
+ <style>
13
+ div :global(svg) {
14
+ fill: var(--cds-support-03, rgba(239, 200, 108, 1)) !important;
15
+ }
16
+
17
+ div :global([data-icon-path="inner-path"]) {
18
+ fill: black;
19
+ }
20
+ </style>