-
-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
671a26e
commit 7e5dbb5
Showing
4 changed files
with
184 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<SurveyStatsCalculator> { | ||
const recipients = await firestoreAdmin.collection<Recipient>(RECIPIENT_FIRESTORE_PATH).get(); | ||
let documents = await Promise.all( | ||
recipients.docs | ||
.filter((recipient) => !recipient.get('test_recipient')) | ||
.map(async (recipient) => { | ||
return await firestoreAdmin | ||
.collection<Survey>(`${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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
88 changes: 88 additions & 0 deletions
88
website/src/app/[lang]/[region]/(website)/survey/responses/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<BaseContainer className="mx-auto flex flex-col"> | ||
<Typography size="4xl" className="mt-4" weight="bold"> | ||
{translator.t('title')} | ||
</Typography> | ||
|
||
<Typography size="xl" className="mx-2 mt-8" weight="bold"> | ||
{translator.t('select-survey')} | ||
</Typography> | ||
<Tabs defaultValue={SurveyQuestionnaire.Onboarding}> | ||
<TabsList className="mx-auto mt-2 grid grid-cols-4"> | ||
{allSurveyData.map( | ||
(surveyData) => | ||
surveyData && ( | ||
<TabsTrigger key={surveyData.type} value={surveyData.type} className="tabs-trigger" asChild={true}> | ||
<Card className="data-[state=active]:bg-primary ml-1 bg-neutral-50 p-2 data-[state=inactive]:cursor-pointer data-[state=active]:text-white"> | ||
<CardTitle className="text py-2">{translator.t(surveyData.type + '.title')}</CardTitle> | ||
<Typography className="mt-2">{translator.t(surveyData.type + '.description')}</Typography> | ||
<Typography className="mt-3">{surveyData.total} data points</Typography> | ||
</Card> | ||
</TabsTrigger> | ||
), | ||
)} | ||
</TabsList> | ||
|
||
{Object.values(SurveyQuestionnaire).map((selectedSurvey) => ( | ||
<TabsContent value={selectedSurvey} key={selectedSurvey} className={'mt-3.5'}> | ||
<Table key={selectedSurvey}> | ||
<TableBody> | ||
{Object.keys(data[selectedSurvey]).map((key) => ( | ||
<TableRow key={selectedSurvey + key + 'statistics'}> | ||
<TableCell key={selectedSurvey + key + 'question'} className="p-1.5 font-medium"> | ||
<div className="border-hidden bg-transparent p-2"> | ||
<Typography size="2xl" className="text py-2"> | ||
{translator.t('survey.questions.' + key.replace('V1', 'TitleV1'))} | ||
</Typography> | ||
<Badge className="bg-amber-400 text-white"> | ||
<Typography className="mt-1">{data[selectedSurvey][key].answers.length} answers</Typography> | ||
</Badge> | ||
</div> | ||
</TableCell> | ||
<TableCell key={selectedSurvey + key + 'answers'}> | ||
<Typography className="border-hidden bg-transparent p-2"> | ||
{JSON.stringify(data[selectedSurvey][key].answers)} | ||
</Typography> | ||
</TableCell> | ||
</TableRow> | ||
))} | ||
</TableBody> | ||
</Table> | ||
</TabsContent> | ||
))} | ||
</Tabs> | ||
</BaseContainer> | ||
); | ||
} |