diff --git a/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses/[[...statusFilter]]/actions.ts b/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses/[[...statusFilter]]/actions.ts index 6d1c5a0f27..73a47dce2b 100644 --- a/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses/[[...statusFilter]]/actions.ts +++ b/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses/[[...statusFilter]]/actions.ts @@ -32,6 +32,8 @@ import { retrieveSubmissionRemovalDate, } from "@lib/vault"; import { transform as csvTransform } from "@lib/responseDownloadFormats/csv"; +import { transform as csvTransformV2 } from "@lib/responseDownloadFormats/csv/v2"; +import { getAnswerCounts } from "@lib/responseDownloadFormats/csv/getAnswerCounts"; import { transform as htmlAggregatedTransform } from "@lib/responseDownloadFormats/html-aggregated"; import { transform as htmlTransform } from "@lib/responseDownloadFormats/html"; import { transform as zipTransform } from "@lib/responseDownloadFormats/html-zipped"; @@ -303,6 +305,16 @@ export const getSubmissionsByFormat = AuthenticatedAction( switch (format) { case DownloadFormat.CSV: + const counts = getAnswerCounts(formResponse.submissions); + + // Use the new CSV format if the answers are not all the same length + if (!counts.allEqual) { + return { + receipt: await htmlAggregatedTransform(formResponse, lang), + responses: csvTransformV2(formResponse), + }; + } + return { receipt: await htmlAggregatedTransform(formResponse, lang), responses: csvTransform(formResponse), diff --git a/lib/responseDownloadFormats/csv/getAnswerCounts.ts b/lib/responseDownloadFormats/csv/getAnswerCounts.ts new file mode 100644 index 0000000000..7c18cb5160 --- /dev/null +++ b/lib/responseDownloadFormats/csv/getAnswerCounts.ts @@ -0,0 +1,11 @@ +import { type Submission } from "../types"; + +export const getAnswerCounts = (submissions: Submission[]) => { + const counts = submissions.map((response) => { + return response.answers.length; + }); + + const allEqual = counts.every((val, i, arr) => val === arr[0]); + + return { counts, allEqual }; +}; diff --git a/lib/responseDownloadFormats/csv/v2.ts b/lib/responseDownloadFormats/csv/v2.ts new file mode 100644 index 0000000000..b9a0958b3b --- /dev/null +++ b/lib/responseDownloadFormats/csv/v2.ts @@ -0,0 +1,84 @@ +import { createArrayCsvStringifier as createCsvStringifier } from "csv-writer"; +import { FormResponseSubmissions } from "../types"; +import { FormElementTypes } from "@lib/types"; +import { customTranslate } from "@lib/i18nHelpers"; +import { sortByLayout } from "@lib/utils/form-builder"; + +const specialChars = ["=", "+", "-", "@"]; + +export const transform = (formResponseSubmissions: FormResponseSubmissions) => { + const { t } = customTranslate("common"); + const { submissions } = formResponseSubmissions; + + const sortedElements = sortByLayout({ + layout: formResponseSubmissions.formRecord.form.layout, + elements: formResponseSubmissions.formRecord.form.elements, + }); + + const header = sortedElements.map((element) => { + return `${element.properties.titleEn}\n${element.properties.titleFr}${ + element.type === FormElementTypes.formattedDate && element.properties.dateFormat + ? "\n" + + t(`formattedDate.${element.properties.dateFormat}`, { lng: "en" }) + + "/" + + t(`formattedDate.${element.properties.dateFormat}`, { lng: "fr" }) + : "" + }`; + }); + + header.unshift( + "Submission ID / Identifiant de soumission", + "Date of submission / Date de soumission" + ); + + header.push("Receipt codes / Codes de réception"); + + const csvStringifier = createCsvStringifier({ + header: header, + alwaysQuote: true, + }); + + const records = submissions.map((response) => { + const answers = sortedElements.map((element) => { + const answer = response.answers.find((answer) => answer.questionId === element.id); + if (!answer) { + return "-"; + } + if (answer.answer instanceof Array) { + return answer.answer + .map((answer) => + answer + .map((subAnswer) => { + let answerText = `${subAnswer.questionEn}\n${subAnswer.questionFr}: ${subAnswer.answer}\n`; + if (specialChars.some((char) => answerText.startsWith(char))) { + answerText = `'${answerText}`; + } + if (answerText == "") { + answerText = "-"; + } + return answerText; + }) + .join("") + ) + .join("\n"); + } + let answerText = answer.answer; + if (specialChars.some((char) => answerText.startsWith(char))) { + answerText = `'${answerText}`; + } + if (answerText == "") { + answerText = "-"; + } + return answerText; + }); + return [ + response.id, + new Date(response.createdAt).toISOString(), + ...answers, + "Receipt codes are in the Official receipt and record of responses\n" + + "Les codes de réception sont dans le Reçu et registre officiel des réponses", + ]; + }); + + return csvStringifier.getHeaderString() + csvStringifier.stringifyRecords(records); +};