diff --git a/api/src/appRequest/appRequest.database.ts b/api/src/appRequest/appRequest.database.ts index 14e6bbb..7c996d8 100644 --- a/api/src/appRequest/appRequest.database.ts +++ b/api/src/appRequest/appRequest.database.ts @@ -603,7 +603,7 @@ export async function evaluateAppRequest (appRequestInternalId: number, tdb?: Qu hasUnanswered ||= !anyOrderAllAnswered for (const prompt of regularPrompts) { prompt.moot = applicationIsIneligible && requirement.type !== RequirementType.WORKFLOW - if (hasUnanswered || resolveInfo.status !== RequirementStatus.PENDING) prompt.visibility = PromptVisibility.UNREACHABLE + if ((hasUnanswered || resolveInfo.status !== RequirementStatus.PENDING) && !promptRegistry.get(prompt.key).optOut) prompt.visibility = PromptVisibility.UNREACHABLE else { if (promptsSeenInApplication.has(prompt.key)) prompt.visibility = PromptVisibility.APPLICATION_DUPE else if (promptsSeenInRequest.has(prompt.key)) prompt.visibility = PromptVisibility.REQUEST_DUPE @@ -683,13 +683,15 @@ export async function evaluateAppRequest (appRequestInternalId: number, tdb?: Qu if (phase === 'acceptance') application.ineligiblePhase = IneligiblePhases.ACCEPTANCE else if (phase === 'applicant') application.ineligiblePhase = firstFailingRequirement?.type === RequirementType.PREQUAL ? IneligiblePhases.PREQUAL : IneligiblePhases.QUALIFICATION else if (phase === 'review') { - application.ineligiblePhase = - firstFailingRequirement?.type === RequirementType.PREQUAL ? IneligiblePhases.PREQUAL : - firstFailingRequirement?.type === RequirementType.QUALIFICATION ? IneligiblePhases.QUALIFICATION : - firstFailingRequirement?.type === RequirementType.PREAPPROVAL ? IneligiblePhases.PREAPPROVAL : - IneligiblePhases.APPROVAL - } - else if (phase === 'blocking') application.ineligiblePhase ??= IneligiblePhases.WORKFLOW + application.ineligiblePhase + = firstFailingRequirement?.type === RequirementType.PREQUAL + ? IneligiblePhases.PREQUAL + : firstFailingRequirement?.type === RequirementType.QUALIFICATION + ? IneligiblePhases.QUALIFICATION + : firstFailingRequirement?.type === RequirementType.PREAPPROVAL + ? IneligiblePhases.PREAPPROVAL + : IneligiblePhases.APPROVAL + } else if (phase === 'blocking') application.ineligiblePhase ??= IneligiblePhases.WORKFLOW } else if (phase !== 'nonblocking' && phase !== 'blocking' && phase !== 'complete') { application.ineligiblePhase = undefined } diff --git a/api/src/prompt/prompt.resolver.ts b/api/src/prompt/prompt.resolver.ts index 8220fd3..cc135bd 100644 --- a/api/src/prompt/prompt.resolver.ts +++ b/api/src/prompt/prompt.resolver.ts @@ -60,6 +60,11 @@ export class RequirementPromptResolver { async prestage (@Ctx() ctx: RQContext, @Root() requirementPrompt: RequirementPrompt) { return await ctx.svc(RequirementPromptService).requiresStaging(requirementPrompt) } + + @FieldResolver(type => Boolean) + async optOut (@Ctx() ctx: RQContext, @Root() requirementPrompt: RequirementPrompt) { + return !!promptRegistry.get(requirementPrompt.key).optOut + } } @Resolver(of => RequirementPromptActions) diff --git a/api/src/registry/prompt.ts b/api/src/registry/prompt.ts index a68b8e9..612b9dd 100644 --- a/api/src/registry/prompt.ts +++ b/api/src/registry/prompt.ts @@ -412,6 +412,10 @@ export interface PromptDefinition diff --git a/demos/src/rc/definitions/programs.ts b/demos/src/rc/definitions/programs.ts index 7fc24a3..9b21fd5 100644 --- a/demos/src/rc/definitions/programs.ts +++ b/demos/src/rc/definitions/programs.ts @@ -17,6 +17,7 @@ export const softwareDevelopment: ProgramDefinition = { key: 'software_development', title: 'Software Development', requirementKeys: [ + 'opt_out_req', 'step1_prequal_req', 'data_related_puzzle_req', 'assess_data_related_puzzle_req', diff --git a/demos/src/rc/definitions/prompts/softwareDevelopment.prompts.ts b/demos/src/rc/definitions/prompts/softwareDevelopment.prompts.ts index 23276a4..88573a6 100644 --- a/demos/src/rc/definitions/prompts/softwareDevelopment.prompts.ts +++ b/demos/src/rc/definitions/prompts/softwareDevelopment.prompts.ts @@ -2,6 +2,18 @@ import { PromptDefinition } from '@reqquest/api' import { MutationMessageType } from '@txstate-mws/graphql-server' import { AssessCriticalThinkingPromptData, AssessCriticalThinkingSchema, AssessOutsideClassExamplePromptData, AssessOutsideClassExampleSchema, AssessPuzzleSolutionPromptData, AssessPuzzleSolutionSchema, CriticalThinkingPromptData, CriticalThinkingSchema, DataRelatedPuzzlePromptData, DataRelatedPuzzleSchema, OutsideClassExamplePromptData, OutsideClassExampleSchema } from '../models/index.js' import { fileHandler } from 'fastify-txstate' +import { OptOutData, OptOutSchema } from '../models/optOut.models.js' + +export const opt_out_prompt: PromptDefinition = { + key: 'opt_out_prompt', + title: 'Software development', + description: 'Opt Out', + schema: OptOutSchema, + optOut: true, + validate: (data, config) => { + return [] + } +} export const data_related_puzzle_prompt: PromptDefinition = { key: 'data_related_puzzle_prompt', @@ -48,15 +60,12 @@ export const outside_class_example_prompt: PromptDefinition { const messages = [] - console.log(data) - console.log(!data.outsideClassExample) if (!data.outsideClassExample) { messages.push({ type: MutationMessageType.warning, message: 'Might want to make something up', arg: 'outsideClassExample' }) } if (data.description && data.description == null) { messages.push({ type: MutationMessageType.error, message: 'Please add description.', arg: 'description' }) } - console.log(messages) return messages } } diff --git a/demos/src/rc/definitions/requirements/prequal.requirements.ts b/demos/src/rc/definitions/requirements/prequal.requirements.ts index dc749b4..fe33534 100644 --- a/demos/src/rc/definitions/requirements/prequal.requirements.ts +++ b/demos/src/rc/definitions/requirements/prequal.requirements.ts @@ -9,7 +9,6 @@ export const step1_prequal_req: RequirementDefinition = { description: 'Pre qualification requirements', promptKeys: ['pre_qual_prompt'], resolve: (data, config) => { - console.log(data) const preQualPromptData = data['pre_qual_prompt'] as PreQualPromptData if (preQualPromptData?.availability == null) return { status: RequirementStatus.PENDING } diff --git a/demos/src/rc/definitions/requirements/projectManagement.requirements.ts b/demos/src/rc/definitions/requirements/projectManagement.requirements.ts index 0d98cb9..6697189 100644 --- a/demos/src/rc/definitions/requirements/projectManagement.requirements.ts +++ b/demos/src/rc/definitions/requirements/projectManagement.requirements.ts @@ -10,7 +10,6 @@ export const communication_req: RequirementDefinition = { promptKeys: ['communication_prompt'], resolve: (data, config) => { const writtenAutomationData = data['communication_prompt'] as CommunicationData - console.log(writtenAutomationData, '🚀🚀🚀🚀🚀🚀') if (writtenAutomationData?.describeCommunication == null) return { status: RequirementStatus.PENDING } return { status: RequirementStatus.MET } } diff --git a/demos/src/rc/definitions/requirements/softwareDevelopment.requirements.ts b/demos/src/rc/definitions/requirements/softwareDevelopment.requirements.ts index 0d5eab4..eae93a6 100644 --- a/demos/src/rc/definitions/requirements/softwareDevelopment.requirements.ts +++ b/demos/src/rc/definitions/requirements/softwareDevelopment.requirements.ts @@ -1,5 +1,20 @@ import { RequirementDefinition, RequirementStatus, RequirementType } from '@reqquest/api' import { DataRelatedPuzzlePromptData, AssessPuzzleSolutionPromptData, OutsideClassExamplePromptData, AssessOutsideClassExamplePromptData, CriticalThinkingPromptData, AssessCriticalThinkingPromptData } from '../models' +import { OptOutData } from '../models/optOut.models' + +export const opt_out_req: RequirementDefinition = { + type: RequirementType.QUALIFICATION, + key: 'opt_out_req', + title: 'Opt Out', + navTitle: 'Opt Out', + description: 'Opt Out', + promptKeys: ['opt_out_prompt'], + resolve: (data, config) => { + const promptData = data['opt_out_prompt'] as OptOutData + if (promptData?.optOut) return { status: RequirementStatus.DISQUALIFYING } + return { status: RequirementStatus.NOT_APPLICABLE } + } +} export const data_related_puzzle_req: RequirementDefinition = { type: RequirementType.QUALIFICATION, diff --git a/ui/src/internal/api.ts b/ui/src/internal/api.ts index 18463bd..f455b9c 100644 --- a/ui/src/internal/api.ts +++ b/ui/src/internal/api.ts @@ -492,7 +492,8 @@ class API extends APIBase { invalidatedReason: true, configurationData: true, gatheredConfigData: true, - prestage: true + prestage: true, + optOut: true } } }, diff --git a/ui/src/internal/components/ApplicantOptOutModal.svelte b/ui/src/internal/components/ApplicantOptOutModal.svelte new file mode 100644 index 0000000..99ef864 --- /dev/null +++ b/ui/src/internal/components/ApplicantOptOutModal.svelte @@ -0,0 +1,73 @@ + + +{#if loading} + +{/if} + + { open = false }} + on:validate={onValidate} + {submit} + title={`${optIn ? 'Opt in to' : 'Opt out of'} ${optOutSelected?.title}?`} + submitText={optIn ? 'Opt in' : 'Opt out'} + cancelText="Cancel" + preload={prompt.preloadData} + preloadAsDraft={!prompt.hasSavedData} + > + + diff --git a/ui/src/internal/components/ApplicantProgramList.svelte b/ui/src/internal/components/ApplicantProgramList.svelte index 115aac9..0ef8140 100644 --- a/ui/src/internal/components/ApplicantProgramList.svelte +++ b/ui/src/internal/components/ApplicantProgramList.svelte @@ -1,17 +1,38 @@
@@ -48,11 +104,20 @@ {#each applications as application (application.id)} {@const programStatus = programButtonStatus[application.id]} {@const programFirstPrompt = programFirstPromptId[application.id]} -
{application.title}
+
+ {application.title} + {#if optedOutPrograms[application.id]} + + {:else if optOutPrograms[application.id]} + + {/if} +
{#if !viewMode}
- {#if application.completionStatus === enumApplicationStatus.INELIGIBLE} + {#if optedOutPrograms[application.id]} + + {:else if application.completionStatus === enumApplicationStatus.INELIGIBLE} {:else if ['start', 'continue'].includes(programStatus)} @@ -63,7 +128,9 @@ {/if}
- {#if programFirstPrompt && programStatus !== 'ineligible'} + {#if optedOutPrograms[application.id]} +

Opted out

+ {:else if programFirstPrompt && programStatus !== 'ineligible'} {/if} {:else} @@ -89,6 +156,11 @@ {/each}
+ +{#if optOutPrompt?.key} + +{/if} + diff --git a/ui/src/lib/registry.ts b/ui/src/lib/registry.ts index 6d99aac..627b748 100644 --- a/ui/src/lib/registry.ts +++ b/ui/src/lib/registry.ts @@ -97,6 +97,10 @@ export interface PromptDefinition { * An icon for the navigation. */ icon?: Component + /** + * Updating UI to show opt out + */ + optOut?: boolean } export interface Terminologies { diff --git a/ui/src/lib/types.ts b/ui/src/lib/types.ts index 2f03b67..a4ef069 100644 --- a/ui/src/lib/types.ts +++ b/ui/src/lib/types.ts @@ -12,6 +12,7 @@ export interface AnsweredPrompt { invalidatedReason: string | null moot: boolean | null visibility: string + optOut?: boolean | null configurationData: Record gatheredConfigData: Record statusReasons: { @@ -61,3 +62,5 @@ export interface ApplicationForDetails { export const phaseChangeMutations = ['submitAppRequest', 'returnToApplicant', 'completeReview', 'returnToReview', 'acceptOffer', 'returnToOffer', 'completeRequest', 'returnToNonBlocking'] as const export type PhaseChangeMutations = typeof phaseChangeMutations[number] + +export type OptOutApplication = ApplicationForDetails & { prompt: AnsweredPrompt } diff --git a/ui/src/local/index.ts b/ui/src/local/index.ts index 3ccbcc8..4055417 100644 --- a/ui/src/local/index.ts +++ b/ui/src/local/index.ts @@ -155,6 +155,8 @@ import AssessMaintainSysDocumentationDisplay from './rc/AssessMaintainSysDocumen import ReccomendationLetterDisplay from './rc/ReccomendationLetterDisplay.svelte' import AssessReccomendationLetterDisplay from './rc/AssessReccomendationLetterDisplay.svelte' import DataRelatedPuzzleDisplay from './rc/DataRelatedPuzzleDisplay.svelte' +import OptOut from './rc/OptOut.svelte' +import OptOutDisplay from './rc/OptOutDisplay.svelte' /** RC */ @@ -527,6 +529,11 @@ function configureDemoInstanceParams () { key: 'assess_reccomendation_letter_prompt', formComponent: AssessReccomendationLetter, displayComponent: AssessReccomendationLetterDisplay + }, + { + key: 'opt_out_prompt', + formComponent: OptOut, + displayComponent: OptOutDisplay } ] } diff --git a/ui/src/local/rc/FileDisplay.svelte b/ui/src/local/rc/FileDisplay.svelte index 2ca9d78..6c4db52 100644 --- a/ui/src/local/rc/FileDisplay.svelte +++ b/ui/src/local/rc/FileDisplay.svelte @@ -38,7 +38,6 @@ const imgBlob: Blob = await res.blob() objURL = URL.createObjectURL(imgBlob); - console.log(objURL) modalOpen = true } catch (err) { diff --git a/ui/src/local/rc/OptOut.svelte b/ui/src/local/rc/OptOut.svelte new file mode 100644 index 0000000..904fe0b --- /dev/null +++ b/ui/src/local/rc/OptOut.svelte @@ -0,0 +1,16 @@ + +{#if wasOptedOut} +

By opting back in, you will be eligible for this program.

+{:else} +

By opting-out, you will not be eligible for this program unless you opt back in before submitting your request.

+{/if} +
+ + + +
diff --git a/ui/src/local/rc/OptOutDisplay.svelte b/ui/src/local/rc/OptOutDisplay.svelte new file mode 100644 index 0000000..460db47 --- /dev/null +++ b/ui/src/local/rc/OptOutDisplay.svelte @@ -0,0 +1,7 @@ + + +Opted Out: {booleanToWord(data?.optOut)} diff --git a/ui/src/local/rc/PreQualPrompt.svelte b/ui/src/local/rc/PreQualPrompt.svelte index 52fc799..ab02f9c 100644 --- a/ui/src/local/rc/PreQualPrompt.svelte +++ b/ui/src/local/rc/PreQualPrompt.svelte @@ -3,7 +3,6 @@ import type { RCPreQual } from './types.js' export let fetched: string[] export let data: Partial - console.log(fetched) diff --git a/ui/src/routes/requests/[id]/apply/+layout.svelte b/ui/src/routes/requests/[id]/apply/+layout.svelte index 8f697ca..2aa37b0 100644 --- a/ui/src/routes/requests/[id]/apply/+layout.svelte +++ b/ui/src/routes/requests/[id]/apply/+layout.svelte @@ -75,6 +75,7 @@ for (const requirement of application.requirements) { if (!submissionRequirementTypes.has(requirement.type)) continue for (const prompt of requirement.prompts) { + if (prompt.optOut) continue if (foundCurrent) { nextHref = resolve(`/requests/${appRequestForExport.id}/apply/${prompt.id}`) foundCurrent = false