mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-23 05:17:49 -05:00
fix: summary calculation in multi choice questions (#4022)
Co-authored-by: Johannes <johannes@formbricks.com>
This commit is contained in:
+10
-1
@@ -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">
|
||||
|
||||
+11
-2
@@ -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>
|
||||
|
||||
+4
-1
@@ -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>
|
||||
)}
|
||||
|
||||
+40
-18
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user