Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: mrf dashboard send reminders #8134

Open
wants to merge 25 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
773408a
feat: add reminders column, button and center align row items in y-axis
kevin9foong Feb 26, 2025
11ae33e
feat: add reminder button isSent state
kevin9foong Feb 26, 2025
9003cc2
chore: remove unused imports
kevin9foong Feb 26, 2025
dc4c9fc
feat: add reminder fe api call
kevin9foong Feb 26, 2025
60924d3
feat: create backend route and validation for sendReminder
kevin9foong Feb 26, 2025
f0f4140
feat: add be reminder logic and filter for if recipient email is empty
kevin9foong Feb 27, 2025
54d3ade
feat: reuse previous retrieveFormById method
kevin9foong Feb 27, 2025
0c48769
feat: update mrfmeta
kevin9foong Feb 27, 2025
c09bf96
feat: implement reusable decrypt hook
kevin9foong Feb 27, 2025
6587696
feat: fix emails not saved for submission updates and add skeleton tcs
kevin9foong Feb 27, 2025
6101d05
fix: failing FE tc
kevin9foong Feb 27, 2025
c895f7f
feat: implement tc for controller methods
kevin9foong Feb 27, 2025
afc6fd8
feat: add service tc
kevin9foong Feb 27, 2025
88a3977
feat: add tc for hasNextStepRecipientEmail and fix bug
kevin9foong Feb 27, 2025
eccc303
feat: fix failing tc
kevin9foong Feb 27, 2025
75b1a72
fix: remove unused import ok
kevin9foong Feb 27, 2025
bba69b9
fix: import to relative
kevin9foong Feb 27, 2025
87ab628
feat: adjust table spacing
kevin9foong Feb 27, 2025
f07390c
feat: disable focus
kevin9foong Feb 27, 2025
805b13f
feat: add rate limit
kevin9foong Feb 28, 2025
fc37adb
feat: reduce rate limit
kevin9foong Feb 28, 2025
2c75d1a
feat: add usage tracking
kevin9foong Feb 28, 2025
ded597f
feat: add dummy key for storybook to resolve submissionSecretKey
kevin9foong Feb 28, 2025
4bd6739
feat: update loading state for submissionsecretkey and fix imports to…
kevin9foong Feb 28, 2025
c78fca1
fix: remove check for submissionSecretKey to remove hacky isTest check
kevin9foong Feb 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions frontend/src/features/admin-form/common/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
} from '../common/AdminViewFormService'
import { downloadFormIssue } from '../responses/FeedbackPage/issue/IssueService'
import { downloadFormReview } from '../responses/FeedbackPage/review/ReviewService'
import { sendReminderForPendingMrfResponse } from '../responses/ResponsesPage/storage/UnlockedResponses/ResponsesTable/reminders/ReminderService'

import { useCollaboratorWizard } from './components/CollaboratorModal/CollaboratorWizardContext'
import { permissionsToRole } from './components/CollaboratorModal/utils'
Expand Down Expand Up @@ -553,3 +554,45 @@ export const useFormIssueMutations = () => {

return { downloadFormIssueMutation: downloadFormIssuesMutation }
}

