Skip to content

Commit

Permalink
Release 2024-04-15 (#98)
Browse files Browse the repository at this point in the history
  • Loading branch information
JohannaPeanut authored Apr 15, 2024
2 parents a8775e6 + 9e2ca43 commit 85446d8
Show file tree
Hide file tree
Showing 6 changed files with 320 additions and 63 deletions.
88 changes: 25 additions & 63 deletions src/pages/[projectSlug]/surveys/[surveyId]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { PageHeader } from "src/core/components/pages/PageHeader"
import { H2 } from "src/core/components/text"
import { useSlugs } from "src/core/hooks"
import { LayoutRs, MetaTags } from "src/core/layouts"
import { TQuestion, TSurvey } from "src/survey-public/components/types"
import { TSurvey } from "src/survey-public/components/types"
import {
getFeedbackDefinitionBySurveySlug,
getResponseConfigBySurveySlug,
Expand All @@ -19,6 +19,10 @@ import {
import { GroupedSurveyResponseItem } from "src/survey-responses/components/analysis/GroupedSurveyResponseItem"
import getGroupedSurveyResponses from "src/survey-responses/queries/getGroupedSurveyResponses"
import { getFormatDistanceInDays } from "src/survey-responses/utils/getFormatDistanceInDays"
import {
extractAndTransformQuestionsFromPages,
transformDeletedQuestions,
} from "src/survey-responses/utils/format-survey-questions"
import { SurveyTabs } from "src/surveys/components/SurveyTabs"
import getSurvey from "src/surveys/queries/getSurvey"

Expand All @@ -29,57 +33,6 @@ export const Survey = () => {
const [{ groupedSurveyResponsesFirstPart, surveySessions, surveyResponsesFeedbackPart }] =
usePaginatedQuery(getGroupedSurveyResponses, { projectSlug, surveyId: survey.id })

type QuestionObject = {
id: number
label: string
component: "singleResponse" | "multipleResponse" | "text"
props: { responses: { id: number; text: string }[] }
}
const transformQuestion = (question: TQuestion): QuestionObject => {
return {
id: question.id,
label: question.label.de,
component: question.component,
props: {
responses: question.props.responses.map((response) => {
return {
id: response.id,
text: response.text.de,
}
}),
},
}
}

const extractAndTransformQuestionsFromPages = (pages: TSurvey["pages"]): QuestionObject[] => {
const transformedArray: QuestionObject[] = []

pages.forEach((page) => {
if (!page.questions || page.questions.length === 0) return

const transformedQuestions = page.questions
.map((question) => {
if (!("responses" in question.props)) return
return transformQuestion(question)
})
.filter(Boolean)
transformedArray.push(...transformedQuestions)
})
return transformedArray
}

const transformDeletedQuestions = (questions: TQuestion[]): QuestionObject[] => {
const transformedArray: QuestionObject[] = []
const transformedQuestions = questions
.map((question) => {
if (!("responses" in question.props)) return
return transformQuestion(question)
})
.filter(Boolean)
transformedArray.push(...transformedQuestions)
return transformedArray
}

const feedbackDefinition = getFeedbackDefinitionBySurveySlug(survey.slug)
const surveyDefinition = getSurveyDefinitionBySurveySlug(survey.slug)
const responseConfig = getResponseConfigBySurveySlug(survey.slug)
Expand Down Expand Up @@ -132,16 +85,16 @@ export const Survey = () => {
})

// get all questions from surveyDefinition and transform them
let surveyDefinitionArray = extractAndTransformQuestionsFromPages(
const surveyDefinitionArrayWithLatestQuestions = extractAndTransformQuestionsFromPages(
surveyDefinition.pages as TSurvey["pages"],
)

// add th deleted questions to the array and transform them
if (surveyDefinition.deletedQuestions) {
surveyDefinitionArray = surveyDefinitionArray.concat(
transformDeletedQuestions(surveyDefinition.deletedQuestions),
)
}
const surveyDefinitionArray = surveyDefinition.deletedQuestions
? surveyDefinitionArrayWithLatestQuestions.concat(
transformDeletedQuestions(surveyDefinition.deletedQuestions),
)
: surveyDefinitionArrayWithLatestQuestions

const groupedSurveyResponseData = rawData.map((r) => {
const questionId = Object.keys(r)[0]
Expand All @@ -159,7 +112,7 @@ export const Survey = () => {
return { questionLabel: question.label, data }
})

const handleCopyButtonClick = async () => {
const handleCopyChartDataButtonClick = async () => {
await navigator.clipboard.writeText(JSON.stringify(groupedSurveyResponseData))
}

Expand Down Expand Up @@ -247,11 +200,20 @@ export const Survey = () => {
</div>

<SuperAdminBox>
<p>
<button onClick={handleCopyButtonClick} className={whiteButtonStyles}>
Beteiligungsergebnisse in die Zwischenablage kopieren
<div className="flex flex-col gap-4 items-start">
<button onClick={handleCopyChartDataButtonClick} className={whiteButtonStyles}>
Beteiligungsergebnisse in die Zwischenablage kopieren - formatiert für Diagramme
</button>
</p>
<Link href={`/api/survey/${survey.id}/questions`} button="white">
Fragen der Beteiligung als CSV herunterladen
</Link>
<Link href={`/api/survey/${survey.id}/responses`} button="white">
Antworten der Beteiligung als CSV herunterladen
</Link>
<Link href={`/api/survey/${survey.id}/results`} button="white">
Ergebnisse der Beteiligung als CSV herunterladen
</Link>
</div>
</SuperAdminBox>

<SuperAdminBox>
Expand Down
66 changes: 66 additions & 0 deletions src/pages/api/survey/[surveyId]/_shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { createObjectCsvStringifier } from "csv-writer"
import { NextApiRequest, NextApiResponse } from "next"
import { api } from "src/blitz-server"
import { getSession } from "@blitzjs/auth"
import { AuthorizationError } from "blitz"
import { ZodError } from "zod"
import dbGetSurvey from "src/surveys/queries/getSurvey"
import { Survey } from "db"

const DEBUG = false

export const getSurvey = async (
req: NextApiRequest,
res: NextApiResponse,
): Promise<undefined | Survey> => {
await api(() => null)

const err = (status: number, message: string) => {
res.status(status).json({ error: true, status: status, message })
res.end()
}

const session = await getSession(req, res)

let survey
try {
// @ts-ignore
survey = await dbGetSurvey({ id: Number(req.query.surveyId) }, { session })
} catch (e) {
if (e instanceof AuthorizationError) {
err(403, "Forbidden")
}
// @ts-ignore
if (e.code === "P2025" || e instanceof ZodError) {
err(404, "Not Found")
}

console.error(e)
err(500, "Internal Server Error")

return
}

return survey
}

export const sendCsv = (
res: NextApiResponse,
headers: { id: string; title: string }[],
data: Record<string, any>[],
filename: string,
) => {
const csvStringifier = createObjectCsvStringifier({
header: headers,
fieldDelimiter: ";",
alwaysQuote: true,
})
const csvString = csvStringifier.getHeaderString() + csvStringifier.stringifyRecords(data)
if (DEBUG) {
res.setHeader("Content-Type", "text/plain")
} else {
res.setHeader("Content-Type", "text/csv")
res.setHeader("Content-Disposition", `attachment; filename=${filename}`)
}
res.send(csvString)
}
43 changes: 43 additions & 0 deletions src/pages/api/survey/[surveyId]/questions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { NextApiRequest, NextApiResponse } from "next"
import { TSurvey } from "src/survey-public/components/types"
import { getSurvey, sendCsv } from "./_shared"
import { getSurveyDefinitionBySurveySlug } from "src/survey-public/utils/getConfigBySurveySlug"

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const survey = await getSurvey(req, res)
if (!survey) return

const surveyDefinition = getSurveyDefinitionBySurveySlug(survey.slug)

const headers = [
{ id: "id", title: "frage_id" },
{ id: "type", title: "typ" },
{ id: "question", title: "frage" },
]

const types = {
singleResponse: "einfach",
multipleResponse: "mehrfach",
}

type Question = { id: number | string; type: string; question: string }
let data: Question[] = []
const addQuestions = (definition: TSurvey) => {
definition.pages.forEach((page) => {
if (!page.questions) return
page.questions.forEach(({ id, component, label }) => {
// @ts-ignore
data.push({ id, type: types[component] || component, question: label.de })
})
})
}

addQuestions(surveyDefinition)

// for now we only want questions, not feedback part
// in case we want to include the feedack part we cvan uncomment that line
// @ts-ignore
// addQuestions(feedbackDefinition)

sendCsv(res, headers, data, "fragen.csv")
}
45 changes: 45 additions & 0 deletions src/pages/api/survey/[surveyId]/responses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { NextApiRequest, NextApiResponse } from "next"
import { TSurvey } from "src/survey-public/components/types"
import { getSurveyDefinitionBySurveySlug } from "src/survey-public/utils/getConfigBySurveySlug"
import { getSurvey, sendCsv } from "./_shared"

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const survey = await getSurvey(req, res)
if (!survey) return
const surveyDefinition = getSurveyDefinitionBySurveySlug(survey.slug)

const headers = [
{ id: "questionId", title: "frage_id" },
{ id: "responseId", title: "antwort_id" },
{ id: "text", title: "antwort" },
]

type Question = { questionId: number | string; responseId: number | string; text: string }
let data: Question[] = []
const addQuestions = (definition: TSurvey) => {
definition.pages.forEach((page) => {
if (!page.questions) return
page.questions.forEach((question) => {
if (!["singleResponse", "multipleResponse"].includes(question.component)) return
// @ts-ignore
question.props.responses.forEach((response) => {
data.push({
questionId: question.id,
responseId: response.id,
text: response.text.de,
})
})
})
})
}

// @ts-ignore
addQuestions(surveyDefinition)

// for now we only want questions, not feedback part
// in case we want to include the feedack part we cvan uncomment that line
// @ts-ignore
// addQuestions(feedbackDefinition)

sendCsv(res, headers, data, "antworten.csv")
}
86 changes: 86 additions & 0 deletions src/pages/api/survey/[surveyId]/results.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import db from "db"
import { NextApiRequest, NextApiResponse } from "next"
import {
getFeedbackDefinitionBySurveySlug,
getSurveyDefinitionBySurveySlug,
} from "src/survey-public/utils/getConfigBySurveySlug"
import { getSurvey, sendCsv } from "./_shared"

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const survey = await getSurvey(req, res)
if (!survey) return

const surveyDefinition = getSurveyDefinitionBySurveySlug(survey.slug)
const feedbackDefinition = getFeedbackDefinitionBySurveySlug(survey.slug)
const surveys = Object.fromEntries([surveyDefinition, feedbackDefinition].map((o) => [o.part, o]))
const questions = {}

Object.values(surveys).forEach((survey) => {
survey.pages.forEach((page) => {
if (!page.questions) return
page.questions.forEach((question) => {
// @ts-ignore
if (["singleResponse", "multipleResponse"].includes(question.component)) {
// @ts-ignore
question.responses = Object.fromEntries(question.props.responses.map((r) => [r.id, r]))
}
// @ts-ignore
questions[question.id] = question
})
})
})

const surveySessions = await db.surveySession.findMany({
where: { surveyId: survey.id },
include: { responses: { where: { surveyPart: 1 } } },
})

// for now we only want questions, not feedback part
// in case we want to include the feedack part we cvan uncomment these lines

const headers = [
{ id: "createdAt", title: "datum" },
{ id: "sessionId", title: "sitzung_id" },
{ id: "questionId", title: "frage_id" },
{ id: "responseId", title: "ergebnis_antwort_id" },
// { id: "responseText", title: "responseText" },
// { id: "responseData", title: "responseData" },
]

type Result = {
createdAt: string
sessionId: string
questionId: string
responseId?: string
// responseText?: string
// responseData?: string
}

const csvData: Result[] = []

// as we only want to include the latest questions in the export we need to check if the question is in the array of latest questions; in the frm7 project we deleted questions after the survey was live
const deletedQuestionIds = surveyDefinition.deletedQuestions?.map((q) => q.id)

surveySessions.forEach((surveySession) => {
const { id, createdAt, responses } = surveySession

responses.forEach(({ data }) => {
// @ts-expect-error
data = JSON.parse(data)
Object.entries(data).map(([questionId, responseData]) => {
if (!deletedQuestionIds || !deletedQuestionIds?.includes(Number(questionId))) {
let row: Result = {
createdAt: createdAt.toLocaleDateString("de-DE"),
sessionId: String(id),
questionId: "n/a",
}
const responseId = responseData
row = { ...row, questionId, responseId }
csvData.push(row)
}
})
})
})

sendCsv(res, headers, csvData, "ergebnisse.csv")
}
Loading

0 comments on commit 85446d8

Please sign in to comment.