Skip to content

Commit

Permalink
Various small cleanups
Browse files Browse the repository at this point in the history
  • Loading branch information
mkue committed Dec 28, 2024
1 parent bf8d50c commit 6abd527
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 108 deletions.
7 changes: 6 additions & 1 deletion shared/src/utils/stats/SurveyStatsCalculator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,12 @@ test('handle edge cases with empty data', async () => {
await testEnv.firestore.clearFirestoreData({ projectId });
const emptyCalculator = await SurveyStatsCalculator.build(firestoreAdmin);
expect(emptyCalculator.data).toEqual([]);
expect(emptyCalculator.aggregatedData).toEqual({});
expect(emptyCalculator.aggregatedData).toEqual({
[SurveyQuestionnaire.Checkin]: {},
[SurveyQuestionnaire.Onboarding]: {},
[SurveyQuestionnaire.OffboardedCheckin]: {},
[SurveyQuestionnaire.Offboarding]: {},
});
});

const surveyRecords = [
Expand Down
52 changes: 27 additions & 25 deletions shared/src/utils/stats/SurveyStatsCalculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,33 @@ export interface SurveyAnswersByType {
question: Question;
}

type AggregatedSurveyData = { [key in SurveyQuestionnaire]: { [questionKey: string]: SurveyAnswersByType } };

const SUPPORTED_SURVEY_QUESTION_TYPES = [QuestionInputType.RADIO_GROUP, QuestionInputType.CHECKBOX];

export class SurveyStatsCalculator {
private readonly _data: SurveyStats[];
private readonly _aggregatedData: { [key: string]: { [key: string]: SurveyAnswersByType } };
private readonly _aggregatedData: AggregatedSurveyData;
private readonly _oldestDate: Date;

private constructor(
data: SurveyStats[],
aggregatedData: { [key: string]: { [key: string]: SurveyAnswersByType } },
oldestDate: Date,
) {
private constructor(data: SurveyStats[], aggregatedData: AggregatedSurveyData, oldestDate: Date) {
this._data = data;
this._aggregatedData = aggregatedData;
this._oldestDate = oldestDate;
}

get oldestDate(): Date {
return this._oldestDate;
}

get data(): SurveyStats[] {
return this._data;
}

get aggregatedData(): AggregatedSurveyData {
return this._aggregatedData;
}

/**
* Builds a new instance of SurveyStatsCalculator
* @param firestoreAdmin Firestore admin instance for accessing the database
Expand Down Expand Up @@ -61,10 +71,15 @@ export class SurveyStatsCalculator {

private static aggregateSurveyData(surveysData: Survey[]): {
typeCounts: { [type: string]: number };
aggregatedData: { [key: string]: { [key: string]: SurveyAnswersByType } };
aggregatedData: AggregatedSurveyData;
oldestDate: Date;
} {
const aggregatedData: { [key: string]: { [key: string]: SurveyAnswersByType } } = {};
const aggregatedSurveyData: AggregatedSurveyData = {
[SurveyQuestionnaire.Checkin]: {},
[SurveyQuestionnaire.Onboarding]: {},
[SurveyQuestionnaire.OffboardedCheckin]: {},
[SurveyQuestionnaire.Offboarding]: {},
};
const typeCounts: { [type: string]: number } = {};
let oldestDate = new Date();

Expand All @@ -77,28 +92,27 @@ export class SurveyStatsCalculator {
const questionnaire = survey.questionnaire!;
typeCounts[questionnaire] = (typeCounts[questionnaire] || 0) + 1;
Object.entries(survey.data!).forEach(([questionKey, response]) => {
this.processSurveyResponse(aggregatedData, questionnaire, questionKey, response);
this.processSurveyResponse(aggregatedSurveyData, questionnaire, questionKey, response);
});
}
});

return { typeCounts, aggregatedData, oldestDate };
return { typeCounts, aggregatedData: aggregatedSurveyData, oldestDate };
}

private static isCompletedSurvey(survey: Survey): boolean {
return !!(survey.data && survey.questionnaire && survey.status === SurveyStatus.Completed);
}

private static processSurveyResponse(
aggregatedData: { [key: string]: { [key: string]: SurveyAnswersByType } },
questionnaire: string,
aggregatedData: { [key in SurveyQuestionnaire]: { [key: string]: SurveyAnswersByType } },
questionnaire: SurveyQuestionnaire,
questionKey: string,
response: any,
): void {
const question = QUESTIONS_DICTIONARY.get(questionKey);
if (!question || !SUPPORTED_SURVEY_QUESTION_TYPES.includes(question.type)) return;

aggregatedData[questionnaire] = aggregatedData[questionnaire] || {};
aggregatedData[questionnaire][questionKey] = aggregatedData[questionnaire][questionKey] || {
answers: {},
total: 0,
Expand All @@ -116,16 +130,4 @@ export class SurveyStatsCalculator {
}
questionData.total++;
}

get oldestDate(): Date {
return this._oldestDate;
}

get data(): SurveyStats[] {
return this._data;
}

get aggregatedData(): { [key: string]: { [key: string]: SurveyAnswersByType } } {
return this._aggregatedData;
}
}
Original file line number Diff line number Diff line change
@@ -1,62 +1,61 @@
'use client';
import { Component } from 'react';
import { Bar, BarChart, LabelList, ResponsiveContainer, XAxis, YAxis } from 'recharts';
import './opacity.css';

export interface ChartData {
name: string;
value: number;
}

export default class BarchartSurveyResponseComponent extends Component<{ data: ChartData[] }> {
render() {
const { data } = this.props;
const barHeight = 30;
const chartHeight = data.length * barHeight + 40;

let fillColor = 'hsl(var(--foreground))';
function BarchartSurveyResponseComponent({ data }: { data: ChartData[] }) {
const barHeight = 30;
const chartHeight = data.length * barHeight + 40;

const customLabel = (props: any) => {
const { x, y, width, value } = props;
return (
<text
aria-label={value}
x={x + width + 10}
y={y + barHeight / 2 + 3}
fontSize={15}
fill={fillColor}
textAnchor="start"
>
<title>{value}</title>
{value}
</text>
);
};
let fillColor = 'hsl(var(--foreground))';

const customLabel = (props: any) => {
const { x, y, width, value } = props;
return (
<ResponsiveContainer className={'survey-chart'} height={chartHeight}>
<BarChart
data={data}
layout="vertical"
margin={{ top: 20, right: 100, bottom: 20, left: 20 }}
barGap={0}
barCategoryGap="0%"
>
<Bar barSize={barHeight} dataKey="value" fill={fillColor}>
<LabelList dataKey="name" position="right" content={customLabel} />
</Bar>
<XAxis type="number" hide />
<YAxis
type="category"
dataKey="value"
axisLine={false}
tickLine={false}
className={'font-bold'}
tick={{ fill: fillColor }}
tickFormatter={(value) => `${value}%`}
minTickGap={1}
/>
</BarChart>
</ResponsiveContainer>
<text
aria-label={value}
x={x + width + 10}
y={y + barHeight / 2 + 3}
fontSize={15}
fill={fillColor}
textAnchor="start"
>
<title>{value}</title>
{value}
</text>
);
}
};

return (
<ResponsiveContainer height={chartHeight}>
<BarChart
data={data}
className="before:to-background before:absolute before:inset-y-0 before:right-0 before:w-10 before:bg-gradient-to-r before:from-transparent before:content-['']"
layout="vertical"
margin={{ top: 20, right: 100, bottom: 20, left: 20 }}
barGap={0}
barCategoryGap="0%"
>
<Bar barSize={barHeight} dataKey="value" fill={fillColor}>
<LabelList dataKey="name" position="right" content={customLabel} />
</Bar>
<XAxis type="number" hide />
<YAxis
type="category"
dataKey="value"
axisLine={false}
tickLine={false}
className={'font-bold'}
tick={{ fill: fillColor }}
tickFormatter={(value) => `${value}%`}
minTickGap={1}
/>
</BarChart>
</ResponsiveContainer>
);
}

export default BarchartSurveyResponseComponent;

This file was deleted.

This file was deleted.

52 changes: 26 additions & 26 deletions website/src/app/[lang]/[region]/(website)/survey/responses/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@ import {
Typography,
} from '@socialincome/ui';
import { Fragment } from 'react';
import './card-tab.css';

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 { aggregatedData: data, data: temp, oldestDate } = await SurveyStatsCalculator.build(firestoreAdmin);

const allSurveyData = Object.values(SurveyQuestionnaire)
.map((it) => temp.find((survey) => survey.type == it))
.filter((it) => !!it);
const data = surveyStatsCalculator.aggregatedData;

const translator = await Translator.getInstance({
language: lang,
namespaces: ['website-responses', 'website-survey'],
Expand Down Expand Up @@ -57,13 +57,13 @@ export default async function Page({ params: { lang } }: DefaultPageProps) {
{allSurveyData.map(
(surveyData) =>
surveyData && (
<TabsTrigger key={surveyData.type} value={surveyData.type} className="tabs-trigger" asChild={true}>
<Card className="card-tab data-[state=active]:bg-primary bg-card-muted data-[state=active]:cursor-default data-[state=inactive]:cursor-pointer data-[state=active]:text-white">
<CardHeader className={'pb-2 pl-4 pt-2'}>
<CardTitle className="card-tab-title py-2">{translator.t(surveyData.type + '.title')}</CardTitle>
<TabsTrigger key={surveyData.type} value={surveyData.type} asChild={true}>
<Card className="card-tab data-[state=active]:bg-primary bg-card-muted [&_h3]:data-[state=inactive]:text-accent-foreground data-[state=active]:cursor-default data-[state=inactive]:cursor-pointer data-[state=active]:text-white">
<CardHeader className="pb-2 pl-4 pt-2">
<CardTitle className="py-2">{translator.t(`${surveyData.type}.title`)}</CardTitle>
</CardHeader>
<CardContent className={'p-2 pl-4'}>
<Typography>{translator.t(surveyData.type + '.description')}</Typography>
<CardContent className="p-2 pl-4">
<Typography>{translator.t(`${surveyData.type}.description`)}</Typography>
<Typography className="mt-3">
{surveyData.total} {translator.t('data-points')}
</Typography>
Expand All @@ -75,34 +75,34 @@ export default async function Page({ params: { lang } }: DefaultPageProps) {
</TabsList>
<Typography className="mt-10 font-bold">
{translator.t('responses-since', {
context: { sinceDate: surveyStatsCalculator.oldestDate.toLocaleDateString() },
context: { sinceDate: oldestDate.toLocaleDateString() },
})}
</Typography>

{Object.values(SurveyQuestionnaire).map((selectedSurvey) => (
<TabsContent value={selectedSurvey} key={selectedSurvey}>
<div className="mx-auto grid grid-cols-1 gap-2 lg:grid-cols-2">
{Object.keys(data[selectedSurvey] || []).map((key) => (
<Fragment key={selectedSurvey + key + 'statistics'}>
<Separator className="col-span-1 mt-2 lg:col-span-2"></Separator>
<div key={selectedSurvey + key + 'question'} className="columns-1 bg-transparent p-2">
<Typography size="2xl" className="text py-2" weight="bold">
{translator.t(data[selectedSurvey][key].question.translationKey)}
{data[selectedSurvey][key].question.type == QuestionInputType.CHECKBOX && (
<Typography className="text-sm">({translator.t('multiple-answers')})</Typography>
{Object.keys(data[selectedSurvey] || []).map((questionKey) => (
<Fragment key={`${selectedSurvey}-${questionKey}statistics`}>
<Separator className="col-span-1 mt-2 lg:col-span-2" />
<div key={`${selectedSurvey}-${questionKey}-question`} className="p-2">
<Typography size="2xl" weight="bold" className="my-2">
{translator.t(data[selectedSurvey][questionKey].question.translationKey)}
{data[selectedSurvey][questionKey].question.type == QuestionInputType.CHECKBOX && (
<Typography size="sm">({translator.t('multiple-answers')})</Typography>
)}
</Typography>

<Badge variant="accent">
<Typography>
{data[selectedSurvey][key].total} {translator.t('answers')}
{data[selectedSurvey][questionKey].total} {translator.t('answers')}
</Typography>
</Badge>
</div>
<div key={selectedSurvey + key + 'answers'} className="w-full p-2">
{data[selectedSurvey][key].answers && (
<div key={`${selectedSurvey}-${questionKey}-answers`} className="p-2">
{data[selectedSurvey][questionKey].answers && (
<BarchartSurveyResponseComponent
data={convertToBarchartData(data[selectedSurvey][key])}
></BarchartSurveyResponseComponent>
data={convertToBarchartData(data[selectedSurvey][questionKey])}
/>
)}
</div>
</Fragment>
Expand All @@ -111,7 +111,7 @@ export default async function Page({ params: { lang } }: DefaultPageProps) {
</TabsContent>
))}
</Tabs>
<Separator></Separator>
<Separator />
</BaseContainer>
);
}

0 comments on commit 6abd527

Please sign in to comment.