@@ -4,16 +4,19 @@ import { err, ok, Result } from 'neverthrow'
44import {
55 BasicField ,
66 FieldResponsesV3 ,
7+ FieldResponseV3 ,
78 FormFieldDto ,
89 FormWorkflowStepDto ,
910 MultirespondentSubmissionDto ,
11+ NdiResponseV3 ,
1012 PublicMultirespondentSubmissionDto ,
1113 SubmissionType ,
1214 WorkflowType ,
1315} from '../../../../../shared/types'
1416import { stripDropdownFieldOptionsToRecipientsMap } from '../../../../../shared/utils/strip-dropdown-field-optionsToRecipientsMap'
1517import { stripWorkflowEmails } from '../../../../../shared/utils/strip-workflow-emails'
1618import {
19+ EmailRespondentConfirmationField ,
1720 FormFieldSchema ,
1821 MultirespondentSubmissionData ,
1922} from '../../../../types'
@@ -27,6 +30,11 @@ import {
2730 ValidateFieldErrorV3 ,
2831} from '../submission.errors'
2932import { buildMrfMetadata } from '../submission.utils'
33+ import { startsWithSPCPFieldTitle } from '../../spcp/spcp.util'
34+ import { convertToSignaturePngDataUri } from '../../../utils/convert-vector-array-to-png'
35+ import { SIGNATURE_CAPTURED_STRING } from '../../../../../shared/utils/signature'
36+ import { handleAddressResponseDisplay } from '../../../../../shared/utils/address'
37+ import { CLIENT_CHECKBOX_OTHERS_INPUT_VALUE } from '../../../../../shared/constants'
3038
3139/**
3240 * Creates and returns a MultirespondentSubmissionDto object from submissionData and
@@ -269,3 +277,228 @@ export const extractRespondentCopyEmailDatas = ({
269277 return [ ] // no respondent copy emails found
270278 } )
271279}
280+
281+ export type QuestionAnswerPair = {
282+ question : string
283+ answer : string
284+ signatureDataPngDataUri ?: string
285+ }
286+
287+ /**
288+ * Given a single form field and its response, extracts question-answer pairs.
289+ * Used for email body/pdf outputs and individualResponsePage displays
290+ * Returns an array since some fields (e.g. table, children) will have
291+ * multiple question-answer pairs per response
292+ * @param formField - Single form field schema. Does not include Ndi responses, @see getQuestionAnswerPairsForMultipleFields on how to include ndi responses.
293+ * @param response - Response for the given form field
294+ * @returns An array of QuestionAnswer objects representing the extracted question-answer pairs for the given form field.
295+ */
296+ const getQuestionAnswerPairsForOneField = ( {
297+ formField,
298+ response,
299+ includeSignatureDataPngDataUri,
300+ } : {
301+ formField : FormFieldSchema | FormFieldDto
302+ response : FieldResponseV3
303+ includeSignatureDataPngDataUri : boolean
304+ } ) : QuestionAnswerPair [ ] => {
305+ let questionTitle = formField . title
306+ let answer = ''
307+ let answerArray : string [ ] = [ ]
308+ const questionAnswerPairs : QuestionAnswerPair [ ] = [ ]
309+
310+ switch ( response . fieldType ) {
311+ case BasicField . Attachment :
312+ questionTitle = `[Attachment] ${ questionTitle } `
313+ answer = response . answer . answer
314+ break
315+ case BasicField . Address : {
316+ const {
317+ postalCode,
318+ blockNumber,
319+ streetName,
320+ buildingName,
321+ levelNumber,
322+ unitNumber,
323+ } = response . answer . addressSubFields
324+ answerArray = [
325+ blockNumber ,
326+ streetName ,
327+ buildingName ,
328+ levelNumber ,
329+ unitNumber ,
330+ postalCode ,
331+ ] // move postal code to end of array
332+ answer = handleAddressResponseDisplay ( Object . values ( answerArray ) ) . join (
333+ ', ' ,
334+ )
335+ break
336+ }
337+ case BasicField . Email :
338+ case BasicField . Mobile :
339+ answer = response . answer . value
340+ break
341+ case BasicField . Table :
342+ if ( formField . fieldType !== BasicField . Table || ! response . answer ) break
343+ // eslint-disable-next-line no-case-declarations
344+ const idToColTitleMap = formField . columns . reduce (
345+ ( acc , col ) => {
346+ acc [ col . _id ] = col . title
347+ return acc
348+ } ,
349+ { } as Record < string , string > ,
350+ )
351+
352+ for ( const row of response . answer ) {
353+ const validColumns = Object . entries ( row ) . filter (
354+ ( [ colId ] ) => colId in idToColTitleMap ,
355+ )
356+
357+ const delimitedColumnTitles = validColumns
358+ . map ( ( [ colId ] ) => {
359+ const colTitle = idToColTitleMap [ colId ]
360+ return `${ colTitle } `
361+ } )
362+ . join ( '; ' )
363+
364+ const delimitedColumnAnswers = validColumns
365+ . map ( ( [ , colAns ] ) => colAns ?? '' )
366+ . join ( '; ' )
367+
368+ const question = `[Table] ${ formField . title } (${ delimitedColumnTitles } )`
369+ const answer = delimitedColumnAnswers
370+
371+ questionAnswerPairs . push ( {
372+ question,
373+ answer,
374+ } )
375+ }
376+ return questionAnswerPairs
377+ case BasicField . Radio :
378+ answer =
379+ 'value' in response . answer
380+ ? response . answer . value
381+ : 'othersInput' in response . answer
382+ ? response . answer . othersInput
383+ : ''
384+ break
385+ case BasicField . Checkbox :
386+ // eslint-disable-next-line no-case-declarations
387+ const selectedAnswers =
388+ ( response . answer . othersInput
389+ ? [ ...response . answer . value , response . answer . othersInput ]
390+ : [ ...response . answer . value ]
391+ ) . filter ( ( val ) => val !== CLIENT_CHECKBOX_OTHERS_INPUT_VALUE ) ?? [ ]
392+
393+ answer = selectedAnswers . toString ( )
394+ break
395+ case BasicField . Signature : {
396+ const signatureQuestionAnswer = {
397+ question : `[signature] ${ questionTitle } ` ,
398+ answer : SIGNATURE_CAPTURED_STRING ,
399+ signatureDataPngDataUri : includeSignatureDataPngDataUri
400+ ? convertToSignaturePngDataUri ( response . answer . value )
401+ : undefined ,
402+ }
403+ return [ signatureQuestionAnswer ]
404+ }
405+ case BasicField . Children :
406+ if ( ! response . answer . childFields || ! response . answer . child ) {
407+ break
408+ }
409+ for ( const [ index , child ] of response . answer . child . entries ( ) ) {
410+ questionAnswerPairs . push ( {
411+ question : `Child ${ index + 1 } : ${ response . answer . childFields . toString ( ) } ` ,
412+ answer : child
413+ ? child . toString ( )
414+ : response . answer . childFields . map ( ( ) => '' ) . toString ( ) ,
415+ } )
416+ }
417+ return questionAnswerPairs
418+ default :
419+ answer = response . answer
420+ }
421+
422+ questionAnswerPairs . push ( {
423+ question : questionTitle ,
424+ answer,
425+ } )
426+ return questionAnswerPairs
427+ }
428+
429+ /**
430+ * Given multiple form fields and their responses, extracts question-answer pairs.
431+ * @param formFields - List of form fields schemas
432+ * @param responses - Corresponding list of responses to the given form fields
433+ * @returns An array of QuestionAnswer pairs representing the extracted question-answer pairs for the all the given form fields.
434+ */
435+ export const getQuestionAnswerPairsForMultipleFields = ( {
436+ formFields,
437+ responses,
438+ includeSignatureDataPngDataUri = false ,
439+ } : {
440+ formFields : FormFieldSchema [ ] | FormFieldDto [ ]
441+ responses : FieldResponsesV3
442+ includeSignatureDataPngDataUri ?: boolean
443+ } ) : QuestionAnswerPair [ ] => {
444+ const questionAnswerPairs : QuestionAnswerPair [ ] = [ ]
445+ if ( ! formFields || ! responses ) {
446+ return [ ]
447+ }
448+ for ( const currentFormField of formFields ) {
449+ const questionTitle = currentFormField . title
450+ const response = responses [ currentFormField . _id ]
451+
452+ if ( ! response || ! questionTitle ) continue
453+ const questionAnswerPairsForCurrentFormField =
454+ getQuestionAnswerPairsForOneField ( {
455+ formField : currentFormField ,
456+ response,
457+ includeSignatureDataPngDataUri,
458+ } )
459+
460+ questionAnswerPairs . push ( ...questionAnswerPairsForCurrentFormField )
461+ }
462+
463+ // Add Ndi responses if they exist
464+ for ( const key in responses ) {
465+ if ( startsWithSPCPFieldTitle ( key ) ) {
466+ const value = responses [ key ] as NdiResponseV3
467+ questionAnswerPairs . push ( {
468+ question : key ,
469+ answer : value . answer ,
470+ } )
471+ }
472+ }
473+ return questionAnswerPairs
474+ }
475+
476+ /**
477+ * Prepares responses data from MRF responses to PDF html format
478+ * @param formFields - The form fields schema
479+ * @param responses - The mrf responses to the form fields
480+ * @returns list of EmailRespondentConfirmationField used for email & pdf generation
481+ */
482+ export const getPdfResponsesData = ( {
483+ formFields,
484+ responses,
485+ } : {
486+ formFields : FormFieldSchema [ ] | FormFieldDto [ ]
487+ responses : FieldResponsesV3
488+ } ) : EmailRespondentConfirmationField [ ] => {
489+ if ( ! formFields || ! responses ) return [ ]
490+
491+ const questionAnswerPairs = getQuestionAnswerPairsForMultipleFields ( {
492+ formFields,
493+ responses,
494+ includeSignatureDataPngDataUri : true ,
495+ } )
496+
497+ return questionAnswerPairs . map ( ( questionAnswerPair ) => {
498+ return {
499+ question : questionAnswerPair . question ,
500+ answerTemplate : [ questionAnswerPair . answer ] ,
501+ answer : questionAnswerPair . signatureDataPngDataUri ,
502+ }
503+ } )
504+ }
0 commit comments