fix: summary calculation in multi choice questions (#4022)

Co-authored-by: Johannes <johannes@formbricks.com>
This commit is contained in:
Piyush Gupta
2024-10-25 14:47:06 +05:30
committed by GitHub
parent 8e16d8daf6
commit 5c0b29eed4
13 changed files with 84 additions and 27 deletions
@@ -1,3 +1,4 @@
import { InboxIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { getPersonIdentifier } from "@formbricks/lib/person/utils";
@@ -68,6 +69,14 @@ export const MultipleChoiceSummary = ({
questionSummary={questionSummary}
survey={survey}
attributeClasses={attributeClasses}
additionalInfo={
questionSummary.type === "multipleChoiceMulti" ? (
<div className="flex items-center rounded-lg bg-slate-100 p-2">
<InboxIcon className="mr-2 h-4 w-4" />
{`${questionSummary.selectionCount} selections`}
</div>
) : undefined
}
/>
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
{results.map((result, resultsIdx) => (
@@ -97,7 +106,7 @@ export const MultipleChoiceSummary = ({
</div>
</div>
<p className="flex w-full pt-1 text-slate-600 sm:items-end sm:justify-end sm:pt-0">
{result.count} {result.count === 1 ? "response" : "responses"}
{result.count} {result.count === 1 ? "selection" : "selections"}
</p>
</div>
<div className="group-hover:opacity-80">
@@ -1,5 +1,6 @@
import { TimerIcon } from "lucide-react";
import { TSurveySummary } from "@formbricks/types/surveys/types";
import { getQuestionIcon } from "@formbricks/lib/utils/questions";
import { TSurveyQuestionType, TSurveySummary } from "@formbricks/types/surveys/types";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/components/Tooltip";
interface SummaryDropOffsProps {
@@ -7,6 +8,11 @@ interface SummaryDropOffsProps {
}
export const SummaryDropOffs = ({ dropOff }: SummaryDropOffsProps) => {
const getIcon = (questionType: TSurveyQuestionType) => {
const Icon = getQuestionIcon(questionType);
return <Icon className="mt-[3px] h-5 w-5 shrink-0 text-slate-600" />;
};
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="">
@@ -31,7 +37,10 @@ export const SummaryDropOffs = ({ dropOff }: SummaryDropOffsProps) => {
<div
key={quesDropOff.questionId}
className="grid grid-cols-6 items-center border-b border-slate-100 py-2 text-sm text-slate-800 md:text-base">
<div className="col-span-3 pl-4 md:pl-6">{quesDropOff.headline}</div>
<div className="col-span-3 flex gap-3 pl-4 md:pl-6">
{getIcon(quesDropOff.questionType)}
<p>{quesDropOff.headline}</p>
</div>
<div className="whitespace-pre-wrap text-center font-semibold">
{quesDropOff.ttc > 0 ? (quesDropOff.ttc / 1000).toFixed(2) + "s" : "N/A"}
</div>
@@ -138,7 +138,10 @@ export const SurveyAnalysisCTA = ({
)}
{!isViewer && (
<Button href={`/environments/${environment.id}/surveys/${survey.id}/edit`} EndIcon={SquarePenIcon}>
<Button
href={`/environments/${environment.id}/surveys/${survey.id}/edit`}
EndIcon={SquarePenIcon}
size="base">
Edit
</Button>
)}
@@ -235,6 +235,7 @@ export const getSurveySummaryDropOff = (
const dropOff = survey.questions.map((question, index) => {
return {
questionId: question.id,
questionType: question.type,
headline: getLocalizedValue(question.headline, "default"),
ttc: convertFloatTo2Decimal(totalTtc[question.id]) || 0,
impressions: impressionsArr[index] || 0,
@@ -318,7 +319,7 @@ export const getQuestionSummary = async (
insightsEnabled: question.insightsEnabled,
});
values;
values = [];
break;
}
case TSurveyQuestionTypeEnum.MultipleChoiceSingle:
@@ -339,6 +340,8 @@ export const getQuestionSummary = async (
}, {});
const otherValues: TSurveyQuestionSummaryMultipleChoice["choices"][number]["others"] = [];
let totalSelectionCount = 0;
let totalResponseCount = 0;
responses.forEach((response) => {
const responseLanguageCode = getLanguageCode(survey.languages, response.language);
@@ -347,36 +350,51 @@ export const getQuestionSummary = async (
? response.data[question.id]
: checkForI18n(response, question.id, survey, responseLanguageCode);
let hasValidAnswer = false;
if (Array.isArray(answer)) {
answer.forEach((value) => {
if (questionChoices.includes(value)) {
choiceCountMap[value]++;
} else {
if (value) {
totalSelectionCount++;
if (questionChoices.includes(value)) {
choiceCountMap[value]++;
} else if (isOthersEnabled) {
otherValues.push({
value,
person: response.person,
personAttributes: response.personAttributes,
});
}
hasValidAnswer = true;
}
});
} else if (typeof answer === "string") {
if (answer) {
totalSelectionCount++;
if (questionChoices.includes(answer)) {
choiceCountMap[answer]++;
} else if (isOthersEnabled) {
otherValues.push({
value,
value: answer,
person: response.person,
personAttributes: response.personAttributes,
});
}
});
} else if (typeof answer === "string") {
if (questionChoices.includes(answer)) {
choiceCountMap[answer]++;
} else {
otherValues.push({
value: answer,
person: response.person,
personAttributes: response.personAttributes,
});
hasValidAnswer = true;
}
}
if (hasValidAnswer) {
totalResponseCount++;
}
});
Object.entries(choiceCountMap).map(([label, count]) => {
values.push({
value: label,
count,
percentage: responses.length > 0 ? convertFloatTo2Decimal((count / responses.length) * 100) : 0,
percentage:
totalSelectionCount > 0 ? convertFloatTo2Decimal((count / totalSelectionCount) * 100) : 0,
});
});
@@ -384,14 +402,18 @@ export const getQuestionSummary = async (
values.push({
value: getLocalizedValue(lastChoice.label, "default") || "Other",
count: otherValues.length,
percentage: convertFloatTo2Decimal((otherValues.length / responses.length) * 100),
percentage:
totalSelectionCount > 0
? convertFloatTo2Decimal((otherValues.length / totalSelectionCount) * 100)
: 0,
others: otherValues.slice(0, VALUES_LIMIT),
});
}
summary.push({
type: question.type,
question,
responseCount: responses.length,
responseCount: totalResponseCount,
selectionCount: totalSelectionCount,
choices: values,
});
@@ -211,6 +211,7 @@ export const SigninForm = ({
)}
{emailAuthEnabled && (
<Button
size="base"
onClick={() => {
if (!showLogin) {
setShowLogin(true);
@@ -224,7 +225,7 @@ export const SigninForm = ({
loading={loggingIn}>
{totpLogin ? "Submit" : "Login with Email"}
{lastLoggedInWith && lastLoggedInWith === "Email" ? (
<span className="absolute right-3 text-xs">Last Used</span>
<span className="absolute right-3 text-xs opacity-50">Last Used</span>
) : null}
</Button>
)}
@@ -367,6 +367,7 @@ export const mockSurveySummaryOutput = {
dropOffCount: 0,
dropOffPercentage: 0,
headline: "Question Text",
questionType: "openText",
questionId: "ars2tjk8hsi8oqk1uac00mo8",
ttc: 0,
impressions: 0,
+4
View File
@@ -267,6 +267,10 @@ export const QUESTIONS_ICON_MAP: Record<TSurveyQuestionTypeEnum, JSX.Element> =
{} as Record<TSurveyQuestionTypeEnum, JSX.Element>
);
export const getQuestionIcon = (type: TSurveyQuestionTypeEnum) => {
return questionTypes.find((questionType) => questionType.id === type)?.icon;
};
export const QUESTIONS_NAME_MAP = questionTypes.reduce(
(prev, curr) => ({
...prev,
+2
View File
@@ -2106,6 +2106,7 @@ export const ZSurveyQuestionSummaryMultipleChoice = z.object({
type: z.union([z.literal("multipleChoiceMulti"), z.literal("multipleChoiceSingle")]),
question: ZSurveyMultipleChoiceQuestion,
responseCount: z.number(),
selectionCount: z.number(),
choices: z.array(
z.object({
value: z.string(),
@@ -2424,6 +2425,7 @@ export const ZSurveySummary = z.object({
dropOff: z.array(
z.object({
questionId: z.string().cuid2(),
questionType: ZSurveyQuestionType,
headline: z.string(),
ttc: z.number(),
impressions: z.number(),
@@ -34,6 +34,7 @@ export const AzureButton = ({
return (
<Button
size="base"
type="button"
EndIcon={MicrosoftIcon}
startIconClassName="ml-2"
@@ -41,7 +42,7 @@ export const AzureButton = ({
variant="secondary"
className="relative w-full justify-center">
{text}
{lastUsed && <span className="absolute right-3 text-xs">Last Used</span>}
{lastUsed && <span className="absolute right-3 text-xs opacity-50">Last Used</span>}
</Button>
);
};
@@ -26,6 +26,7 @@ export const GithubButton = ({
return (
<Button
size="base"
type="button"
EndIcon={GithubIcon}
startIconClassName="ml-2"
@@ -33,7 +34,7 @@ export const GithubButton = ({
variant="secondary"
className="relative w-full justify-center">
{text}
{lastUsed && <span className="absolute right-3 text-xs">Last Used</span>}
{lastUsed && <span className="absolute right-3 text-xs opacity-50">Last Used</span>}
</Button>
);
};
@@ -26,6 +26,7 @@ export const GoogleButton = ({
return (
<Button
size="base"
type="button"
EndIcon={GoogleIcon}
startIconClassName="ml-3"
@@ -33,7 +34,7 @@ export const GoogleButton = ({
variant="secondary"
className="relative w-full justify-center">
{text}
{lastUsed && <span className="absolute right-3 text-xs">Last Used</span>}
{lastUsed && <span className="absolute right-3 text-xs opacity-50">Last Used</span>}
</Button>
);
};
@@ -32,13 +32,14 @@ export const OpenIdButton = ({
return (
<Button
size="base"
type="button"
startIconClassName="ml-2"
onClick={handleLogin}
variant="secondary"
className="relative w-full justify-center">
{text}
{lastUsed && <span className="absolute right-3 text-xs">Last Used</span>}
{lastUsed && <span className="absolute right-3 text-xs opacity-50">Last Used</span>}
</Button>
);
};
@@ -157,6 +157,7 @@ export const SignupOptions = ({
)}
{showLogin && (
<Button
size="base"
type="submit"
className="w-full justify-center"
loading={signingUp}
@@ -167,6 +168,7 @@ export const SignupOptions = ({
{!showLogin && (
<Button
size="base"
type="button"
onClick={() => {
setShowLogin(true);