export const useFormRemindersMutations = () => {
const toast = useToast({ status: 'success', isClosable: true })
const handleError = useCallback(
(error: Error) => {
toast.closeAll()
toast({
description: error.message,
status: 'danger',
})
},
[toast],
)

const sendReminderForResponseMutation = useMutation(
({
formId,
submissionId,
submissionSecretKey,
}: {
formId: string
submissionId: string
submissionSecretKey: string
}) => {
return sendReminderForPendingMrfResponse({
formId,
submissionId,
submissionSecretKey,
})
},
{
onSuccess: () => {
toast({
description: 'Your reminder has been sent',
})
},
onError: handleError,
},
)

return { sendReminderForResponseMutation }
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,9 @@ import { useCallback } from 'react'
import { useMutation } from 'react-query'
import FileSaver from 'file-saver'

import { BasicField } from '~shared/types'

import { useToast } from '~hooks/useToast'

import { AttachmentsDownloadMap } from '../ResponsesPage/storage/types'
import { AugmentedDecryptedResponse } from '../ResponsesPage/storage/utils/augmentDecryptedResponses'
import {
downloadAndDecryptAttachment,
downloadAndDecryptAttachmentsAsZip,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,27 @@ import { useStorageResponsesContext } from '../ResponsesPage/storage'
* @precondition Must be wrapped in a Router as `useParam` is used.
*/
export const useIndividualSubmission = () => {
const toast = useToast({
status: 'danger',
})

const { formId, submissionId } = useParams()
const { data: { responseMode } = {} } = useAdminForm()

if (!formId || !submissionId) {
throw new Error('No formId or submissionId provided')
}

return useGetIndividualDecryptedSubmission({ formId, submissionId })
}

export const useGetIndividualDecryptedSubmission = ({
formId,
submissionId,
}: {
formId: string
submissionId: string
}) => {
const toast = useToast({
status: 'danger',
})
const { secretKey } = useStorageResponsesContext()
const { data: { responseMode } = {} } = useAdminForm()

return useQuery(
adminFormResponsesKeys.individual(formId, submissionId),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ import { useAdminForm } from '~features/admin-form/common/queries'
import { getPendingResponseAtString } from '~features/admin-form/responses/common/utils/mrfSubmissionView'
import {
MRF_PENDING_RESPONSE_AT_LABEL,
MRF_REMINDERS_LABEL,
MRF_RESPONSE_TIMESTAMP_LABEL,
MRF_WORKFLOW_STATUS_LABEL,
} from '~features/admin-form/responses/constants'

import { useUnlockedResponses } from '../UnlockedResponsesProvider'

import { SendReminderButton } from './SendReminderButton'
import { getNetAmount } from './utils'

type ResponseColumnData = SubmissionMetadata
Expand Down Expand Up @@ -190,9 +192,9 @@ const MRF_RESPONSE_TABLE_COLUMNS: Column<ResponseColumnData>[] = [
{
Header: 'Response ID',
accessor: 'refNo',
width: 300,
minWidth: 300,
maxWidth: 300,
width: 240,
minWidth: 240,
maxWidth: 240,
},
{
Header: MRF_WORKFLOW_STATUS_LABEL,
Expand All @@ -213,9 +215,9 @@ const MRF_RESPONSE_TABLE_COLUMNS: Column<ResponseColumnData>[] = [
return <CompletedBadge />
}
},
width: 200,
minWidth: 180,
maxWidth: 220,
width: 160,
minWidth: 160,
maxWidth: 160,
},
{
Header: MRF_PENDING_RESPONSE_AT_LABEL,
Expand All @@ -236,9 +238,9 @@ const MRF_RESPONSE_TABLE_COLUMNS: Column<ResponseColumnData>[] = [
workflowNumTotalSteps,
})
},
width: 200,
width: 180,
minWidth: 180,
maxWidth: 220,
maxWidth: 180,
},
{
Header: MRF_RESPONSE_TIMESTAMP_LABEL,
Expand All @@ -252,9 +254,24 @@ const MRF_RESPONSE_TABLE_COLUMNS: Column<ResponseColumnData>[] = [
// 'do MMM yyyy, hh:mm:ss a',
// )
// : '',
width: 250,
minWidth: 250,
disableResizing: true,
width: 240,
minWidth: 240,
maxWidth: 240,
},
{
Header: MRF_REMINDERS_LABEL,
Cell: ({ row }) => {
const isPending =
row.original.mrf?.workflowStatus === WorkflowStatus.PENDING
const hasNextStepRecipientEmails =
row.original.mrf?.hasNextStepRecipientEmails
const submissionId = row.original.refNo
return isPending && hasNextStepRecipientEmails ? (
<SendReminderButton submissionId={submissionId} />
) : null
},
minWidth: 160,
width: 160,
},
]

Expand Down Expand Up @@ -427,6 +444,8 @@ export const ResponsesTable = () => {
as="div"
{...cell.getCellProps()}
key={cell.getCellProps().key}
display="flex"
alignItems="center"
>
{cell.render('Cell')}
</Td>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useState } from 'react'
import { BiBell, BiCheck } from 'react-icons/bi'
import { useParams } from 'react-router-dom'
import { Text } from '@chakra-ui/react'

import Button from '~components/Button'

import { useFormRemindersMutations } from '~features/admin-form/common/mutations'
import { useGetIndividualDecryptedSubmission } from '~features/admin-form/responses/IndividualResponsePage/queries'

export const SendReminderButton = ({
submissionId,
}: {
submissionId: string
}) => {
const { formId = '' } = useParams()

const { sendReminderForResponseMutation } = useFormRemindersMutations()

const sendReminderForResponse = sendReminderForResponseMutation

const [isSent, setIsSent] = useState(false)

const { data: submissionData, isLoading: isLoadingSubmissionData } =
useGetIndividualDecryptedSubmission({
formId,
submissionId,
})
const submissionSecretKey = submissionData?.submissionSecretKey

if (!formId) {
return null
}

return !isSent ? (
<Button
isLoading={isLoadingSubmissionData}
loadingText={isLoadingSubmissionData ? 'Loading' : 'Sending'}
m="0"
p="0"
variant="clear"
leftIcon={<BiBell />}
_focus={{}}
onClick={(e) => {
e.stopPropagation()
if (!submissionSecretKey) {
return
}
sendReminderForResponse.mutate({
formId,
submissionId,
submissionSecretKey,
})
setIsSent(true)
}}
>
<Text textStyle="subhead-2">Send reminder</Text>
</Button>
) : (
<Button variant="clear" m="0" p="0" leftIcon={<BiCheck />} isDisabled>
<Text textStyle="subhead-2">Reminder sent</Text>
</Button>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ApiService } from '~services/ApiService'

import { ADMIN_FORM_ENDPOINT } from '~features/admin-form/common/AdminViewFormService'

/**
* Trigger a reminder for the pending step.
* @param formId the id of the form
* @param submissionId the id of the submission to send reminder for
*/
export const sendReminderForPendingMrfResponse = async ({
formId,
submissionId,
submissionSecretKey,
}: {
formId: string
submissionId: string
submissionSecretKey: string
}): Promise<void> => {
return ApiService.post<void>(
`${ADMIN_FORM_ENDPOINT}/${formId}/submissions/${submissionId}/remind`,
{
submissionSecretKey,
},
).then(() => {})
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { WorkflowStatus } from '~shared/types'

import {
MRF_PENDING_RESPONSE_AT_LABEL,
MRF_REMINDERS_LABEL,
MRF_WORKFLOW_STATUS_LABEL,
} from '~features/admin-form/responses/constants'

Expand All @@ -26,6 +27,7 @@ describe('CsvRecord', () => {
workflowCurrentStepNumber: 1,
workflowNumTotalSteps: 2,
lastSubmittedAt: '2025-02-17T00:00:00.000Z',
hasNextStepRecipientEmails: false,
},
)

Expand All @@ -39,6 +41,9 @@ describe('CsvRecord', () => {
expect.objectContaining({ question: MRF_WORKFLOW_STATUS_LABEL }),
expect.objectContaining({ question: MRF_PENDING_RESPONSE_AT_LABEL }),
])
expect(recordResult).not.toContainEqual(
expect.objectContaining({ question: MRF_REMINDERS_LABEL }),
)
})
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,7 @@ import { SetOptional } from 'type-fest'
import { WorkflowStatus } from '~shared/types/submission'
import { answerKey } from '~shared/utils/address'

import {
getPendingResponseAtString,
MRF_STATUS,
} from '~features/admin-form/responses/common/utils/mrfSubmissionView'
import {
MRF_PENDING_RESPONSE_AT_LABEL,
MRF_RESPONSE_TIMESTAMP_LABEL,
MRF_WORKFLOW_STATUS_LABEL,
} from '~features/admin-form/responses/constants'
import { MRF_RESPONSE_TIMESTAMP_LABEL } from '~features/admin-form/responses/constants'

import { CsvRecordData, DecryptedSubmissionData } from '../../types'
import {
Expand Down Expand Up @@ -388,6 +380,7 @@ describe('EncryptedResponseCsvGenerator', () => {
workflowCurrentStepNumber: 2,
workflowNumTotalSteps: 3,
lastSubmittedAt: '2024-01-01T00:00:00.000Z',
hasNextStepRecipientEmails: false,
},
}
mrfGenerator.addRecord(mockRecord)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ async function decryptIntoCsv(
submission.mrfMeta.workflowCurrentStepNumber,
workflowNumTotalSteps: submission.mrfMeta.workflowNumTotalSteps,
lastSubmittedAt: submission.mrfMeta.lastSubmittedAt,
hasNextStepRecipientEmails:
submission.mrfMeta.hasNextStepRecipientEmails,
}
: undefined,
)
Expand Down
1 change: 1 addition & 0 deletions frontend/src/features/admin-form/responses/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const MRF_RESPONSE_TIMESTAMP_LABEL = 'Response timestamp'
export const MRF_PENDING_RESPONSE_AT_LABEL = 'Pending response at'
export const MRF_WORKFLOW_STATUS_LABEL = 'Workflow status'
export const MRF_REMINDERS_LABEL = 'Reminders'
5 changes: 5 additions & 0 deletions frontend/src/mocks/msw/handlers/admin-form/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,7 @@ const DEFAULT_MULTIRESPONDENT_METADATA = [
workflowCurrentStepNumber: 1,
workflowNumTotalSteps: 5,
workflowStatus: WorkflowStatus.PENDING,
hasNextStepRecipientEmails: true,
},
},
{
Expand All @@ -611,6 +612,7 @@ const DEFAULT_MULTIRESPONDENT_METADATA = [
workflowCurrentStepNumber: 3,
workflowNumTotalSteps: 3,
workflowStatus: WorkflowStatus.APPROVED,
hasNextStepRecipientEmails: true,
},
},
{
Expand All @@ -621,6 +623,7 @@ const DEFAULT_MULTIRESPONDENT_METADATA = [
workflowCurrentStepNumber: 2,
workflowNumTotalSteps: 3,
workflowStatus: WorkflowStatus.REJECTED,
hasNextStepRecipientEmails: true,
},
},
{
Expand All @@ -631,6 +634,7 @@ const DEFAULT_MULTIRESPONDENT_METADATA = [
workflowCurrentStepNumber: 4,
workflowNumTotalSteps: 4,
workflowStatus: WorkflowStatus.COMPLETED,
hasNextStepRecipientEmails: true,
},
},
// simulates a submission prior to https://github.com/opengovsg/FormSG/pull/7965
Expand All @@ -643,6 +647,7 @@ const DEFAULT_MULTIRESPONDENT_METADATA = [
workflowCurrentStepNumber: 1,
workflowNumTotalSteps: 4,
workflowStatus: undefined,
hasNextStepRecipientEmails: true,
},
},
],
Expand Down
1 change: 1 addition & 0 deletions shared/types/form/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export enum FormResponseMode {

export interface FormMetadata {
mfb_text_prompt_count?: number
num_mrf_reminder_emails_sent?: number
}

export type FormPaymentsChannel = {
Expand Down
Loading
Loading