diff --git a/shared/locales/en/website-responses.json b/shared/locales/en/website-responses.json new file mode 100644 index 000000000..d8272a45f --- /dev/null +++ b/shared/locales/en/website-responses.json @@ -0,0 +1,20 @@ +{ + "select-survey": "Select survey", + "title": "Survey Responses", + "onboarding": { + "title": "Onboarding Survey", + "description": "Filled out once before recipient is joining the program" + }, + "checkin": { + "title": "Check-in Survey", + "description": "Filled out every 6 months while recipient is in the program" + }, + "offboarding": { + "title": "Offboarding Survey", + "description": "Filled out once when recipient is finishing the program" + }, + "offboarded-checkin": { + "title": "Follow-up Survey", + "description": "Filled out every 6 months after recipient left the program" + } +} diff --git a/shared/src/utils/stats/SurveyStatsCalculator.ts b/shared/src/utils/stats/SurveyStatsCalculator.ts new file mode 100644 index 000000000..70850017b --- /dev/null +++ b/shared/src/utils/stats/SurveyStatsCalculator.ts @@ -0,0 +1,73 @@ +import { FirestoreAdmin } from '../../firebase/admin/FirestoreAdmin'; +import { Recipient, RECIPIENT_FIRESTORE_PATH } from '../../types/recipient'; +import { Survey, SURVEY_FIRETORE_PATH, SurveyQuestionnaire, SurveyStatus } from '../../types/survey'; + +export interface SurveyStats { + total: number; + type: SurveyQuestionnaire; +} + +export interface SurveyAnswersByType { + answers: any[]; +} + +export class SurveyStatsCalculator { + private readonly _data: SurveyStats[]; + private readonly _aggregatedData: { [key: string]: { [key: string]: SurveyAnswersByType } }; + + private constructor( + data: SurveyStats[], + aggregatedData: { + [key: string]: { [key: string]: SurveyAnswersByType }; + }, + ) { + this._data = data; + this._aggregatedData = aggregatedData; + } + + /** + * @param firestoreAdmin + */ + static async build(firestoreAdmin: FirestoreAdmin): Promise { + const recipients = await firestoreAdmin.collection(RECIPIENT_FIRESTORE_PATH).get(); + let documents = await Promise.all( + recipients.docs + .filter((recipient) => !recipient.get('test_recipient')) + .map(async (recipient) => { + return await firestoreAdmin + .collection(`${RECIPIENT_FIRESTORE_PATH}/${recipient.id}/${SURVEY_FIRETORE_PATH}`) + .get(); + }), + ); + let ignored = ['pageNo']; + let surveysData = documents.flatMap((snapshot) => snapshot.docs).map((survey) => survey.data()); + + let aggregatedData: { [key: string]: { [key: string]: SurveyAnswersByType } } = {}; + const typeCounts: { [type: string]: number } = {}; + surveysData.forEach((item) => { + if (item.status === SurveyStatus.Completed) { + typeCounts[item.questionnaire] = (typeCounts[item.questionnaire] || 0) + 1; + for (const [question, response] of Object.entries(item.data)) { + if (!ignored.includes(question)) { + aggregatedData[item.questionnaire] = aggregatedData[item.questionnaire] || {}; + aggregatedData[item.questionnaire][question] = aggregatedData[item.questionnaire][question] || { + answers: [], + }; + aggregatedData[item.questionnaire][question].answers.push(response); + } + } + } + }); + const data = Object.entries(typeCounts).map(([type, total]) => ({ type, total }) as SurveyStats); + + return new SurveyStatsCalculator(data, aggregatedData); + } + + get data(): SurveyStats[] { + return this._data; + } + + get aggregatedData(): { [key: string]: { [key: string]: SurveyAnswersByType } } { + return this._aggregatedData; + } +} diff --git a/ui/src/components/tabs.tsx b/ui/src/components/tabs.tsx index 35f472449..1aa319d4a 100644 --- a/ui/src/components/tabs.tsx +++ b/ui/src/components/tabs.tsx @@ -2,53 +2,25 @@ import * as TabsPrimitive from '@radix-ui/react-tabs'; import * as React from 'react'; -import { cn } from '../lib/utils'; const Tabs = TabsPrimitive.Root; const TabsList = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); +>(({ className, ...props }, ref) => ); TabsList.displayName = TabsPrimitive.List.displayName; const TabsTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); +>(({ className, ...props }, ref) => ); TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; const TabsContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); +>(({ className, ...props }, ref) => ); TabsContent.displayName = TabsPrimitive.Content.displayName; export { Tabs, TabsContent, TabsList, TabsTrigger }; diff --git a/website/src/app/[lang]/[region]/(website)/survey/responses/page.tsx b/website/src/app/[lang]/[region]/(website)/survey/responses/page.tsx new file mode 100644 index 000000000..a8ca1e906 --- /dev/null +++ b/website/src/app/[lang]/[region]/(website)/survey/responses/page.tsx @@ -0,0 +1,88 @@ +import { DefaultPageProps } from '@/app/[lang]/[region]'; +import { firestoreAdmin } from '@/firebase-admin'; +import { SurveyQuestionnaire } from '@socialincome/shared/src/types/survey'; +import { Translator } from '@socialincome/shared/src/utils/i18n'; +import { SurveyStatsCalculator } from '@socialincome/shared/src/utils/stats/SurveyStatsCalculator'; +import { + Badge, + BaseContainer, + Card, + CardTitle, + Table, + TableBody, + TableCell, + TableRow, + Tabs, + TabsContent, + TabsList, + TabsTrigger, + Typography, +} from '@socialincome/ui'; + +export const revalidate = 3600; // update once an hour +export default async function Page({ params: { lang } }: DefaultPageProps) { + const surveyStatsCalculator = await SurveyStatsCalculator.build(firestoreAdmin); + const temp = surveyStatsCalculator.data; + const allSurveyData = Object.values(SurveyQuestionnaire).map((it) => temp.find((survey) => survey.type == it)); + const data = surveyStatsCalculator.aggregatedData; + const translator = await Translator.getInstance({ + language: lang, + namespaces: ['website-responses', 'website-survey'], + }); + + return ( + + + {translator.t('title')} + + + + {translator.t('select-survey')} + + + + {allSurveyData.map( + (surveyData) => + surveyData && ( + + + {translator.t(surveyData.type + '.title')} + {translator.t(surveyData.type + '.description')} + {surveyData.total} data points + + + ), + )} + + + {Object.values(SurveyQuestionnaire).map((selectedSurvey) => ( + + + + {Object.keys(data[selectedSurvey]).map((key) => ( + + +
+ + {translator.t('survey.questions.' + key.replace('V1', 'TitleV1'))} + + + {data[selectedSurvey][key].answers.length} answers + +
+
+ + + {JSON.stringify(data[selectedSurvey][key].answers)} + + +
+ ))} +
+
+
+ ))} +
+
+ ); +}