Skip to content

Commit

Permalink
feat(app): finalize survey entries download (#190)
Browse files Browse the repository at this point in the history
feat(app): finalize survey entries CSV download
  • Loading branch information
ivangabriele committed Dec 23, 2022
1 parent 33b9afd commit 65643e8
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 92 deletions.
7 changes: 4 additions & 3 deletions app/atoms/AdminHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import styled from 'styled-components'

export const AdminHeader = styled.div`
align-items: center;
align-items: flex-start;
display: flex;
justify-content: space-between;
height: 2.5rem;
margin-bottom: 1.5rem;
min-height: 2.5rem;
h1 {
line-height: 1;
line-height: 1.5;
padding: 0 1.5rem 0 0.5rem;
}
`
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"ajv": "8.11.0",
"bhala": "3.0.4",
"cors": "2.8.5",
"csv-stringify": "6.2.3",
"cuid": "2.1.8",
"dayjs": "1.11.3",
"dotenv": "16.0.1",
Expand Down Expand Up @@ -92,7 +93,6 @@
"styled-components": "5.3.3",
"type-fest": "2.13.1",
"typescript": "4.7.4",
"xlsx": "0.17.4",
"yup": "0.32.9",
"zxcvbn": "4.4.2"
},
Expand Down
100 changes: 100 additions & 0 deletions pages/api/surveys/[surveyId]/entries/download.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { ApiError } from '@api/libs/ApiError'
import { prisma } from '@api/libs/prisma'
import { handleAuth } from '@api/middlewares/withAuth/handleAuth'
import { handleApiEndpointError } from '@common/helpers/handleApiEndpointError'
import { UserRole } from '@prisma/client'
import { TellMe } from '@schemas/1.0.0/TellMe'
import { stringify } from 'csv-stringify'
import { keys, uniq } from 'ramda'

import type { NextApiRequest, NextApiResponse } from 'next'

const ERROR_PATH = 'pages/api/surveys/[surveyId]/entries/download.ts'

async function generateCsvFromObject(data: Array<Record<string, number | string | undefined>>): Promise<string> {
return new Promise((resolve, reject) => {
stringify(
data,
{
header: true,
quoted_empty: true,
quoted_string: true,
},
(err, dataAsCsv) => {
if (err) {
reject(err)

return
}

resolve(dataAsCsv)
},
)
})
}

export default async function SurveyEntriesDownloadEndpoint(req: NextApiRequest, res: NextApiResponse) {
switch (req.method) {
case 'GET':
try {
await handleAuth(req, res, [UserRole.ADMINISTRATOR, UserRole.MANAGER], true)

const { surveyId } = req.query
if (typeof surveyId !== 'string') {
throw new ApiError('`surveyId` must be a string.', 422, true)
}

const maybeSurvey = await prisma.survey.findUnique({
where: {
id: surveyId,
},
})
if (maybeSurvey === null) {
throw new ApiError('Not found.', 404, true)
}

const data = maybeSurvey.data as TellMe.Data
const dataAsFlatObject = data.entries.map(({ answers, id, openedAt, submittedAt }) =>
answers
.filter(({ type }) => type !== 'file')
.reduce(
(prev, { question, rawValue }) => ({
...prev,
[question.value]: rawValue,
}),
{
/* eslint-disable sort-keys-fix/sort-keys-fix */
ID: id,
'Opened At': openedAt,
'Submitted At': submittedAt,
/* eslint-enable sort-keys-fix/sort-keys-fix */
} as Record<string, number | string | undefined>,
),
)
const dataKeys = uniq(dataAsFlatObject.map(keys).flat())
const consolidatedDataAsFlatObject = dataAsFlatObject.map(record =>
dataKeys.reduce((_record, dataKey) => {
if (_record[dataKey] === undefined) {
return {
..._record,
[dataKey]: undefined,
}
}

return _record
}, record),
)
const dataAsCsv = await generateCsvFromObject(consolidatedDataAsFlatObject)

res.setHeader('Content-Type', 'text/csv')
res.status(200).send(dataAsCsv)
} catch (err) {
handleApiEndpointError(err, ERROR_PATH, res, true)
}

return undefined

default:
handleApiEndpointError(new ApiError('Method not allowed.', 405, true), ERROR_PATH, res, true)
}
}
2 changes: 1 addition & 1 deletion pages/api/surveys/[surveyId]/entries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import cuid from 'cuid'
import type { TellMe } from '@schemas/1.0.0/TellMe'
import type { NextApiRequest, NextApiResponse } from 'next'

const ERROR_PATH = 'pages/api/surveys/[surveyId]/index.ts'
const ERROR_PATH = 'pages/api/surveys/[surveyId]/entries/index.ts'

export default async function SurveyEntryIndexEndpoint(req: NextApiRequest, res: NextApiResponse) {
switch (req.method) {
Expand Down
18 changes: 13 additions & 5 deletions pages/surveys/[id]/entries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export default function SurveyEntryListPage() {
return
}

const maybeBody = await api.get('auth/one-time-token')
const maybeBody = await api.get('one-time-tokens/new')
if (maybeBody === null || maybeBody.hasError) {
return
}
Expand All @@ -109,19 +109,19 @@ export default function SurveyEntryListPage() {
window.open(`${url}?mimeType=${mimeType}&oneTimeToken=${oneTimeToken}`, '')
}

const exportEntries = async fileExtension => {
const exportEntries = async () => {
if (isMounted()) {
setIsDownloading(true)
}

const maybeBody = await api.get(`auth/one-time-token`)
const maybeBody = await api.get(`one-time-tokens/new`)
if (maybeBody === null || maybeBody.hasError) {
return
}

const { oneTimeToken } = maybeBody.data

window.open(`/api/surveys/${surveyId}/download?fileExtension=${fileExtension}&oneTimeToken=${oneTimeToken}`)
window.open(`/api/surveys/${surveyId}/entries/download?oneTimeToken=${oneTimeToken}`)

if (isMounted()) {
setIsDownloading(false)
Expand Down Expand Up @@ -245,7 +245,15 @@ export default function SurveyEntryListPage() {
<AdminHeader>
<Title>{survey.data.title}</Title>

<Button disabled={isDownloading} onClick={() => exportEntries('csv')} size="small">
<Button
disabled={isDownloading}
onClick={exportEntries}
size="small"
// TODO Fix that in @singularity/core.
style={{
whiteSpace: 'nowrap',
}}
>
{intl.formatMessage({
defaultMessage: 'Export as CSV',
description: '[Survey Submissions List] Export answers in CSV format button label.',
Expand Down
88 changes: 6 additions & 82 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 65643e8

Please sign in to comment.