From d01b293a27015f14ebedd0b9e521dcea2c13e31d Mon Sep 17 00:00:00 2001
From: Piyush Gupta <56182734+gupta-piyush19@users.noreply.github.com>
Date: Wed, 6 Mar 2024 17:23:21 +0530
Subject: [PATCH] feat: Move Response Summary Server-side (#2160)
Co-authored-by: Matthias Nannt
---
.../[personId]/components/ResponsesFeed.tsx | 78 ++-
.../surveys/[surveyId]/(analysis)/actions.ts | 17 +-
.../surveys/[surveyId]/(analysis)/data.ts | 14 +-
.../responses/components/ResponseTimeline.tsx | 6 +
.../[surveyId]/(analysis)/summary/actions.ts | 5 +-
.../summary/components/CTASummary.tsx | 29 +-
.../summary/components/CalSummary.tsx | 85 ++-
.../summary/components/ConsentSummary.tsx | 46 +-
.../components/DateQuestionSummary.tsx | 87 +--
.../summary/components/FileUploadSummary.tsx | 135 ++--
.../components/HiddenFieldsSummary.tsx | 103 ++-
.../components/MultipleChoiceSummary.tsx | 146 +----
.../summary/components/NPSSummary.tsx | 92 +--
.../summary/components/OpenTextSummary.tsx | 88 +--
.../components/PictureChoiceSummary.tsx | 67 +-
.../summary/components/RatingSummary.tsx | 111 +---
.../summary/components/SummaryDropOffs.tsx | 173 +----
.../summary/components/SummaryList.tsx | 241 +++----
.../summary/components/SummaryMetadata.tsx | 58 +-
.../summary/components/SummaryPage.tsx | 56 +-
.../[surveyId]/(analysis)/summary/page.tsx | 8 +-
.../[surveyId]/components/CustomFilter.tsx | 17 +-
apps/web/app/lib/surveys/surveys.ts | 280 +-------
.../responses/components/ResponsePage.tsx | 58 +-
.../responses/components/ResponseTimeline.tsx | 37 +-
.../(analysis)/responses/page.tsx | 9 +-
.../summary/components/SummaryPage.tsx | 68 +-
.../[sharingKey]/(analysis)/summary/page.tsx | 11 +-
apps/web/app/share/[sharingKey]/action.ts | 20 +
.../[sharingKey]/components/CustomFilter.tsx | 261 +-------
packages/lib/response/service.ts | 59 +-
.../lib/response/tests/__mocks__/data.mock.ts | 39 ++
packages/lib/response/tests/response.test.ts | 40 ++
packages/lib/response/util.ts | 618 +++++++++++++++++-
.../lib/utils}/evaluateLogic.ts | 0
packages/types/responses.ts | 248 ++++++-
.../ui/PictureSelectionResponse/index.tsx | 2 +-
packages/ui/SingleResponseCard/index.tsx | 28 +-
38 files changed, 1691 insertions(+), 1749 deletions(-)
rename {apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components => packages/lib/utils}/evaluateLogic.ts (100%)
diff --git a/apps/web/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/ResponsesFeed.tsx b/apps/web/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/ResponsesFeed.tsx
index 0592edb5b3..1755adeaba 100644
--- a/apps/web/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/ResponsesFeed.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/ResponsesFeed.tsx
@@ -2,6 +2,8 @@
import { useEffect, useState } from "react";
+import { useMembershipRole } from "@formbricks/lib/membership/hooks/useMembershipRole";
+import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { TEnvironment } from "@formbricks/types/environment";
import { TResponse } from "@formbricks/types/responses";
import { TSurvey } from "@formbricks/types/surveys";
@@ -44,28 +46,62 @@ export default function ResponseFeed({
{fetchedResponses.length === 0 ? (
) : (
- fetchedResponses.map((response) => {
- const survey = surveys.find((survey) => {
- return survey.id === response.surveyId;
- });
- return (
-
- {survey && (
-
- )}
-
- );
- })
+ fetchedResponses.map((response) => (
+
+ ))
)}
>
);
}
+
+const ResponseSurveyCard = ({
+ response,
+ surveys,
+ user,
+ environmentTags,
+ environment,
+ deleteResponse,
+ updateResponse,
+}: {
+ response: TResponse;
+ surveys: TSurvey[];
+ user: TUser;
+ environmentTags: TTag[];
+ environment: TEnvironment;
+ deleteResponse: (responseId: string) => void;
+ updateResponse: (responseId: string, response: TResponse) => void;
+}) => {
+ const survey = surveys.find((survey) => {
+ return survey.id === response.surveyId;
+ });
+
+ const { membershipRole } = useMembershipRole(survey?.environmentId || "");
+ const { isViewer } = getAccessFlags(membershipRole);
+
+ return (
+
+ {survey && (
+
+ )}
+
+ );
+};
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions.ts
index bd18b909b4..0756e6e221 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions.ts
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions.ts
@@ -4,10 +4,10 @@ import { getServerSession } from "next-auth";
import { revalidatePath } from "next/cache";
import { authOptions } from "@formbricks/lib/authOptions";
-import { getResponses } from "@formbricks/lib/response/service";
+import { getResponses, getSurveySummary } from "@formbricks/lib/response/service";
import { canUserAccessSurvey } from "@formbricks/lib/survey/auth";
import { AuthorizationError } from "@formbricks/types/errors";
-import { TResponse, TResponseFilterCriteria } from "@formbricks/types/responses";
+import { TResponse, TResponseFilterCriteria, TSurveySummary } from "@formbricks/types/responses";
export default async function revalidateSurveyIdPath(environmentId: string, surveyId: string) {
revalidatePath(`/environments/${environmentId}/surveys/${surveyId}`);
@@ -45,3 +45,16 @@ export async function getResponsesAction(
const responses = await getResponses(surveyId, page, batchSize, filterCriteria);
return responses;
}
+
+export const getSurveySummaryAction = async (
+ surveyId: string,
+ filterCriteria?: TResponseFilterCriteria
+): Promise => {
+ const session = await getServerSession(authOptions);
+ if (!session) throw new AuthorizationError("Not authorized");
+
+ const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId);
+ if (!isAuthorized) throw new AuthorizationError("Not authorized");
+
+ return await getSurveySummary(surveyId, filterCriteria);
+};
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/data.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/data.ts
index 236192be1d..0af1d05686 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/data.ts
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/data.ts
@@ -1,19 +1,13 @@
-import { getDisplayCountBySurveyId } from "@formbricks/lib/display/service";
-import { getResponses } from "@formbricks/lib/response/service";
+import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
import { getSurvey } from "@formbricks/lib/survey/service";
-import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
export const getAnalysisData = async (surveyId: string, environmentId: string) => {
- const [survey, team, responses, displayCount] = await Promise.all([
+ const [survey, responseCount] = await Promise.all([
getSurvey(surveyId),
- getTeamByEnvironmentId(environmentId),
- getResponses(surveyId),
- getDisplayCountBySurveyId(surveyId),
+ getResponseCountBySurveyId(surveyId),
]);
if (!survey) throw new Error(`Survey not found: ${surveyId}`);
- if (!team) throw new Error(`Team not found for environment: ${environmentId}`);
if (survey.environmentId !== environmentId) throw new Error(`Survey not found: ${surveyId}`);
- const responseCount = responses.length;
- return { responses, responseCount, survey, displayCount };
+ return { responseCount, survey };
};
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTimeline.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTimeline.tsx
index c823a26bbc..1840c3cf0b 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTimeline.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTimeline.tsx
@@ -3,6 +3,8 @@
import EmptyInAppSurveys from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys";
import React, { useEffect, useRef } from "react";
+import { useMembershipRole } from "@formbricks/lib/membership/hooks/useMembershipRole";
+import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { TEnvironment } from "@formbricks/types/environment";
import { TResponse } from "@formbricks/types/responses";
import { TSurvey } from "@formbricks/types/surveys";
@@ -60,6 +62,9 @@ export default function ResponseTimeline({
};
}, [fetchNextPage, hasMore]);
+ const { membershipRole } = useMembershipRole(survey.environmentId);
+ const { isViewer } = getAccessFlags(membershipRole);
+
return (
{survey.type === "web" && responses.length === 0 && !environment.widgetSetupCompleted ? (
@@ -84,6 +89,7 @@ export default function ResponseTimeline({
environment={environment}
updateResponse={updateResponse}
deleteResponse={deleteResponse}
+ isViewer={isViewer}
/>
);
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions.ts
index 776e8376d0..923dd8b172 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions.ts
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions.ts
@@ -9,6 +9,7 @@ import { authOptions } from "@formbricks/lib/authOptions";
import { sendEmbedSurveyPreviewEmail } from "@formbricks/lib/emails/emails";
import { canUserAccessSurvey } from "@formbricks/lib/survey/auth";
import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service";
+import { formatSurveyDateFields } from "@formbricks/lib/survey/util";
import { AuthenticationError, AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
type TSendEmailActionArgs = {
@@ -54,7 +55,7 @@ export async function generateResultShareUrlAction(surveyId: string): Promise {
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CTASummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CTASummary.tsx
index b35d1d3326..482a79af73 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CTASummary.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CTASummary.tsx
@@ -1,34 +1,17 @@
import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/Headline";
import { questionTypes } from "@/app/lib/questions";
import { InboxStackIcon } from "@heroicons/react/24/solid";
-import { useMemo } from "react";
-import type { TSurveyQuestionSummary } from "@formbricks/types/surveys";
-import { TSurveyCTAQuestion } from "@formbricks/types/surveys";
+import { TSurveySummaryCta } from "@formbricks/types/responses";
import { ProgressBar } from "@formbricks/ui/ProgressBar";
interface CTASummaryProps {
- questionSummary: TSurveyQuestionSummary;
-}
-
-interface ChoiceResult {
- count: number;
- percentage: number;
+ questionSummary: TSurveySummaryCta;
}
export default function CTASummary({ questionSummary }: CTASummaryProps) {
const questionTypeInfo = questionTypes.find((type) => type.id === questionSummary.question.type);
- const ctr: ChoiceResult = useMemo(() => {
- const clickedAbs = questionSummary.responses.filter((response) => response.value === "clicked").length;
- const count = questionSummary.responses.length;
- if (count === 0) return { count: 0, percentage: 0 };
- return {
- count: count,
- percentage: clickedAbs / count,
- };
- }, [questionSummary]);
-
return (
@@ -41,7 +24,7 @@ export default function CTASummary({ questionSummary }: CTASummaryProps) {
- {ctr.count} responses
+ {questionSummary.responseCount} responses
{!questionSummary.question.required && (
Optional
@@ -54,15 +37,15 @@ export default function CTASummary({ questionSummary }: CTASummaryProps) {
Clickthrough Rate (CTR)
- {Math.round(ctr.percentage * 100)}%
+ {Math.round(questionSummary.ctr.percentage)}%
- {ctr.count} {ctr.count === 1 ? "response" : "responses"}
+ {questionSummary.ctr.count} {questionSummary.ctr.count === 1 ? "response" : "responses"}
-
+
);
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CalSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CalSummary.tsx
index e15d1645cd..67d9d74592 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CalSummary.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CalSummary.tsx
@@ -1,19 +1,16 @@
import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/Headline";
import { questionTypes } from "@/app/lib/questions";
import { InboxStackIcon } from "@heroicons/react/24/solid";
-import Link from "next/link";
-import { getPersonIdentifier } from "@formbricks/lib/person/util";
-import { timeSince } from "@formbricks/lib/time";
-import { TSurveyCalQuestion, TSurveyQuestionSummary } from "@formbricks/types/surveys";
-import { PersonAvatar } from "@formbricks/ui/Avatars";
+import { TSurveySummaryCal } from "@formbricks/types/responses";
+import { ProgressBar } from "@formbricks/ui/ProgressBar";
interface CalSummaryProps {
- questionSummary: TSurveyQuestionSummary;
+ questionSummary: TSurveySummaryCal;
environmentId: string;
}
-export default function CalSummary({ questionSummary, environmentId }: CalSummaryProps) {
+export default function CalSummary({ questionSummary }: CalSummaryProps) {
const questionTypeInfo = questionTypes.find((type) => type.id === questionSummary.question.type);
return (
@@ -28,50 +25,46 @@ export default function CalSummary({ questionSummary, environmentId }: CalSummar
- {questionSummary.responses.length} Responses
+ {questionSummary.responseCount} Responses
+ {!questionSummary.question.required && (
+ Optional
+ )}
-
-
-
User
-
Response
-
Time
-
- {questionSummary.responses.map((response) => {
- const displayIdentifier = response.person ? getPersonIdentifier(response.person) : null;
- return (
-
-
- {response.person ? (
-
-
-
- {displayIdentifier}
-
-
- ) : (
-
- )}
+
+
+
+
+
Booked
+
+
+ {Math.round(questionSummary.booked.percentage)}%
+
-
- {response.value}
-
-
{timeSince(response.updatedAt.toISOString())}
- );
- })}
+
+ {questionSummary.booked.count} {questionSummary.booked.count === 1 ? "response" : "responses"}
+
+
+
+
+
+
+
+
Dismissed
+
+
+ {Math.round(questionSummary.skipped.percentage)}%
+
+
+
+
+ {questionSummary.skipped.count} {questionSummary.skipped.count === 1 ? "response" : "responses"}
+
+
+
+
);
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary.tsx
index af5ce5119e..7bc3c03e38 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary.tsx
@@ -1,46 +1,22 @@
import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/Headline";
import { questionTypes } from "@/app/lib/questions";
import { InboxStackIcon } from "@heroicons/react/24/solid";
-import { useMemo } from "react";
-import type { TSurveyQuestionSummary } from "@formbricks/types/surveys";
-import { TSurveyConsentQuestion } from "@formbricks/types/surveys";
+import { TSurveySummaryConsent } from "@formbricks/types/responses";
import { ProgressBar } from "@formbricks/ui/ProgressBar";
interface ConsentSummaryProps {
- questionSummary: TSurveyQuestionSummary
;
-}
-
-interface ChoiceResult {
- count: number;
- acceptedCount: number;
- acceptedPercentage: number;
- dismissedCount: number;
- dismissedPercentage: number;
+ questionSummary: TSurveySummaryConsent;
}
export default function ConsentSummary({ questionSummary }: ConsentSummaryProps) {
const questionTypeInfo = questionTypes.find((type) => type.id === questionSummary.question.type);
- const ctr: ChoiceResult = useMemo(() => {
- const total = questionSummary.responses.length;
- const clickedAbs = questionSummary.responses.filter((response) => response.value !== "dismissed").length;
- if (total === 0) {
- return { count: 0, acceptedCount: 0, acceptedPercentage: 0, dismissedCount: 0, dismissedPercentage: 0 };
- }
- return {
- count: total,
- acceptedCount: clickedAbs,
- acceptedPercentage: clickedAbs / total,
- dismissedCount: total - clickedAbs,
- dismissedPercentage: 1 - clickedAbs / total,
- };
- }, [questionSummary]);
-
return (
+
{questionTypeInfo && }
@@ -48,7 +24,7 @@ export default function ConsentSummary({ questionSummary }: ConsentSummaryProps)
- {ctr.count} responses
+ {questionSummary.responseCount} responses
{!questionSummary.question.required && (
Optional
@@ -62,15 +38,16 @@ export default function ConsentSummary({ questionSummary }: ConsentSummaryProps)
Accepted
- {Math.round(ctr.acceptedPercentage * 100)}%
+ {Math.round(questionSummary.accepted.percentage)}%
- {ctr.acceptedCount} {ctr.acceptedCount === 1 ? "response" : "responses"}
+ {questionSummary.accepted.count}{" "}
+ {questionSummary.accepted.count === 1 ? "response" : "responses"}
-
+
@@ -78,15 +55,16 @@ export default function ConsentSummary({ questionSummary }: ConsentSummaryProps)
Dismissed
- {Math.round(ctr.dismissedPercentage * 100)}%
+ {Math.round(questionSummary.dismissed.percentage)}%
- {ctr.dismissedCount} {ctr.dismissedCount === 1 ? "response" : "responses"}
+ {questionSummary.dismissed.count}{" "}
+ {questionSummary.dismissed.count === 1 ? "response" : "responses"}
-
+
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/DateQuestionSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/DateQuestionSummary.tsx
index 0445ae90b2..cb163d3b7e 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/DateQuestionSummary.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/DateQuestionSummary.tsx
@@ -2,27 +2,20 @@ import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId
import { questionTypes } from "@/app/lib/questions";
import { InboxStackIcon } from "@heroicons/react/24/solid";
import Link from "next/link";
-import { useState } from "react";
import { getPersonIdentifier } from "@formbricks/lib/person/util";
import { timeSince } from "@formbricks/lib/time";
import { formatDateWithOrdinal } from "@formbricks/lib/utils/datetime";
-import type { TSurveyDateQuestion, TSurveyQuestionSummary } from "@formbricks/types/surveys";
+import { TSurveySummaryDate } from "@formbricks/types/responses";
import { PersonAvatar } from "@formbricks/ui/Avatars";
interface DateQuestionSummary {
- questionSummary: TSurveyQuestionSummary;
+ questionSummary: TSurveySummaryDate;
environmentId: string;
- responsesPerPage: number;
}
-export default function DateQuestionSummary({
- questionSummary,
- environmentId,
- responsesPerPage,
-}: DateQuestionSummary) {
+export default function DateQuestionSummary({ questionSummary, environmentId }: DateQuestionSummary) {
const questionTypeInfo = questionTypes.find((type) => type.id === questionSummary.question.type);
- const [displayCount, setDisplayCount] = useState(responsesPerPage);
return (
@@ -36,7 +29,7 @@ export default function DateQuestionSummary({
- {questionSummary.responses.length} Responses
+ {questionSummary.responseCount} Responses
{!questionSummary.question.required && (
Optional
@@ -49,51 +42,39 @@ export default function DateQuestionSummary({
Response
Time
- {questionSummary.responses.slice(0, displayCount).map((response) => {
- const displayIdentifier = getPersonIdentifier(response.person!);
- return (
-
-
- {response.person ? (
-
-
-
- {displayIdentifier}
-
-
- ) : (
-
-
-
Anonymous
+ {questionSummary.samples.map((response) => (
+
+
+ {response.person ? (
+
+
- )}
-
-
- {formatDateWithOrdinal(new Date(response.value as string))}
-
-
{timeSince(response.updatedAt.toISOString())}
+
+ {getPersonIdentifier(response.person)}
+
+
+ ) : (
+
+ )}
+
+
+ {formatDateWithOrdinal(new Date(response.value as string))}
+
+
+ {timeSince(new Date(response.updatedAt).toISOString())}
- );
- })}
-
- {displayCount < questionSummary.responses.length && (
-
- setDisplayCount((prevCount) => prevCount + responsesPerPage)}
- className="my-2 flex h-8 items-center justify-center rounded-lg border border-slate-300 bg-white px-3 text-sm text-slate-500 hover:bg-slate-100 hover:text-slate-700">
- Show more
-
- )}
+ ))}
);
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/FileUploadSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/FileUploadSummary.tsx
index cc3afbe812..3b7f7bdfc4 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/FileUploadSummary.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/FileUploadSummary.tsx
@@ -7,12 +7,11 @@ import Link from "next/link";
import { getPersonIdentifier } from "@formbricks/lib/person/util";
import { getOriginalFileNameFromUrl } from "@formbricks/lib/storage/utils";
import { timeSince } from "@formbricks/lib/time";
-import type { TSurveyQuestionSummary } from "@formbricks/types/surveys";
-import { TSurveyFileUploadQuestion } from "@formbricks/types/surveys";
+import { TSurveySummaryFileUpload } from "@formbricks/types/responses";
import { PersonAvatar } from "@formbricks/ui/Avatars";
interface FileUploadSummaryProps {
- questionSummary: TSurveyQuestionSummary
;
+ questionSummary: TSurveySummaryFileUpload;
environmentId: string;
}
@@ -31,7 +30,7 @@ export default function FileUploadSummary({ questionSummary, environmentId }: Fi
- {questionSummary.responses.length} Responses
+ {questionSummary.responseCount} Responses
{!questionSummary.question.required && (
Optional
@@ -44,80 +43,72 @@ export default function FileUploadSummary({ questionSummary, environmentId }: Fi
Response
Time
- {questionSummary.responses.map((response) => {
- const displayIdentifier = response.person ? getPersonIdentifier(response.person) : null;
-
- return (
-
-
- {response.person ? (
-
-
-
- {displayIdentifier}
-
-
- ) : (
-
-
-
Anonymous
+ {questionSummary.files.map((response) => (
+
+
+ {response.person ? (
+
+
- )}
-
+
+ {getPersonIdentifier(response.person)}
+
+
+ ) : (
+
+ )}
+
-
- {response.value === "skipped" && (
+
+ {Array.isArray(response.value) &&
+ (response.value.length > 0 ? (
+ response.value.map((fileUrl, index) => {
+ const fileName = getOriginalFileNameFromUrl(fileUrl);
+
+ return (
+
+ );
+ })
+ ) : (
- )}
-
- {Array.isArray(response.value) &&
- (response.value.length > 0 ? (
- response.value.map((fileUrl, index) => {
- const fileName = getOriginalFileNameFromUrl(fileUrl);
-
- return (
-
- );
- })
- ) : (
-
- ))}
-
-
-
{timeSince(response.updatedAt.toISOString())}
+ ))}
- );
- })}
+
+
+ {timeSince(new Date(response.updatedAt).toISOString())}
+
+
+ ))}
);
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary.tsx
index 77750b13fa..685cbb3a1f 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary.tsx
@@ -1,46 +1,24 @@
import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/Headline";
import { ChatBubbleBottomCenterTextIcon, InboxStackIcon } from "@heroicons/react/24/solid";
import { Link } from "lucide-react";
-import { FC, useMemo } from "react";
+import { FC } from "react";
import { getPersonIdentifier } from "@formbricks/lib/person/util";
import { timeSince } from "@formbricks/lib/time";
import { TEnvironment } from "@formbricks/types/environment";
-import { TResponse } from "@formbricks/types/responses";
-import { TSurvey } from "@formbricks/types/surveys";
+import { TSurveySummaryHiddenField } from "@formbricks/types/responses";
import { PersonAvatar } from "@formbricks/ui/Avatars";
interface HiddenFieldsSummaryProps {
- question: string;
- survey: TSurvey;
- responses: TResponse[];
environment: TEnvironment;
+ questionSummary: TSurveySummaryHiddenField;
}
-const HiddenFieldsSummary: FC = ({ environment, responses, survey, question }) => {
- const hiddenFieldResponses = useMemo(
- () =>
- survey.hiddenFields?.fieldIds?.map((question) => {
- const questionResponses = responses
- .filter((response) => question in response.data)
- .map((r) => ({
- id: r.id,
- value: r.data[question],
- updatedAt: r.updatedAt,
- person: r.person,
- }));
- return {
- question,
- responses: questionResponses,
- };
- }),
- [responses, survey.hiddenFields?.fieldIds]
- );
-
+const HiddenFieldsSummary: FC = ({ environment, questionSummary }) => {
return (
-
+
@@ -49,7 +27,7 @@ const HiddenFieldsSummary: FC = ({ environment, respon
- {hiddenFieldResponses?.find((q) => q.question === question)?.responses?.length} Responses
+ {questionSummary.responseCount} {questionSummary.responseCount === 1 ? "Response" : "Responses"}
@@ -59,44 +37,39 @@ const HiddenFieldsSummary: FC
= ({ environment, respon
Response
Time
- {hiddenFieldResponses
- ?.find((q) => q.question === question)
- ?.responses.map((response) => {
- const displayIdentifier = getPersonIdentifier(response.person!);
- return (
-
-
- {response.person ? (
-
-
-
- {displayIdentifier}
-
-
- ) : (
-
- )}
+ {questionSummary.samples.map((response) => (
+
+
+ {response.person ? (
+
+
+
+ {getPersonIdentifier(response.person)}
+
+
+ ) : (
+
-
- {response.value}
-
-
- {timeSince(response.updatedAt.toISOString())}
-
-
- );
- })}
+ )}
+
+
+ {response.value}
+
+
+ {timeSince(new Date(response.updatedAt).toISOString())}
+
+
+ ))}
);
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.tsx
index 87894abe0b..cea3f14734 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.tsx
@@ -2,130 +2,33 @@ import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId
import { questionTypes } from "@/app/lib/questions";
import { InboxStackIcon } from "@heroicons/react/24/solid";
import Link from "next/link";
-import { useMemo, useState } from "react";
import { getPersonIdentifier } from "@formbricks/lib/person/util";
-import type { TSurveyQuestionSummary } from "@formbricks/types/surveys";
-import {
- TSurveyMultipleChoiceMultiQuestion,
- TSurveyMultipleChoiceSingleQuestion,
- TSurveyQuestionType,
-} from "@formbricks/types/surveys";
+import { TSurveySummaryMultipleChoice } from "@formbricks/types/responses";
import { PersonAvatar } from "@formbricks/ui/Avatars";
import { ProgressBar } from "@formbricks/ui/ProgressBar";
interface MultipleChoiceSummaryProps {
- questionSummary: TSurveyQuestionSummary<
- TSurveyMultipleChoiceMultiQuestion | TSurveyMultipleChoiceSingleQuestion
- >;
+ questionSummary: TSurveySummaryMultipleChoice;
environmentId: string;
surveyType: string;
- responsesPerPage: number;
-}
-
-interface ChoiceResult {
- id: string;
- label: string;
- count: number;
- percentage?: number;
- otherValues?: {
- value: string;
- person: {
- id: string;
- name?: string;
- email?: string;
- };
- }[];
}
export default function MultipleChoiceSummary({
questionSummary,
environmentId,
surveyType,
- responsesPerPage,
}: MultipleChoiceSummaryProps) {
- const isSingleChoice = questionSummary.question.type === TSurveyQuestionType.MultipleChoiceSingle;
- const [otherDisplayCount, setOtherDisplayCount] = useState(responsesPerPage);
const questionTypeInfo = questionTypes.find((type) => type.id === questionSummary.question.type);
- const results: ChoiceResult[] = useMemo(() => {
- if (!("choices" in questionSummary.question)) return [];
+ // sort by count and transform to array
+ const results = Object.values(questionSummary.choices).sort((a, b) => {
+ if (a.others) return 1; // Always put a after b if a has 'others'
+ if (b.others) return -1; // Always put b after a if b has 'others'
- // build a dictionary of choices
- const resultsDict: { [key: string]: ChoiceResult } = {};
- for (const choice of questionSummary.question.choices) {
- resultsDict[choice.label] = {
- id: choice.id,
- label: choice.label,
- count: 0,
- percentage: 0,
- otherValues: [],
- };
- }
-
- const addOtherChoice = (response, value) => {
- for (const key in resultsDict) {
- if (resultsDict[key].id === "other" && value !== "") {
- const displayIdentifier = getPersonIdentifier(response.person);
- resultsDict[key].otherValues?.push({
- value,
- person: {
- id: response.personId,
- email: typeof displayIdentifier === "string" ? displayIdentifier : undefined,
- },
- });
- resultsDict[key].count += 1;
- break;
- }
- }
- };
-
- // count the responses
- for (const response of questionSummary.responses) {
- // if single choice, only add responses that are in the choices
- if (isSingleChoice && response.value.toString() in resultsDict) {
- resultsDict[response.value.toString()].count += 1;
- } else if (isSingleChoice) {
- // if single choice and not in choices, add to other
- addOtherChoice(response, response.value);
- } else if (Array.isArray(response.value)) {
- // if multi choice add all responses
- for (const choice of response.value) {
- if (choice in resultsDict) {
- resultsDict[choice].count += 1;
- } else {
- // if multi choice and not in choices, add to other
- addOtherChoice(response, choice);
- }
- }
- }
- }
- // add the percentage
- const total = questionSummary.responses.length;
- for (const key of Object.keys(resultsDict)) {
- if (resultsDict[key].count) {
- resultsDict[key].percentage = resultsDict[key].count / total;
- }
- }
-
- // sort by count and transform to array
- const results = Object.values(resultsDict).sort((a: any, b: any) => {
- if (a.id === "other") return 1; // Always put a after b if a's id is 'other'
- if (b.id === "other") return -1; // Always put b after a if b's id is 'other'
-
- // If neither id is 'other', compare counts
- return b.count - a.count;
- });
- return results;
- }, [questionSummary, isSingleChoice]);
-
- const totalResponses = useMemo(() => {
- let total = 0;
- for (const result of results) {
- total += result.count;
- }
- return total;
- }, [results]);
+ // Sort by count
+ return b.count - a.count;
+ });
return (
@@ -139,7 +42,7 @@ export default function MultipleChoiceSummary({
- {totalResponses} responses
+ {questionSummary.responseCount} responses
{!questionSummary.question.required && (
Optional
@@ -151,16 +54,16 @@ export default function MultipleChoiceSummary({
- {results.map((result: any, resultsIdx) => (
-
+ {results.map((result, resultsIdx) => (
+
- {results.length - resultsIdx} - {result.label}
+ {results.length - resultsIdx} - {result.value}
- {Math.round(result.percentage * 100)}%
+ {Math.round(result.percentage)}%
@@ -168,16 +71,15 @@ export default function MultipleChoiceSummary({
{result.count} {result.count === 1 ? "response" : "responses"}
-
- {result.otherValues.length > 0 && (
+
+ {result.others && result.others.length > 0 && (
Specified "Other" answers
{surveyType === "web" && "User"}
- {result.otherValues
- .filter((otherValue) => otherValue !== "")
- .slice(0, otherDisplayCount)
+ {result.others
+ .filter((otherValue) => otherValue.value !== "")
.map((otherValue, idx) => (
{surveyType === "link" && (
@@ -187,7 +89,7 @@ export default function MultipleChoiceSummary({
{otherValue.value}
)}
- {surveyType === "web" && (
+ {surveyType === "web" && otherValue.person && (
))}
- {otherDisplayCount < result.otherValues.length && (
-
- setOtherDisplayCount(otherDisplayCount + responsesPerPage)}
- className="my-2 flex h-8 items-center justify-center rounded-lg border border-slate-300 bg-white px-3 text-sm text-slate-500 hover:bg-slate-100 hover:text-slate-700">
- Show more
-
-
- )}
)}
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary.tsx
index 1f15037ddb..38bd6f61c6 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary.tsx
@@ -1,82 +1,17 @@
import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/Headline";
import { questionTypes } from "@/app/lib/questions";
import { InboxStackIcon } from "@heroicons/react/24/solid";
-import { useMemo } from "react";
-import type { TSurveyQuestionSummary } from "@formbricks/types/surveys";
-import { TSurveyNPSQuestion } from "@formbricks/types/surveys";
+import { TSurveySummaryNps } from "@formbricks/types/responses";
import { HalfCircle, ProgressBar } from "@formbricks/ui/ProgressBar";
interface NPSSummaryProps {
- questionSummary: TSurveyQuestionSummary
;
-}
-
-interface Result {
- promoters: number;
- passives: number;
- detractors: number;
- total: number;
- score: number;
-}
-
-interface ChoiceResult {
- label: string;
- count: number;
- percentage: number;
+ questionSummary: TSurveySummaryNps;
}
export default function NPSSummary({ questionSummary }: NPSSummaryProps) {
- const percentage = (count, total) => {
- const result = count / total;
- return result || 0;
- };
-
const questionTypeInfo = questionTypes.find((type) => type.id === questionSummary.question.type);
- const result: Result = useMemo(() => {
- let data = {
- promoters: 0,
- passives: 0,
- detractors: 0,
- total: 0,
- score: 0,
- };
-
- for (let response of questionSummary.responses) {
- const value = response.value;
- if (typeof value !== "number") continue;
-
- data.total++;
- if (value >= 9) {
- data.promoters++;
- } else if (value >= 7) {
- data.passives++;
- } else {
- data.detractors++;
- }
- }
-
- data.score = (percentage(data.promoters, data.total) - percentage(data.detractors, data.total)) * 100;
- return data;
- }, [questionSummary]);
-
- const dismissed: ChoiceResult = useMemo(() => {
- if (questionSummary.question.required) return { count: 0, label: "Dismissed", percentage: 0 };
-
- const total = questionSummary.responses.length;
- let count = 0;
- for (const response of questionSummary.responses) {
- if (!response.value) {
- count += 1;
- }
- }
- return {
- count,
- label: "Dismissed",
- percentage: count / total,
- };
- }, [questionSummary]);
-
return (
@@ -89,7 +24,7 @@ export default function NPSSummary({ questionSummary }: NPSSummaryProps) {
- {result.total} responses
+ {questionSummary.responseCount} responses
{!questionSummary.question.required && (
Optional
@@ -104,40 +39,41 @@ export default function NPSSummary({ questionSummary }: NPSSummaryProps) {
{group}
- {Math.round(percentage(result[group], result.total) * 100)}%
+ {Math.round(questionSummary[group].percentage)}%
- {result[group]} {result[group] === 1 ? "response" : "responses"}
+ {questionSummary[group].count} {questionSummary[group].count === 1 ? "response" : "responses"}
-
+
))}
- {dismissed.count > 0 && (
+ {questionSummary.dismissed?.count > 0 && (
-
+
-
{dismissed.label}
+
dismissed
- {Math.round(dismissed.percentage * 100)}%
+ {Math.round(questionSummary.dismissed.percentage)}%
- {dismissed.count} {dismissed.count === 1 ? "response" : "responses"}
+ {questionSummary.dismissed.count}{" "}
+ {questionSummary.dismissed.count === 1 ? "response" : "responses"}
-
+
)}
-
+
);
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary.tsx
index c90c840b9e..5e012cf4ff 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary.tsx
@@ -2,27 +2,19 @@ import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId
import { questionTypes } from "@/app/lib/questions";
import { InboxStackIcon } from "@heroicons/react/24/solid";
import Link from "next/link";
-import { useState } from "react";
import { getPersonIdentifier } from "@formbricks/lib/person/util";
import { timeSince } from "@formbricks/lib/time";
-import type { TSurveyQuestionSummary } from "@formbricks/types/surveys";
-import { TSurveyOpenTextQuestion } from "@formbricks/types/surveys";
+import { TSurveySummaryOpenText } from "@formbricks/types/responses";
import { PersonAvatar } from "@formbricks/ui/Avatars";
interface OpenTextSummaryProps {
- questionSummary: TSurveyQuestionSummary;
+ questionSummary: TSurveySummaryOpenText;
environmentId: string;
- responsesPerPage: number;
}
-export default function OpenTextSummary({
- questionSummary,
- environmentId,
- responsesPerPage,
-}: OpenTextSummaryProps) {
+export default function OpenTextSummary({ questionSummary, environmentId }: OpenTextSummaryProps) {
const questionTypeInfo = questionTypes.find((type) => type.id === questionSummary.question.type);
- const [displayCount, setDisplayCount] = useState(responsesPerPage);
return (
@@ -35,7 +27,7 @@ export default function OpenTextSummary({
- {questionSummary.responses.length} Responses
+ {questionSummary.responseCount} Responses
{!questionSummary.question.required && (
Optional
@@ -48,51 +40,39 @@ export default function OpenTextSummary({
Response
Time
- {questionSummary.responses.slice(0, displayCount).map((response) => {
- const displayIdentifier = getPersonIdentifier(response.person!);
- return (
-
-
- {response.person ? (
-
-
-
- {displayIdentifier}
-
-
- ) : (
-
-
-
Anonymous
+ {questionSummary.samples.map((response) => (
+
+
+ {response.person ? (
+
+
- )}
-
-
- {response.value}
-
-
{timeSince(response.updatedAt.toISOString())}
+
+ {getPersonIdentifier(response.person)}
+
+
+ ) : (
+
+ )}
+
+
+ {response.value}
+
+
+ {timeSince(new Date(response.updatedAt).toISOString())}
- );
- })}
-
- {displayCount < questionSummary.responses.length && (
-
- setDisplayCount((prevCount) => prevCount + responsesPerPage)}
- className="my-2 flex h-8 items-center justify-center rounded-lg border border-slate-300 bg-white px-3 text-sm text-slate-500 hover:bg-slate-100 hover:text-slate-700">
- Show more
-
- )}
+ ))}
);
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary.tsx
index d4410c1c73..d2bb8a928e 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary.tsx
@@ -2,74 +2,19 @@ import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId
import { questionTypes } from "@/app/lib/questions";
import { InboxStackIcon } from "@heroicons/react/24/solid";
import Image from "next/image";
-import { useMemo } from "react";
-import type { TSurveyPictureSelectionQuestion, TSurveyQuestionSummary } from "@formbricks/types/surveys";
+import { TSurveySummaryPictureSelection } from "@formbricks/types/responses";
import { ProgressBar } from "@formbricks/ui/ProgressBar";
interface PictureChoiceSummaryProps {
- questionSummary: TSurveyQuestionSummary
;
-}
-
-interface ChoiceResult {
- id: string;
- imageUrl: string;
- count: number;
- percentage?: number;
+ questionSummary: TSurveySummaryPictureSelection;
}
export default function PictureChoiceSummary({ questionSummary }: PictureChoiceSummaryProps) {
const isMulti = questionSummary.question.allowMulti;
const questionTypeInfo = questionTypes.find((type) => type.id === questionSummary.question.type);
- const results: ChoiceResult[] = useMemo(() => {
- if (!("choices" in questionSummary.question)) return [];
-
- // build a dictionary of choices
- const resultsDict: { [key: string]: ChoiceResult } = {};
- for (const choice of questionSummary.question.choices) {
- resultsDict[choice.id] = {
- id: choice.id,
- imageUrl: choice.imageUrl,
- count: 0,
- percentage: 0,
- };
- }
-
- // count the responses
- for (const response of questionSummary.responses) {
- if (Array.isArray(response.value)) {
- for (const choice of response.value) {
- if (choice in resultsDict) {
- resultsDict[choice].count += 1;
- }
- }
- }
- }
-
- // add the percentage
- const total = questionSummary.responses.length;
- for (const key of Object.keys(resultsDict)) {
- if (resultsDict[key].count) {
- resultsDict[key].percentage = resultsDict[key].count / total;
- }
- }
-
- // sort by count and transform to array
- const results = Object.values(resultsDict).sort((a, b) => {
- return b.count - a.count;
- });
-
- return results;
- }, [questionSummary]);
-
- const totalResponses = useMemo(() => {
- let total = 0;
- for (const result of results) {
- total += result.count;
- }
- return total;
- }, [results]);
+ const results = questionSummary.choices.sort((a, b) => b.count - a.count);
return (
@@ -83,7 +28,7 @@ export default function PictureChoiceSummary({ questionSummary }: PictureChoiceS
- {totalResponses} responses
+ {questionSummary.responseCount} responses
{isMulti ? "Multi" : "Single"} Select
@@ -109,7 +54,7 @@ export default function PictureChoiceSummary({ questionSummary }: PictureChoiceS
- {Math.round((result.percentage || 0) * 100)}%
+ {Math.round(result.percentage)}%
@@ -117,7 +62,7 @@ export default function PictureChoiceSummary({ questionSummary }: PictureChoiceS
{result.count} {result.count === 1 ? "response" : "responses"}
-
+
))}
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary.tsx
index c883ea9d27..c4b77507a4 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary.tsx
@@ -4,95 +4,17 @@ import { InboxStackIcon } from "@heroicons/react/24/solid";
import { CircleSlash2, SmileIcon, StarIcon } from "lucide-react";
import { useMemo } from "react";
-import type { TSurveyQuestionSummary } from "@formbricks/types/surveys";
-import { TSurveyQuestionType } from "@formbricks/types/surveys";
-import { TSurveyRatingQuestion } from "@formbricks/types/surveys";
+import { TSurveySummaryRating } from "@formbricks/types/responses";
import { ProgressBar } from "@formbricks/ui/ProgressBar";
import { RatingResponse } from "@formbricks/ui/RatingResponse";
interface RatingSummaryProps {
- questionSummary: TSurveyQuestionSummary;
-}
-
-interface ChoiceResult {
- label: number | string;
- count: number;
- percentage: number;
+ questionSummary: TSurveySummaryRating;
}
export default function RatingSummary({ questionSummary }: RatingSummaryProps) {
const questionTypeInfo = questionTypes.find((type) => type.id === questionSummary.question.type);
- const results: ChoiceResult[] = useMemo(() => {
- if (questionSummary.question.type !== TSurveyQuestionType.Rating) return [];
- // build a dictionary of choices
- const resultsDict: { [key: string]: ChoiceResult } = {};
- for (let i = 1; i <= questionSummary.question.range; i++) {
- resultsDict[i.toString()] = {
- count: 0,
- label: i,
- percentage: 0,
- };
- }
- // count the responses
- for (const response of questionSummary.responses) {
- // if single choice, only add responses that are in the choices
- if (!Array.isArray(response.value) && response.value in resultsDict) {
- resultsDict[response.value].count += 1;
- }
- }
- // add the percentage
- const total = questionSummary.responses.length;
- for (const key of Object.keys(resultsDict)) {
- if (resultsDict[key].count) {
- resultsDict[key].percentage = resultsDict[key].count / total;
- }
- }
-
- // sort by count and transform to array
- const results = Object.values(resultsDict).sort((a: any, b: any) => a.label - b.label);
-
- return results;
- }, [questionSummary]);
-
- const dismissed: ChoiceResult = useMemo(() => {
- if (questionSummary.question.required) return { count: 0, label: "Dismissed", percentage: 0 };
-
- const total = questionSummary.responses.length;
- let count = 0;
- for (const response of questionSummary.responses) {
- if (!response.value) {
- count += 1;
- }
- }
- return {
- count,
- label: "Dismissed",
- percentage: count / total,
- };
- }, [questionSummary]);
-
- const totalResponses = useMemo(() => {
- let total = 0;
- for (const result of results) {
- total += result.count;
- }
- return total;
- }, [results]);
-
- const averageRating = useMemo(() => {
- let total = 0;
- let count = 0;
- questionSummary.responses.forEach((response) => {
- if (response.value && typeof response.value === "number") {
- total += response.value;
- count += 1;
- }
- });
- const average = count > 0 ? total / count : 0;
- return parseFloat(average.toFixed(2));
- }, [questionSummary]);
-
const getIconBasedOnScale = useMemo(() => {
const scale = questionSummary.question.scale;
if (scale === "number") return ;
@@ -112,11 +34,11 @@ export default function RatingSummary({ questionSummary }: RatingSummaryProps) {
- {totalResponses} responses
+ {questionSummary.responseCount} responses
{getIconBasedOnScale}
-
Overall: {averageRating}
+
Overall: {questionSummary.average.toFixed(2)}
{!questionSummary.question.required && (
Optional
@@ -124,20 +46,20 @@ export default function RatingSummary({ questionSummary }: RatingSummaryProps) {
- {results.map((result: any) => (
-
+ {questionSummary.choices.map((result) => (
+
-
+
- {Math.round(result.percentage * 100)}%
+ {Math.round(result.percentage)}%
@@ -145,27 +67,28 @@ export default function RatingSummary({ questionSummary }: RatingSummaryProps) {
{result.count} {result.count === 1 ? "response" : "responses"}
-
+
))}
- {dismissed.count > 0 && (
+ {questionSummary.dismissed && questionSummary.dismissed.count > 0 && (
-
+
-
{dismissed.label}
+
dismissed
- {Math.round(dismissed.percentage * 100)}%
+ {Math.round(questionSummary.dismissed.percentage)}%
- {dismissed.count} {dismissed.count === 1 ? "response" : "responses"}
+ {questionSummary.dismissed.count}{" "}
+ {questionSummary.dismissed.count === 1 ? "response" : "responses"}
-
+
)}
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs.tsx
index 5ddab5cb92..62c224b631 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs.tsx
@@ -1,164 +1,13 @@
-import { evaluateCondition } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/evaluateLogic";
import { TimerIcon } from "lucide-react";
-import { useCallback, useEffect, useMemo, useState } from "react";
-import { TResponse } from "@formbricks/types/responses";
-import { TSurvey } from "@formbricks/types/surveys";
+import { TSurveySummary } from "@formbricks/types/responses";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/Tooltip";
interface SummaryDropOffsProps {
- survey: TSurvey;
- responses: TResponse[];
- displayCount: number;
+ dropOff: TSurveySummary["dropOff"];
}
-export default function SummaryDropOffs({ responses, survey, displayCount }: SummaryDropOffsProps) {
- const initialAvgTtc = useMemo(
- () =>
- survey.questions.reduce((acc, question) => {
- acc[question.id] = 0;
- return acc;
- }, {}),
- [survey.questions]
- );
-
- const [avgTtc, setAvgTtc] = useState(initialAvgTtc);
-
- interface DropoffMetricsType {
- dropoffCount: number[];
- viewsCount: number[];
- dropoffPercentage: number[];
- }
- const [dropoffMetrics, setDropoffMetrics] = useState
({
- dropoffCount: [],
- viewsCount: [],
- dropoffPercentage: [],
- });
-
- const calculateMetrics = useCallback(() => {
- let totalTtc = { ...initialAvgTtc };
- let responseCounts = { ...initialAvgTtc };
-
- let dropoffArr = new Array(survey.questions.length).fill(0);
- let viewsArr = new Array(survey.questions.length).fill(0);
- let dropoffPercentageArr = new Array(survey.questions.length).fill(0);
-
- responses.forEach((response) => {
- // Calculate total time-to-completion
- Object.keys(avgTtc).forEach((questionId) => {
- if (response.ttc && response.ttc[questionId]) {
- totalTtc[questionId] += response.ttc[questionId];
- responseCounts[questionId]++;
- }
- });
-
- let currQuesIdx = 0;
-
- while (currQuesIdx < survey.questions.length) {
- const currQues = survey.questions[currQuesIdx];
- if (!currQues) break;
-
- if (!currQues.required) {
- if (!response.data[currQues.id]) {
- viewsArr[currQuesIdx]++;
-
- if (currQuesIdx === survey.questions.length - 1 && !response.finished) {
- dropoffArr[currQuesIdx]++;
- break;
- }
-
- const questionHasCustomLogic = currQues.logic;
- if (questionHasCustomLogic) {
- let didLogicPass = false;
- for (let logic of questionHasCustomLogic) {
- if (!logic.destination) continue;
- if (evaluateCondition(logic, response.data[currQues.id] ?? null)) {
- didLogicPass = true;
- currQuesIdx = survey.questions.findIndex((q) => q.id === logic.destination);
- break;
- }
- }
- if (!didLogicPass) currQuesIdx++;
- } else {
- currQuesIdx++;
- }
- continue;
- }
- }
-
- if (
- (response.data[currQues.id] === undefined && !response.finished) ||
- (currQues.required && !response.data[currQues.id])
- ) {
- dropoffArr[currQuesIdx]++;
- viewsArr[currQuesIdx]++;
- break;
- }
-
- viewsArr[currQuesIdx]++;
-
- let nextQuesIdx = currQuesIdx + 1;
- const questionHasCustomLogic = currQues.logic;
-
- if (questionHasCustomLogic) {
- for (let logic of questionHasCustomLogic) {
- if (!logic.destination) continue;
- if (evaluateCondition(logic, response.data[currQues.id])) {
- nextQuesIdx = survey.questions.findIndex((q) => q.id === logic.destination);
- break;
- }
- }
- }
-
- if (!response.data[survey.questions[nextQuesIdx]?.id] && !response.finished) {
- dropoffArr[nextQuesIdx]++;
- viewsArr[nextQuesIdx]++;
- break;
- }
-
- currQuesIdx = nextQuesIdx;
- }
- });
-
- // Calculate the average time for each question
- Object.keys(totalTtc).forEach((questionId) => {
- totalTtc[questionId] =
- responseCounts[questionId] > 0 ? totalTtc[questionId] / responseCounts[questionId] : 0;
- });
-
- if (!survey.welcomeCard.enabled) {
- dropoffArr[0] = displayCount - viewsArr[0];
- if (viewsArr[0] > displayCount) dropoffPercentageArr[0] = 0;
-
- dropoffPercentageArr[0] =
- viewsArr[0] - displayCount >= 0 ? 0 : ((displayCount - viewsArr[0]) / displayCount) * 100 || 0;
-
- viewsArr[0] = displayCount;
- } else {
- dropoffPercentageArr[0] = (dropoffArr[0] / viewsArr[0]) * 100;
- }
-
- for (let i = 1; i < survey.questions.length; i++) {
- if (viewsArr[i] !== 0) {
- dropoffPercentageArr[i] = (dropoffArr[i] / viewsArr[i]) * 100;
- }
- }
-
- return {
- newAvgTtc: totalTtc,
- dropoffCount: dropoffArr,
- viewsCount: viewsArr,
- dropoffPercentage: dropoffPercentageArr,
- };
- }, [responses, survey.questions, displayCount, initialAvgTtc, avgTtc, survey.welcomeCard.enabled]);
-
- useEffect(() => {
- const { newAvgTtc, dropoffCount, viewsCount, dropoffPercentage } = calculateMetrics();
- setAvgTtc(newAvgTtc);
- setDropoffMetrics({ dropoffCount, viewsCount, dropoffPercentage });
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [responses]);
-
+export default function SummaryDropOffs({ dropOff }: SummaryDropOffsProps) {
return (
@@ -179,20 +28,18 @@ export default function SummaryDropOffs({ responses, survey, displayCount }: Sum
Views
Drop Offs
- {survey.questions.map((question, i) => (
+ {dropOff.map((quesDropOff) => (
-
{question.headline}
+
{quesDropOff.headline}
- {avgTtc[question.id] !== undefined ? (avgTtc[question.id] / 1000).toFixed(2) + "s" : "N/A"}
-
-
- {dropoffMetrics.viewsCount[i]}
+ {quesDropOff.ttc > 0 ? (quesDropOff.ttc / 1000).toFixed(2) + "s" : "N/A"}
+
{quesDropOff.views}
- {dropoffMetrics.dropoffCount[i]}
- ({Math.round(dropoffMetrics.dropoffPercentage[i])}%)
+ {quesDropOff.dropOffCount}
+ ({Math.round(quesDropOff.dropOffPercentage)}%)
))}
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.tsx
index a0bce72259..510467d953 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.tsx
@@ -4,26 +4,9 @@ import ConsentSummary from "@/app/(app)/environments/[environmentId]/surveys/[su
import HiddenFieldsSummary from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary";
import { TEnvironment } from "@formbricks/types/environment";
-import { TResponse } from "@formbricks/types/responses";
+import { TSurveySummary } from "@formbricks/types/responses";
import { TSurveyQuestionType } from "@formbricks/types/surveys";
-import type {
- TSurveyCalQuestion,
- TSurveyDateQuestion,
- TSurveyFileUploadQuestion,
- TSurveyPictureSelectionQuestion,
- TSurveyQuestionSummary,
-} from "@formbricks/types/surveys";
-import {
- TSurvey,
- TSurveyCTAQuestion,
- TSurveyConsentQuestion,
- TSurveyMultipleChoiceMultiQuestion,
- TSurveyMultipleChoiceSingleQuestion,
- TSurveyNPSQuestion,
- TSurveyOpenTextQuestion,
- TSurveyQuestion,
- TSurveyRatingQuestion,
-} from "@formbricks/types/surveys";
+import { TSurvey } from "@formbricks/types/surveys";
import EmptySpaceFiller from "@formbricks/ui/EmptySpaceFiller";
import CTASummary from "./CTASummary";
@@ -36,157 +19,103 @@ import PictureChoiceSummary from "./PictureChoiceSummary";
import RatingSummary from "./RatingSummary";
interface SummaryListProps {
+ summary: TSurveySummary["summary"];
+ responseCount: number;
environment: TEnvironment;
survey: TSurvey;
- responses: TResponse[];
- responsesPerPage: number;
}
-export default function SummaryList({ environment, survey, responses, responsesPerPage }: SummaryListProps) {
- const getSummaryData = (): TSurveyQuestionSummary
[] =>
- survey.questions.map((question) => {
- const questionResponses = responses
- .filter((response) => question.id in response.data)
- .map((r) => ({
- id: r.id,
- value: r.data[question.id],
- updatedAt: r.updatedAt,
- person: r.person,
- }));
-
- return {
- question,
- responses: questionResponses,
- };
- });
-
+export default function SummaryList({ summary, environment, responseCount, survey }: SummaryListProps) {
return (
- {survey.type === "web" && responses.length === 0 && !environment.widgetSetupCompleted ? (
+ {survey.type === "web" && responseCount === 0 && !environment.widgetSetupCompleted ? (
- ) : responses.length === 0 ? (
+ ) : responseCount === 0 ? (
) : (
- <>
- {getSummaryData().map((questionSummary) => {
- if (questionSummary.question.type === TSurveyQuestionType.OpenText) {
- return (
-
}
- environmentId={environment.id}
- responsesPerPage={responsesPerPage}
- />
- );
- }
- if (
- questionSummary.question.type === TSurveyQuestionType.MultipleChoiceSingle ||
- questionSummary.question.type === TSurveyQuestionType.MultipleChoiceMulti
- ) {
- return (
-
- }
- environmentId={environment.id}
- surveyType={survey.type}
- responsesPerPage={responsesPerPage}
- />
- );
- }
- if (questionSummary.question.type === TSurveyQuestionType.NPS) {
- return (
- }
- />
- );
- }
- if (questionSummary.question.type === TSurveyQuestionType.CTA) {
- return (
- }
- />
- );
- }
- if (questionSummary.question.type === TSurveyQuestionType.Rating) {
- return (
- }
- />
- );
- }
- if (questionSummary.question.type === TSurveyQuestionType.Consent) {
- return (
- }
- />
- );
- }
- if (questionSummary.question.type === TSurveyQuestionType.PictureSelection) {
- return (
- }
- />
- );
- }
- if (questionSummary.question.type === TSurveyQuestionType.Date) {
- return (
- }
- environmentId={environment.id}
- responsesPerPage={responsesPerPage}
- />
- );
- }
- if (questionSummary.question.type === TSurveyQuestionType.FileUpload) {
- return (
- }
- environmentId={environment.id}
- />
- );
- }
+ summary.map((questionSummary) => {
+ if (questionSummary.type === TSurveyQuestionType.OpenText) {
+ return (
+
+ );
+ }
+ if (
+ questionSummary.type === TSurveyQuestionType.MultipleChoiceSingle ||
+ questionSummary.type === TSurveyQuestionType.MultipleChoiceMulti
+ ) {
+ return (
+
+ );
+ }
+ if (questionSummary.type === TSurveyQuestionType.NPS) {
+ return ;
+ }
+ if (questionSummary.type === TSurveyQuestionType.CTA) {
+ return ;
+ }
+ if (questionSummary.type === TSurveyQuestionType.Rating) {
+ return ;
+ }
+ if (questionSummary.type === TSurveyQuestionType.Consent) {
+ return ;
+ }
+ if (questionSummary.type === TSurveyQuestionType.PictureSelection) {
+ return (
+
+ );
+ }
+ if (questionSummary.type === TSurveyQuestionType.Date) {
+ return (
+
+ );
+ }
+ if (questionSummary.type === TSurveyQuestionType.FileUpload) {
+ return (
+
+ );
+ }
+ if (questionSummary.type === TSurveyQuestionType.Cal) {
+ return (
+
+ );
+ }
+ if (questionSummary.type === "hiddenField") {
+ return (
+
+ );
+ }
- if (questionSummary.question.type === TSurveyQuestionType.Cal) {
- return (
- }
- environmentId={environment.id}
- />
- );
- }
-
- return null;
- })}
-
- {survey.hiddenFields?.enabled &&
- survey.hiddenFields.fieldIds?.map((question) => {
- return (
-
- );
- })}
- >
+ return null;
+ })
)}
);
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata.tsx
index 8e1f591eb8..8960c7b5d1 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata.tsx
@@ -1,18 +1,16 @@
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/solid";
-import { useMemo, useState } from "react";
import { timeSinceConditionally } from "@formbricks/lib/time";
-import { TResponse } from "@formbricks/types/responses";
+import { TSurveySummary } from "@formbricks/types/responses";
import { TSurvey } from "@formbricks/types/surveys";
import { Button } from "@formbricks/ui/Button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/Tooltip";
interface SummaryMetadataProps {
- responses: TResponse[];
- showDropOffs: boolean;
- setShowDropOffs: React.Dispatch>;
survey: TSurvey;
- displayCount: number;
+ setShowDropOffs: React.Dispatch>;
+ showDropOffs: boolean;
+ surveySummary: TSurveySummary["meta"];
}
const StatCard = ({ label, percentage, value, tooltipText }) => (
@@ -36,8 +34,8 @@ const StatCard = ({ label, percentage, value, tooltipText }) => (
);
-function formatTime(ttc, totalResponses) {
- const seconds = ttc / (1000 * totalResponses);
+function formatTime(ttc) {
+ const seconds = ttc / 1000;
let formattedValue;
if (seconds >= 60) {
@@ -52,29 +50,21 @@ function formatTime(ttc, totalResponses) {
}
export default function SummaryMetadata({
- responses,
survey,
- displayCount,
setShowDropOffs,
showDropOffs,
+ surveySummary,
}: SummaryMetadataProps) {
- const completedResponsesCount = useMemo(() => responses.filter((r) => r.finished).length, [responses]);
- const [validTtcResponsesCount, setValidResponsesCount] = useState(0);
-
- const ttc = useMemo(() => {
- let validTtcResponsesCountAcc = 0; //stores the count of responses that contains a _total value
- const ttc = responses.reduce((acc, response) => {
- if (response.ttc?._total) {
- validTtcResponsesCountAcc++;
- return acc + response.ttc._total;
- }
- return acc;
- }, 0);
- setValidResponsesCount(validTtcResponsesCountAcc);
- return ttc;
- }, [responses]);
-
- const totalResponses = responses.length;
+ const {
+ completedPercentage,
+ completedResponses,
+ displayCount,
+ dropOffPercentage,
+ dropOffCount,
+ startsPercentage,
+ totalResponses,
+ ttcAverage,
+ } = surveySummary;
return (
@@ -88,28 +78,26 @@ export default function SummaryMetadata({
/>
- : totalResponses}
tooltipText="Number of times the survey has been started."
/>
- : completedResponsesCount}
+ percentage={`${Math.round(completedPercentage)}%`}
+ value={completedResponses === 0 ? - : completedResponses}
tooltipText="Number of times the survey has been completed."
/>
- : totalResponses - completedResponsesCount}
+ percentage={`${Math.round(dropOffPercentage)}%`}
+ value={dropOffCount === 0 ? - : dropOffCount}
tooltipText="Number of times the survey has been started but not completed."
/>
- : `${formatTime(ttc, validTtcResponsesCount)}`
- }
+ value={ttcAverage === 0 ? - : `${formatTime(ttcAverage)}`}
tooltipText="Average time to complete the survey."
/>
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.tsx
index 41279d6f8f..da2963d16f 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.tsx
@@ -1,13 +1,14 @@
"use client";
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
+import { getSurveySummaryAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
import SurveyResultsTabs from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyResultsTabs";
import SummaryDropOffs from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs";
import SummaryList from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList";
import SummaryMetadata from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata";
import CustomFilter from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter";
import SummaryHeader from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SummaryHeader";
-import { getFilterResponses } from "@/app/lib/surveys/surveys";
+import { getFormattedFilters } from "@/app/lib/surveys/surveys";
import { useSearchParams } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
@@ -15,7 +16,7 @@ import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
import { TEnvironment } from "@formbricks/types/environment";
import { TMembershipRole } from "@formbricks/types/memberships";
import { TProduct } from "@formbricks/types/product";
-import { TResponse, TSurveyPersonAttributes } from "@formbricks/types/responses";
+import { TSurveyPersonAttributes, TSurveySummary } from "@formbricks/types/responses";
import { TSurvey } from "@formbricks/types/surveys";
import { TTag } from "@formbricks/types/tags";
import { TUser } from "@formbricks/types/user";
@@ -27,47 +28,69 @@ interface SummaryPageProps {
environment: TEnvironment;
survey: TSurvey;
surveyId: string;
- responses: TResponse[];
webAppUrl: string;
product: TProduct;
user: TUser;
environmentTags: TTag[];
attributes: TSurveyPersonAttributes;
- displayCount: number;
- responsesPerPage: number;
membershipRole?: TMembershipRole;
+ responseCount: number;
}
const SummaryPage = ({
environment,
survey,
surveyId,
- responses,
webAppUrl,
product,
user,
environmentTags,
attributes,
- displayCount,
- responsesPerPage,
membershipRole,
+ responseCount,
}: SummaryPageProps) => {
const { selectedFilter, dateRange, resetState } = useResponseFilter();
+ const [surveySummary, setSurveySummary] = useState({
+ meta: {
+ completedPercentage: 0,
+ completedResponses: 0,
+ displayCount: 0,
+ dropOffPercentage: 0,
+ dropOffCount: 0,
+ startsPercentage: 0,
+ totalResponses: 0,
+ ttcAverage: 0,
+ },
+ dropOff: [],
+ summary: [],
+ });
const [showDropOffs, setShowDropOffs] = useState(false);
+
+ const filters = useMemo(
+ () => getFormattedFilters(survey, selectedFilter, dateRange),
+ [survey, selectedFilter, dateRange]
+ );
+
+ useEffect(() => {
+ const fetchSurveySummary = async () => {
+ const response = await getSurveySummaryAction(surveyId, filters);
+ setSurveySummary(response);
+ };
+ fetchSurveySummary();
+ }, [filters, surveyId]);
+
const searchParams = useSearchParams();
survey = useMemo(() => {
return checkForRecallInHeadline(survey);
}, [survey]);
+
useEffect(() => {
if (!searchParams?.get("referer")) {
resetState();
}
}, [searchParams, resetState]);
- // get the filtered array when the selected filter value changes
- const filterResponses: TResponse[] = useMemo(() => {
- return getFilterResponses(responses, selectedFilter, survey, dateRange);
- }, [selectedFilter, responses, survey, dateRange]);
+ console.log({ surveySummary });
return (
@@ -86,18 +109,17 @@ const SummaryPage = ({
- {showDropOffs && }
+ {showDropOffs && }
);
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx
index 4687577c0b..4bfd9d8120 100644
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx
@@ -3,7 +3,7 @@ import SummaryPage from "@/app/(app)/environments/[environmentId]/surveys/[surve
import { getServerSession } from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions";
-import { TEXT_RESPONSES_PER_PAGE, WEBAPP_URL } from "@formbricks/lib/constants";
+import { WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
@@ -18,7 +18,7 @@ export default async function Page({ params }) {
throw new Error("Unauthorized");
}
- const [{ responses, survey, displayCount }, environment] = await Promise.all([
+ const [{ survey, responseCount }, environment] = await Promise.all([
getAnalysisData(params.surveyId, params.environmentId),
getEnvironment(params.environmentId),
]);
@@ -51,7 +51,6 @@ export default async function Page({ params }) {
<>
>
);
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.tsx
index 100dea4514..7f3d12f1c2 100755
--- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.tsx
@@ -64,7 +64,7 @@ const getDifferenceOfDays = (from, to) => {
};
const CustomFilter = ({ environmentTags, attributes, survey }: CustomFilterProps) => {
- const { selectedFilter, setSelectedOptions, dateRange, setDateRange } = useResponseFilter();
+ const { selectedFilter, setSelectedOptions, dateRange, setDateRange, resetState } = useResponseFilter();
const [filterRange, setFilterRange] = useState(
dateRange.from && dateRange.to
? getDifferenceOfDays(dateRange.from, dateRange.to)
@@ -76,6 +76,21 @@ const CustomFilter = ({ environmentTags, attributes, survey }: CustomFilterProps
const [isDownloadDropDownOpen, setIsDownloadDropDownOpen] = useState(false);
const [hoveredRange, setHoveredRange] = useState(null);
+ const firstMountRef = useRef(true);
+
+ useEffect(() => {
+ if (!firstMountRef.current) {
+ firstMountRef.current = false;
+ return;
+ }
+ }, []);
+
+ useEffect(() => {
+ if (!firstMountRef.current) {
+ resetState();
+ }
+ }, [survey?.id]);
+
// when the page loads we get total responses and iterate over the responses and questions, tags and attributes to create the filter options
useEffect(() => {
const { questionFilterOptions, questionOptions } = generateQuestionAndFilterOptions(
diff --git a/apps/web/app/lib/surveys/surveys.ts b/apps/web/app/lib/surveys/surveys.ts
index 3f12edfb7e..b77341317a 100644
--- a/apps/web/app/lib/surveys/surveys.ts
+++ b/apps/web/app/lib/surveys/surveys.ts
@@ -8,9 +8,8 @@ import {
QuestionOptions,
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
import { QuestionFilterOptions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter";
-import { isWithinInterval } from "date-fns";
-import { TResponse, TResponseFilterCriteria, TSurveyPersonAttributes } from "@formbricks/types/responses";
+import { TResponseFilterCriteria, TSurveyPersonAttributes } from "@formbricks/types/responses";
import { TSurveyQuestionType } from "@formbricks/types/surveys";
import { TSurvey } from "@formbricks/types/surveys";
import { TTag } from "@formbricks/types/tags";
@@ -310,286 +309,9 @@ export const getFormattedFilters = (
return filters;
};
-// get the filtered responses
-export const getFilterResponses = (
- responses: TResponse[],
- selectedFilter: SelectedFilterValue,
- survey: TSurvey,
- dateRange: DateRange
-) => {
- // added the question on the response object to filter out the responses which has been selected
- let toBeFilterResponses = responses.map((r) => {
- return {
- ...r,
- questions: survey.questions.map((q) => {
- if (q.id in r.data) {
- return q;
- }
- }),
- };
- });
-
- // filtering the responses according to the value selected
- selectedFilter.filter.forEach((filter) => {
- if (filter.questionType?.type === "Questions") {
- switch (filter.questionType?.questionType) {
- case TSurveyQuestionType.Consent:
- toBeFilterResponses = toBeFilterResponses.filter((response) => {
- const questionID = response.questions.find(
- (q) => q?.type === TSurveyQuestionType.Consent && q?.id === filter?.questionType?.id
- )?.id;
- if (filter?.filterType?.filterComboBoxValue) {
- if (questionID) {
- const responseValue = response.data[questionID];
- if (filter?.filterType?.filterComboBoxValue === "Accepted") {
- return responseValue === "accepted";
- }
- if (filter?.filterType?.filterComboBoxValue === "Dismissed") {
- return responseValue === "dismissed";
- }
- return true;
- }
- return false;
- }
- return true;
- });
- break;
- case TSurveyQuestionType.OpenText:
- toBeFilterResponses = toBeFilterResponses.filter((response) => {
- const questionID = response.questions.find(
- (q) => q?.type === TSurveyQuestionType.OpenText && q?.id === filter?.questionType?.id
- )?.id;
- if (filter?.filterType?.filterComboBoxValue) {
- if (questionID) {
- const responseValue = response.data[questionID];
- if (filter?.filterType?.filterComboBoxValue === "Filled out") {
- return typeof responseValue === "string" && responseValue.trim() !== "" ? true : false;
- }
- if (filter?.filterType?.filterComboBoxValue === "Skipped") {
- return typeof responseValue === "string" && responseValue.trim() === "" ? true : false;
- }
- return true;
- }
- return false;
- }
- return true;
- });
- break;
- case TSurveyQuestionType.CTA:
- toBeFilterResponses = toBeFilterResponses.filter((response) => {
- const questionID = response.questions.find(
- (q) => q?.type === TSurveyQuestionType.CTA && q?.id === filter?.questionType?.id
- )?.id;
- if (filter?.filterType?.filterComboBoxValue) {
- if (questionID) {
- const responseValue = response.data[questionID];
- if (filter?.filterType?.filterComboBoxValue === "Clicked") {
- return responseValue === "clicked";
- }
- if (filter?.filterType?.filterComboBoxValue === "Dismissed") {
- return responseValue === "dismissed";
- }
- return true;
- }
- return false;
- }
- return true;
- });
- break;
- case TSurveyQuestionType.MultipleChoiceMulti:
- toBeFilterResponses = toBeFilterResponses.filter((response) => {
- const question = response.questions.find(
- (q) => q?.type === TSurveyQuestionType.MultipleChoiceMulti && q?.id === filter?.questionType?.id
- );
- if (filter?.filterType?.filterComboBoxValue) {
- if (question) {
- const responseValue = response.data[question.id];
- const filterValue = filter?.filterType?.filterComboBoxValue;
- if (Array.isArray(responseValue) && Array.isArray(filterValue) && filterValue.length > 0) {
- //@ts-expect-error
- const updatedResponseValue = question?.choices
- ? //@ts-expect-error
- matchAndUpdateArray([...question?.choices], [...responseValue])
- : responseValue;
- if (filter?.filterType?.filterValue === "Includes all") {
- return filterValue.every((item) => updatedResponseValue.includes(item));
- }
- if (filter?.filterType?.filterValue === "Includes either") {
- return filterValue.some((item) => updatedResponseValue.includes(item));
- }
- }
- return true;
- }
- return false;
- }
- return true;
- });
- break;
- case TSurveyQuestionType.MultipleChoiceSingle:
- toBeFilterResponses = toBeFilterResponses.filter((response) => {
- const questionID = response.questions.find(
- (q) =>
- q?.type === TSurveyQuestionType.MultipleChoiceSingle && q?.id === filter?.questionType?.id
- )?.id;
- if (filter?.filterType?.filterComboBoxValue) {
- if (questionID) {
- const responseValue = response.data[questionID];
- const filterValue = filter?.filterType?.filterComboBoxValue;
- if (
- filter?.filterType?.filterValue === "Includes either" &&
- Array.isArray(filterValue) &&
- filterValue.length > 0 &&
- typeof responseValue === "string"
- ) {
- return filterValue.includes(responseValue);
- }
- return true;
- }
- return false;
- }
- return true;
- });
- break;
- case TSurveyQuestionType.NPS:
- toBeFilterResponses = toBeFilterResponses.filter((response) => {
- const questionID = response.questions.find(
- (q) => q?.type === TSurveyQuestionType.NPS && q?.id === filter?.questionType?.id
- )?.id;
- const responseValue = questionID ? response.data[questionID] : undefined;
- const filterValue =
- filter?.filterType?.filterComboBoxValue &&
- typeof filter?.filterType?.filterComboBoxValue === "string" &&
- parseInt(filter?.filterType?.filterComboBoxValue);
- if (filter?.filterType?.filterValue === "Submitted") {
- return responseValue ? true : false;
- }
- if (filter?.filterType?.filterValue === "Skipped") {
- return responseValue === "dismissed";
- }
- if (!questionID && typeof filterValue === "number") {
- return false;
- }
- if (questionID && typeof responseValue === "number" && typeof filterValue === "number") {
- if (filter?.filterType?.filterValue === "Is equal to") {
- return responseValue === filterValue;
- }
- if (filter?.filterType?.filterValue === "Is more than") {
- return responseValue > filterValue;
- }
- if (filter?.filterType?.filterValue === "Is less than") {
- return responseValue < filterValue;
- }
- }
- return true;
- });
- break;
- case TSurveyQuestionType.Rating:
- toBeFilterResponses = toBeFilterResponses.filter((response) => {
- const questionID = response.questions.find(
- (q) => q?.type === TSurveyQuestionType.Rating && q?.id === filter?.questionType?.id
- )?.id;
- const responseValue = questionID ? response.data[questionID] : undefined;
- const filterValue =
- filter?.filterType?.filterComboBoxValue &&
- typeof filter?.filterType?.filterComboBoxValue === "string" &&
- parseInt(filter?.filterType?.filterComboBoxValue);
- if (filter?.filterType?.filterValue === "Submitted") {
- return responseValue ? true : false;
- }
- if (filter?.filterType?.filterValue === "Skipped") {
- return responseValue === "dismissed";
- }
- if (!questionID && typeof filterValue === "number") {
- return false;
- }
- if (questionID && typeof responseValue === "number" && typeof filterValue === "number") {
- if (filter?.filterType?.filterValue === "Is equal to") {
- return responseValue === filterValue;
- }
- if (filter?.filterType?.filterValue === "Is more than") {
- return responseValue > filterValue;
- }
- if (filter?.filterType?.filterValue === "Is less than") {
- return responseValue < filterValue;
- }
- }
- return true;
- });
- break;
- }
- }
- if (filter.questionType?.type === "Tags") {
- toBeFilterResponses = toBeFilterResponses.filter((response) => {
- const tagNames = response.tags.map((tag) => tag.name);
- if (filter?.filterType?.filterComboBoxValue) {
- if (filter?.filterType?.filterComboBoxValue === "Applied") {
- if (filter?.questionType?.label) return tagNames.includes(filter.questionType.label);
- }
- if (filter?.filterType?.filterComboBoxValue === "Not applied") {
- if (filter?.questionType?.label) return !tagNames.includes(filter?.questionType?.label);
- }
- }
- return true;
- });
- }
- if (filter.questionType?.type === "Attributes") {
- toBeFilterResponses = toBeFilterResponses.filter((response) => {
- if (filter?.questionType?.label && filter?.filterType?.filterComboBoxValue) {
- const attributes =
- response.personAttributes && Object.keys(response.personAttributes).length > 0
- ? response.personAttributes
- : null;
- if (attributes && attributes.hasOwnProperty(filter?.questionType?.label)) {
- if (filter?.filterType?.filterValue === "Equals") {
- return attributes[filter?.questionType?.label] === filter?.filterType?.filterComboBoxValue;
- }
- if (filter?.filterType?.filterValue === "Not equals") {
- return attributes[filter?.questionType?.label] !== filter?.filterType?.filterComboBoxValue;
- }
- } else {
- return false;
- }
- }
- return true;
- });
- }
- });
-
- // filtering for the responses which is completed
- toBeFilterResponses = toBeFilterResponses.filter((r) => (selectedFilter.onlyComplete ? r.finished : true));
-
- // filtering the data according to the dates
- if (dateRange?.from !== undefined && dateRange?.to !== undefined) {
- toBeFilterResponses = toBeFilterResponses.filter((r) =>
- isWithinInterval(r.createdAt, { start: dateRange.from!, end: dateRange.to! })
- );
- }
-
- return toBeFilterResponses;
-};
-
// get the today date with full hours
export const getTodayDate = (): Date => {
const date = new Date();
date.setHours(23, 59, 59, 999);
return date;
};
-
-// function update the response value of question multiChoiceSelect
-function matchAndUpdateArray(choices: any, responseValue: string[]) {
- const choicesArray = choices.map((obj) => obj.label);
-
- responseValue.forEach((element, index) => {
- // Check if the element is present in the choices
- if (choicesArray.includes(element)) {
- return; // No changes needed, move to the next iteration
- }
-
- // Check if the choices has 'Other'
- if (choicesArray.includes("Other") && !choicesArray.includes(element)) {
- responseValue[index] = "Other"; // Update the element to 'Other'
- }
- });
-
- return responseValue;
-}
diff --git a/apps/web/app/share/[sharingKey]/(analysis)/responses/components/ResponsePage.tsx b/apps/web/app/share/[sharingKey]/(analysis)/responses/components/ResponsePage.tsx
index 8a0d4a966d..fdbe6711d6 100644
--- a/apps/web/app/share/[sharingKey]/(analysis)/responses/components/ResponsePage.tsx
+++ b/apps/web/app/share/[sharingKey]/(analysis)/responses/components/ResponsePage.tsx
@@ -1,14 +1,16 @@
"use client";
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
-import { getFilterResponses } from "@/app/lib/surveys/surveys";
+import { getFormattedFilters } from "@/app/lib/surveys/surveys";
import SurveyResultsTabs from "@/app/share/[sharingKey]/(analysis)/components/SurveyResultsTabs";
import ResponseTimeline from "@/app/share/[sharingKey]/(analysis)/responses/components/ResponseTimeline";
+import { getResponsesUnauthorizedAction } from "@/app/share/[sharingKey]/action";
import CustomFilter from "@/app/share/[sharingKey]/components/CustomFilter";
import SummaryHeader from "@/app/share/[sharingKey]/components/SummaryHeader";
import { useSearchParams } from "next/navigation";
-import { useEffect, useMemo } from "react";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
import { TEnvironment } from "@formbricks/types/environment";
import { TProduct } from "@formbricks/types/product";
import { TResponse, TSurveyPersonAttributes } from "@formbricks/types/responses";
@@ -20,7 +22,6 @@ interface ResponsePageProps {
environment: TEnvironment;
survey: TSurvey;
surveyId: string;
- responses: TResponse[];
webAppUrl: string;
product: TProduct;
sharingKey: string;
@@ -33,36 +34,60 @@ const ResponsePage = ({
environment,
survey,
surveyId,
- responses,
product,
sharingKey,
environmentTags,
attributes,
responsesPerPage,
}: ResponsePageProps) => {
+ const [responses, setResponses] = useState([]);
+ const [page, setPage] = useState(1);
+ const [hasMore, setHasMore] = useState(true);
+
const { selectedFilter, dateRange, resetState } = useResponseFilter();
+ const filters = useMemo(
+ () => getFormattedFilters(survey, selectedFilter, dateRange),
+ [survey, selectedFilter, dateRange]
+ );
+
const searchParams = useSearchParams();
+ survey = useMemo(() => {
+ return checkForRecallInHeadline(survey);
+ }, [survey]);
+
useEffect(() => {
if (!searchParams?.get("referer")) {
resetState();
}
}, [searchParams, resetState]);
- // get the filtered array when the selected filter value changes
- const filterResponses: TResponse[] = useMemo(() => {
- return getFilterResponses(responses, selectedFilter, survey, dateRange);
- }, [selectedFilter, responses, survey, dateRange]);
+ useEffect(() => {
+ const fetchInitialResponses = async () => {
+ const responses = await getResponsesUnauthorizedAction(surveyId, 1, responsesPerPage, filters);
+ if (responses.length < responsesPerPage) {
+ setHasMore(false);
+ }
+ setResponses(responses);
+ };
+ fetchInitialResponses();
+ }, [surveyId, filters, responsesPerPage]);
+
+ const fetchNextPage = useCallback(async () => {
+ const newPage = page + 1;
+ const newResponses = await getResponsesUnauthorizedAction(surveyId, newPage, responsesPerPage, filters);
+ if (newResponses.length === 0 || newResponses.length < responsesPerPage) {
+ setHasMore(false);
+ }
+ setResponses([...responses, ...newResponses]);
+ setPage(newPage);
+ }, [filters, page, responses, responsesPerPage, surveyId]);
+
return (
-
+
);
diff --git a/apps/web/app/share/[sharingKey]/(analysis)/responses/components/ResponseTimeline.tsx b/apps/web/app/share/[sharingKey]/(analysis)/responses/components/ResponseTimeline.tsx
index 9d091238e3..c4d7a869b9 100644
--- a/apps/web/app/share/[sharingKey]/(analysis)/responses/components/ResponseTimeline.tsx
+++ b/apps/web/app/share/[sharingKey]/(analysis)/responses/components/ResponseTimeline.tsx
@@ -1,9 +1,10 @@
"use client";
-import { getMoreResponses } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
import EmptyInAppSurveys from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys";
-import { useEffect, useRef, useState } from "react";
+import { useEffect, useRef } from "react";
+import { useMembershipRole } from "@formbricks/lib/membership/hooks/useMembershipRole";
+import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { TEnvironment } from "@formbricks/types/environment";
import { TResponse } from "@formbricks/types/responses";
import { TSurvey } from "@formbricks/types/surveys";
@@ -17,7 +18,8 @@ interface ResponseTimelineProps {
responses: TResponse[];
survey: TSurvey;
environmentTags: TTag[];
- responsesPerPage: number;
+ fetchNextPage: () => void;
+ hasMore: boolean;
}
export default function ResponseTimeline({
@@ -25,29 +27,18 @@ export default function ResponseTimeline({
responses,
survey,
environmentTags,
- responsesPerPage,
+ fetchNextPage,
+ hasMore,
}: ResponseTimelineProps) {
- const [fetchedResponses, setFetchedResponses] = useState(responses);
const loadingRef = useRef(null);
- const [page, setPage] = useState(2);
- const [hasMoreResponses, setHasMoreResponses] = useState(responses.length > 0);
useEffect(() => {
const currentLoadingRef = loadingRef.current;
- const loadResponses = async () => {
- const newResponses = await getMoreResponses(survey.id, page);
- if (newResponses.length === 0) {
- setHasMoreResponses(false);
- } else {
- setPage(page + 1);
- }
- setFetchedResponses((prevResponses) => [...prevResponses, ...newResponses]);
- };
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
- if (hasMoreResponses) loadResponses();
+ if (hasMore) fetchNextPage();
}
},
{ threshold: 0.8 }
@@ -62,13 +53,16 @@ export default function ResponseTimeline({
observer.unobserve(currentLoadingRef);
}
};
- }, [responsesPerPage, page, survey.id, fetchedResponses.length, hasMoreResponses]);
+ }, [fetchNextPage, hasMore]);
+
+ const { membershipRole } = useMembershipRole(survey.environmentId);
+ const { isViewer } = getAccessFlags(membershipRole);
return (
- {survey.type === "web" && fetchedResponses.length === 0 && !environment.widgetSetupCompleted ? (
+ {survey.type === "web" && responses.length === 0 && !environment.widgetSetupCompleted ? (
- ) : fetchedResponses.length === 0 ? (
+ ) : responses.length === 0 ? (
) : (
- {fetchedResponses.map((response) => {
+ {responses.map((response) => {
return (
);
diff --git a/apps/web/app/share/[sharingKey]/(analysis)/responses/page.tsx b/apps/web/app/share/[sharingKey]/(analysis)/responses/page.tsx
index d69046cfb4..fe6f3c4253 100644
--- a/apps/web/app/share/[sharingKey]/(analysis)/responses/page.tsx
+++ b/apps/web/app/share/[sharingKey]/(analysis)/responses/page.tsx
@@ -1,4 +1,3 @@
-import { getAnalysisData } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/data";
import ResponsePage from "@/app/share/[sharingKey]/(analysis)/responses/components/ResponsePage";
import { getResultShareUrlSurveyAction } from "@/app/share/[sharingKey]/action";
import { notFound } from "next/navigation";
@@ -25,10 +24,7 @@ export default async function Page({ params }) {
throw new Error("Survey not found");
}
- const [{ responses }, environment] = await Promise.all([
- getAnalysisData(survey.id, survey.environmentId),
- getEnvironment(survey.environmentId),
- ]);
+ const environment = await getEnvironment(survey.environmentId);
if (!environment) {
throw new Error("Environment not found");
@@ -45,9 +41,8 @@ export default async function Page({ params }) {
<>
{
const { selectedFilter, dateRange, resetState } = useResponseFilter();
+ const [surveySummary, setSurveySummary] = useState({
+ meta: {
+ completedPercentage: 0,
+ completedResponses: 0,
+ displayCount: 0,
+ dropOffPercentage: 0,
+ dropOffCount: 0,
+ startsPercentage: 0,
+ totalResponses: 0,
+ ttcAverage: 0,
+ },
+ dropOff: [],
+ summary: [],
+ });
const [showDropOffs, setShowDropOffs] = useState(false);
+
+ const filters = useMemo(
+ () => getFormattedFilters(survey, selectedFilter, dateRange),
+ [survey, selectedFilter, dateRange]
+ );
+
+ useEffect(() => {
+ const fetchSurveySummary = async () => {
+ const response = await getSurveySummaryUnauthorizedAction(surveyId, filters);
+ setSurveySummary(response);
+ };
+ fetchSurveySummary();
+ }, [filters, surveyId]);
+
+ survey = useMemo(() => {
+ return checkForRecallInHeadline(survey);
+ }, [survey]);
+
const searchParams = useSearchParams();
useEffect(() => {
@@ -53,20 +83,10 @@ const SummaryPage = ({
}
}, [searchParams, resetState]);
- // get the filtered array when the selected filter value changes
- const filterResponses: TResponse[] = useMemo(() => {
- return getFilterResponses(responses, selectedFilter, survey, dateRange);
- }, [selectedFilter, responses, survey, dateRange]);
-
return (
-
+
- {showDropOffs && }
+ {showDropOffs && }
+
);
diff --git a/apps/web/app/share/[sharingKey]/(analysis)/summary/page.tsx b/apps/web/app/share/[sharingKey]/(analysis)/summary/page.tsx
index 1d39e2d14f..6bde70ad53 100644
--- a/apps/web/app/share/[sharingKey]/(analysis)/summary/page.tsx
+++ b/apps/web/app/share/[sharingKey]/(analysis)/summary/page.tsx
@@ -3,7 +3,7 @@ import SummaryPage from "@/app/share/[sharingKey]/(analysis)/summary/components/
import { getResultShareUrlSurveyAction } from "@/app/share/[sharingKey]/action";
import { notFound } from "next/navigation";
-import { REVALIDATION_INTERVAL, TEXT_RESPONSES_PER_PAGE } from "@formbricks/lib/constants";
+import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getResponsePersonAttributes } from "@formbricks/lib/response/service";
@@ -24,8 +24,7 @@ export default async function Page({ params }) {
if (!survey) {
throw new Error("Survey not found");
}
-
- const [{ responses, displayCount }, environment] = await Promise.all([
+ const [{ responseCount }, environment] = await Promise.all([
getAnalysisData(survey.id, survey.environmentId),
getEnvironment(survey.environmentId),
]);
@@ -46,15 +45,13 @@ export default async function Page({ params }) {
<>
>
);
diff --git a/apps/web/app/share/[sharingKey]/action.ts b/apps/web/app/share/[sharingKey]/action.ts
index bab2488c7b..e9951c8573 100644
--- a/apps/web/app/share/[sharingKey]/action.ts
+++ b/apps/web/app/share/[sharingKey]/action.ts
@@ -1,7 +1,27 @@
"use server";
+import { getResponses, getSurveySummary } from "@formbricks/lib/response/service";
import { getSurveyByResultShareKey } from "@formbricks/lib/survey/service";
+import { TResponse, TResponseFilterCriteria, TSurveySummary } from "@formbricks/types/responses";
export async function getResultShareUrlSurveyAction(key: string): Promise {
return getSurveyByResultShareKey(key);
}
+
+export async function getResponsesUnauthorizedAction(
+ surveyId: string,
+ page: number,
+ batchSize?: number,
+ filterCriteria?: TResponseFilterCriteria
+): Promise {
+ batchSize = batchSize ?? 10;
+ const responses = await getResponses(surveyId, page, batchSize, filterCriteria);
+ return responses;
+}
+
+export const getSurveySummaryUnauthorizedAction = async (
+ surveyId: string,
+ filterCriteria?: TResponseFilterCriteria
+): Promise => {
+ return await getSurveySummary(surveyId, filterCriteria);
+};
diff --git a/apps/web/app/share/[sharingKey]/components/CustomFilter.tsx b/apps/web/app/share/[sharingKey]/components/CustomFilter.tsx
index 5e10a21663..fd20b9ad0c 100755
--- a/apps/web/app/share/[sharingKey]/components/CustomFilter.tsx
+++ b/apps/web/app/share/[sharingKey]/components/CustomFilter.tsx
@@ -4,19 +4,14 @@ import {
DateRange,
useResponseFilter,
} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
-import { getResponsesAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
import ResponseFilter from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter";
-import { fetchFile } from "@/app/lib/fetchFile";
import { generateQuestionAndFilterOptions, getTodayDate } from "@/app/lib/surveys/surveys";
-import { createId } from "@paralleldrive/cuid2";
-import { differenceInDays, format, subDays } from "date-fns";
-import { ChevronDown, ChevronUp, DownloadIcon } from "lucide-react";
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import toast from "react-hot-toast";
+import { differenceInDays, format, startOfDay, subDays } from "date-fns";
+import { ChevronDown, ChevronUp } from "lucide-react";
+import { useCallback, useEffect, useRef, useState } from "react";
-import { getTodaysDateFormatted } from "@formbricks/lib/time";
import useClickOutside from "@formbricks/lib/useClickOutside";
-import { TResponse, TSurveyPersonAttributes } from "@formbricks/types/responses";
+import { TSurveyPersonAttributes } from "@formbricks/types/responses";
import { TSurvey } from "@formbricks/types/surveys";
import { TTag } from "@formbricks/types/tags";
import { Calendar } from "@formbricks/ui/Calendar";
@@ -32,11 +27,6 @@ enum DateSelected {
TO = "to",
}
-enum FilterDownload {
- ALL = "all",
- FILTER = "filter",
-}
-
enum FilterDropDownLabels {
ALL_TIME = "All time",
LAST_7_DAYS = "Last 7 days",
@@ -48,7 +38,6 @@ interface CustomFilterProps {
environmentTags: TTag[];
attributes: TSurveyPersonAttributes;
survey: TSurvey;
- responses: TResponse[];
}
const getDifferenceOfDays = (from, to) => {
@@ -62,7 +51,7 @@ const getDifferenceOfDays = (from, to) => {
}
};
-const CustomFilter = ({ environmentTags, attributes, responses, survey }: CustomFilterProps) => {
+const CustomFilter = ({ environmentTags, attributes, survey }: CustomFilterProps) => {
const { setSelectedOptions, dateRange, setDateRange } = useResponseFilter();
const [filterRange, setFilterRange] = useState(
dateRange.from && dateRange.to
@@ -72,7 +61,6 @@ const CustomFilter = ({ environmentTags, attributes, responses, survey }: Custom
const [selectingDate, setSelectingDate] = useState(DateSelected.FROM);
const [isDatePickerOpen, setIsDatePickerOpen] = useState(false);
const [isFilterDropDownOpen, setIsFilterDropDownOpen] = useState(false);
- const [isDownloadDropDownOpen, setIsDownloadDropDownOpen] = useState(false);
const [hoveredRange, setHoveredRange] = useState(null);
// when the page loads we get total responses and iterate over the responses and questions, tags and attributes to create the filter options
@@ -87,60 +75,6 @@ const CustomFilter = ({ environmentTags, attributes, responses, survey }: Custom
const datePickerRef = useRef(null);
- const getMatchQandA = (responses: TResponse[], survey: TSurvey) => {
- if (survey && responses) {
- // Create a mapping of question IDs to their headlines
- const questionIdToHeadline = {};
- survey.questions.forEach((question) => {
- questionIdToHeadline[question.id] = question.headline;
- });
-
- // Replace question IDs with question headlines in response data
- const updatedResponses = responses.map((response) => {
- const updatedResponse: Array<{
- id: string;
- question: string;
- answer: string;
- type: string;
- scale?: "number" | "star" | "smiley";
- range?: number;
- }> = []; // Specify the type of updatedData
- // iterate over survey questions and build the updated response
- for (const question of survey.questions) {
- const answer = response.data[question.id];
- if (answer) {
- updatedResponse.push({
- id: createId(),
- question: question.headline,
- type: question.type,
- scale: question.scale,
- range: question.range,
- answer: answer as string,
- });
- }
- }
- return { ...response, responses: updatedResponse };
- });
-
- const updatedResponsesWithTags = updatedResponses.map((response) => ({
- ...response,
- tags: response.tags?.map((tag) => tag),
- }));
-
- return updatedResponsesWithTags;
- }
- return [];
- };
-
- const downloadFileName = useMemo(() => {
- if (survey) {
- const formattedDateString = getTodaysDateFormatted("_");
- return `${survey.name.split(" ").join("_")}_responses_${formattedDateString}`.toLocaleLowerCase();
- }
-
- return "my_survey_responses";
- }, [survey]);
-
const extracMetadataKeys = useCallback((obj, parentKey = "") => {
let keys: string[] = [];
@@ -155,136 +89,6 @@ const CustomFilter = ({ environmentTags, attributes, responses, survey }: Custom
return keys;
}, []);
- const getAllResponsesInBatches = useCallback(async () => {
- const BATCH_SIZE = 3000;
- const responses: TResponse[] = [];
- for (let page = 1; ; page++) {
- const batchResponses = await getResponsesAction(survey.id, page, BATCH_SIZE);
- responses.push(...batchResponses);
- if (batchResponses.length < BATCH_SIZE) {
- break;
- }
- }
- return responses;
- }, [survey.id]);
-
- const downloadResponses = useCallback(
- async (filter: FilterDownload, filetype: "csv" | "xlsx") => {
- const downloadResponse = filter === FilterDownload.ALL ? await getAllResponsesInBatches() : responses;
- const questionNames = survey.questions?.map((question) => question.headline);
- const hiddenFieldIds = survey.hiddenFields.fieldIds;
- const hiddenFieldResponse = {};
- let metaDataFields = extracMetadataKeys(downloadResponse[0].meta);
- const userAttributes = ["Init Attribute 1", "Init Attribute 2"];
- const matchQandA = getMatchQandA(downloadResponse, survey);
- const jsonData = matchQandA.map((response) => {
- const basicInfo = {
- "Response ID": response.id,
- Timestamp: response.createdAt,
- Finished: response.finished,
- "User ID": response.person?.userId,
- "Survey ID": response.surveyId,
- };
- const metaDataKeys = extracMetadataKeys(response.meta);
- let metaData = {};
- metaDataKeys.forEach((key) => {
- if (!metaDataFields.includes(key)) metaDataFields.push(key);
- if (response.meta) {
- if (key.includes("-")) {
- const nestedKeyArray = key.split("-");
- metaData[key] = response.meta[nestedKeyArray[0].trim()][nestedKeyArray[1].trim()] ?? "";
- } else {
- metaData[key] = response.meta[key] ?? "";
- }
- }
- });
-
- const personAttributes = response.personAttributes;
- if (hiddenFieldIds && hiddenFieldIds.length > 0) {
- hiddenFieldIds.forEach((hiddenFieldId) => {
- hiddenFieldResponse[hiddenFieldId] = response.data[hiddenFieldId] ?? "";
- });
- }
- const fileResponse = { ...basicInfo, ...metaData, ...personAttributes, ...hiddenFieldResponse };
- // Map each question name to its corresponding answer
- questionNames.forEach((questionName: string) => {
- const matchingQuestion = response.responses.find((question) => question.question === questionName);
- let transformedAnswer = "";
- if (matchingQuestion) {
- const answer = matchingQuestion.answer;
- if (Array.isArray(answer)) {
- transformedAnswer = answer.join("; ");
- } else {
- transformedAnswer = answer;
- }
- }
- fileResponse[questionName] = matchingQuestion ? transformedAnswer : "";
- });
-
- return fileResponse;
- });
-
- // Fields which will be used as column headers in the file
- const fields = [
- "Response ID",
- "Timestamp",
- "Finished",
- "Survey ID",
- "User ID",
- ...metaDataFields,
- ...questionNames,
- ...(hiddenFieldIds ?? []),
- ...(survey.type === "web" ? userAttributes : []),
- ];
-
- let response;
-
- try {
- response = await fetchFile(
- {
- json: jsonData,
- fields,
- fileName: downloadFileName,
- },
- filetype
- );
- } catch (err) {
- toast.error(`Error downloading ${filetype === "csv" ? "CSV" : "Excel"}`);
- return;
- }
-
- let blob: Blob;
- if (filetype === "csv") {
- blob = new Blob([response.fileResponse], { type: "text/csv;charset=utf-8;" });
- } else if (filetype === "xlsx") {
- const binaryString = atob(response["fileResponse"]);
- const byteArray = new Uint8Array(binaryString.length);
- for (let i = 0; i < binaryString.length; i++) {
- byteArray[i] = binaryString.charCodeAt(i);
- }
- blob = new Blob([byteArray], {
- type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
- });
- } else {
- throw new Error(`Unsupported filetype: ${filetype}`);
- }
-
- const downloadUrl = URL.createObjectURL(blob);
-
- const link = document.createElement("a");
- link.href = downloadUrl;
- link.download = `${downloadFileName}.${filetype}`;
-
- document.body.appendChild(link);
- link.click();
-
- document.body.removeChild(link);
-
- URL.revokeObjectURL(downloadUrl);
- },
- [downloadFileName, extracMetadataKeys, getAllResponsesInBatches, responses, survey]
- );
-
const handleDateHoveredChange = (date: Date) => {
if (selectingDate === DateSelected.FROM) {
const startOfRange = new Date(date);
@@ -360,7 +164,7 @@ const CustomFilter = ({ environmentTags, attributes, responses, survey }: Custom
setIsFilterDropDownOpen(value);
}}>
-
+
{filterRange === FilterDropDownLabels.CUSTOM_RANGE
? `${dateRange?.from ? format(dateRange?.from, "dd LLL") : "Select first date"} - ${
@@ -388,7 +192,7 @@ const CustomFilter = ({ environmentTags, attributes, responses, survey }: Custom
className="hover:ring-0"
onClick={() => {
setFilterRange(FilterDropDownLabels.LAST_7_DAYS);
- setDateRange({ from: subDays(new Date(), 7), to: getTodayDate() });
+ setDateRange({ from: startOfDay(subDays(new Date(), 7)), to: getTodayDate() });
}}>
Last 7 days
@@ -396,7 +200,7 @@ const CustomFilter = ({ environmentTags, attributes, responses, survey }: Custom
className="hover:ring-0"
onClick={() => {
setFilterRange(FilterDropDownLabels.LAST_30_DAYS);
- setDateRange({ from: subDays(new Date(), 30), to: getTodayDate() });
+ setDateRange({ from: startOfDay(subDays(new Date(), 30)), to: getTodayDate() });
}}>
Last 30 days
@@ -411,55 +215,6 @@ const CustomFilter = ({ environmentTags, attributes, responses, survey }: Custom
- {
- value && handleDatePickerClose();
- setIsDownloadDropDownOpen(value);
- }}>
-
-
-
- Download
- {isDownloadDropDownOpen ? (
-
- ) : (
-
- )}
-
-
-
-
-
- {
- downloadResponses(FilterDownload.ALL, "csv");
- }}>
- All responses (CSV)
-
- {
- downloadResponses(FilterDownload.ALL, "xlsx");
- }}>
- All responses (Excel)
-
- {
- downloadResponses(FilterDownload.FILTER, "csv");
- }}>
- Current selection (CSV)
-
- {
- downloadResponses(FilterDownload.FILTER, "xlsx");
- }}>
- Current selection (Excel)
-
-
-
{isDatePickerOpen && (
diff --git a/packages/lib/response/service.ts b/packages/lib/response/service.ts
index 90dd889487..f67b67770f 100644
--- a/packages/lib/response/service.ts
+++ b/packages/lib/response/service.ts
@@ -15,6 +15,7 @@ import {
TResponseLegacyInput,
TResponseUpdateInput,
TSurveyPersonAttributes,
+ TSurveySummary,
ZResponse,
ZResponseFilterCriteria,
ZResponseInput,
@@ -31,8 +32,11 @@ import {
buildWhereClause,
calculateTtcTotal,
extractSurveyDetails,
+ getQuestionWiseSummary,
getResponsesFileName,
getResponsesJson,
+ getSurveySummaryDropOff,
+ getSurveySummaryMeta,
} from "../response/util";
import { responseNoteCache } from "../responseNote/cache";
import { getResponseNotes } from "../responseNote/service";
@@ -515,6 +519,53 @@ export const getResponses = async (
}));
};
+export const getSurveySummary = (
+ surveyId: string,
+ filterCriteria?: TResponseFilterCriteria
+): Promise
=> {
+ const summary = unstable_cache(
+ async () => {
+ validateInputs([surveyId, ZId], [filterCriteria, ZResponseFilterCriteria.optional()]);
+
+ const survey = await getSurvey(surveyId);
+
+ if (!survey) {
+ throw new ResourceNotFoundError("Survey", surveyId);
+ }
+
+ const batchSize = 3000;
+ const responseCount = await getResponseCountBySurveyId(surveyId);
+ const pages = Math.ceil(responseCount / batchSize);
+
+ const responsesArray = await Promise.all(
+ Array.from({ length: pages }, (_, i) => {
+ return getResponses(surveyId, i + 1, batchSize, filterCriteria);
+ })
+ );
+ const responses = responsesArray.flat();
+
+ const displayCount = await prisma.display.count({
+ where: {
+ surveyId,
+ },
+ });
+
+ const meta = getSurveySummaryMeta(responses, displayCount);
+ const dropOff = getSurveySummaryDropOff(survey, responses, displayCount);
+ const questionWiseSummary = getQuestionWiseSummary(survey, responses);
+
+ return { meta, dropOff, summary: questionWiseSummary };
+ },
+ [`getSurveySummary-${surveyId}-${JSON.stringify(filterCriteria)}`],
+ {
+ tags: [responseCache.tag.bySurveyId(surveyId)],
+ revalidate: SERVICES_REVALIDATION_INTERVAL,
+ }
+ )();
+
+ return summary;
+};
+
export const getResponseDownloadUrl = async (
surveyId: string,
format: "csv" | "xlsx",
@@ -755,15 +806,19 @@ export const deleteResponse = async (responseId: string): Promise =>
}
};
-export const getResponseCountBySurveyId = async (surveyId: string): Promise =>
+export const getResponseCountBySurveyId = async (
+ surveyId: string,
+ filterCriteria?: TResponseFilterCriteria
+): Promise =>
unstable_cache(
async () => {
- validateInputs([surveyId, ZId]);
+ validateInputs([surveyId, ZId], [filterCriteria, ZResponseFilterCriteria.optional()]);
try {
const responseCount = await prisma.response.count({
where: {
surveyId: surveyId,
+ ...buildWhereClause(filterCriteria),
},
});
return responseCount;
diff --git a/packages/lib/response/tests/__mocks__/data.mock.ts b/packages/lib/response/tests/__mocks__/data.mock.ts
index 58d2bff0d2..39581a6b1d 100644
--- a/packages/lib/response/tests/__mocks__/data.mock.ts
+++ b/packages/lib/response/tests/__mocks__/data.mock.ts
@@ -7,7 +7,9 @@ import {
TResponseFilterCriteria,
TResponseUpdateInput,
TSurveyPersonAttributes,
+ TSurveySummary,
} from "@formbricks/types/responses";
+import { TSurveyQuestionType } from "@formbricks/types/surveys";
import { TTag } from "@formbricks/types/tags";
import { transformPrismaPerson } from "../../../person/service";
@@ -458,3 +460,40 @@ export const getMockUpdateResponseInput = (finished: boolean = false): TResponse
data: mockResponseData,
finished,
});
+
+export const mockSurveySummaryOutput: TSurveySummary = {
+ dropOff: [
+ {
+ dropOffCount: 0,
+ dropOffPercentage: 0,
+ headline: "Question Text",
+ questionId: "ars2tjk8hsi8oqk1uac00mo8",
+ ttc: 0,
+ views: 0,
+ },
+ ],
+ meta: {
+ completedPercentage: 0,
+ completedResponses: 1,
+ displayCount: 0,
+ dropOffPercentage: 0,
+ dropOffCount: 0,
+ startsPercentage: 0,
+ totalResponses: 1,
+ ttcAverage: 0,
+ },
+ summary: [
+ {
+ question: {
+ headline: "Question Text",
+ id: "ars2tjk8hsi8oqk1uac00mo8",
+ inputType: "text",
+ required: false,
+ type: TSurveyQuestionType.OpenText,
+ },
+ responseCount: 0,
+ samples: [],
+ type: "openText",
+ },
+ ],
+};
diff --git a/packages/lib/response/tests/response.test.ts b/packages/lib/response/tests/response.test.ts
index 594c75b291..ad4e43c9ef 100644
--- a/packages/lib/response/tests/response.test.ts
+++ b/packages/lib/response/tests/response.test.ts
@@ -15,6 +15,7 @@ import {
mockResponseWithMockPerson,
mockSingleUseId,
mockSurveyId,
+ mockSurveySummaryOutput,
mockTags,
mockUserId,
} from "./__mocks__/data.mock";
@@ -45,6 +46,7 @@ import {
getResponses,
getResponsesByEnvironmentId,
getResponsesByPersonId,
+ getSurveySummary,
updateResponse,
} from "../service";
import { buildWhereClause } from "../util";
@@ -468,6 +470,44 @@ describe("Tests for getResponses service", () => {
});
});
+describe("Tests for getSurveySummary service", () => {
+ describe("Happy Path", () => {
+ it("Returns a summary of the survey responses", async () => {
+ prisma.survey.findUnique.mockResolvedValue(mockSurveyOutput);
+ prisma.response.findMany.mockResolvedValue([mockResponse]);
+
+ const summary = await getSurveySummary(mockSurveyId);
+ expect(summary).toEqual(mockSurveySummaryOutput);
+ });
+ });
+
+ describe("Sad Path", () => {
+ testInputValidation(getSurveySummary, 1);
+
+ it("Throws DatabaseError on PrismaClientKnownRequestError", async () => {
+ const mockErrorMessage = "Mock error message";
+ const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, {
+ code: "P2002",
+ clientVersion: "0.0.1",
+ });
+
+ prisma.survey.findUnique.mockResolvedValue(mockSurveyOutput);
+ prisma.response.findMany.mockRejectedValue(errToThrow);
+
+ await expect(getSurveySummary(mockSurveyId)).rejects.toThrow(DatabaseError);
+ });
+
+ it("Throws a generic Error for unexpected problems", async () => {
+ const mockErrorMessage = "Mock error message";
+
+ prisma.survey.findUnique.mockResolvedValue(mockSurveyOutput);
+ prisma.response.findMany.mockRejectedValue(new Error(mockErrorMessage));
+
+ await expect(getSurveySummary(mockSurveyId)).rejects.toThrow(Error);
+ });
+ });
+});
+
describe("Tests for getResponseDownloadUrl service", () => {
describe("Happy Path", () => {
it("Returns a download URL for the csv response file", async () => {
diff --git a/packages/lib/response/util.ts b/packages/lib/response/util.ts
index f1aee3e3f6..64aeeeed02 100644
--- a/packages/lib/response/util.ts
+++ b/packages/lib/response/util.ts
@@ -2,10 +2,24 @@ import "server-only";
import { Prisma } from "@prisma/client";
-import { TResponse, TResponseFilterCriteria, TResponseTtc } from "@formbricks/types/responses";
-import { TSurvey } from "@formbricks/types/surveys";
+import { TPerson } from "@formbricks/types/people";
+import {
+ TResponse,
+ TResponseFilterCriteria,
+ TResponseTtc,
+ TSurveySummary,
+ TSurveySummaryDate,
+ TSurveySummaryFileUpload,
+ TSurveySummaryHiddenField,
+ TSurveySummaryMultipleChoice,
+ TSurveySummaryOpenText,
+ TSurveySummaryPictureSelection,
+ TSurveySummaryRating,
+} from "@formbricks/types/responses";
+import { TSurvey, TSurveyQuestionType } from "@formbricks/types/surveys";
import { getTodaysDateTimeFormatted } from "../time";
+import { evaluateCondition } from "../utils/evaluateLogic";
export function calculateTtcTotal(ttc: TResponseTtc) {
const result = { ...ttc };
@@ -382,3 +396,603 @@ export const getResponsesJson = (
return jsonData;
};
+
+const convertFloatTo2Decimal = (num: number) => {
+ return Math.round(num * 100) / 100;
+};
+
+export const getSurveySummaryMeta = (
+ responses: TResponse[],
+ displayCount: number
+): TSurveySummary["meta"] => {
+ const completedResponses = responses.filter((response) => response.finished).length;
+
+ let ttcResponseCount = 0;
+ const ttcSum = responses.reduce((acc, response) => {
+ if (response.ttc?._total) {
+ ttcResponseCount++;
+ return acc + response.ttc._total;
+ }
+ return acc;
+ }, 0);
+ const responseCount = responses.length;
+
+ const startsPercentage = displayCount > 0 ? (responseCount / displayCount) * 100 : 0;
+ const completedPercentage = displayCount > 0 ? (completedResponses / displayCount) * 100 : 0;
+ const dropOffCount = responseCount - completedResponses;
+ const dropOffPercentage = responseCount > 0 ? (dropOffCount / responseCount) * 100 : 0;
+ const ttcAverage = ttcResponseCount > 0 ? ttcSum / ttcResponseCount : 0;
+
+ return {
+ displayCount: displayCount || 0,
+ totalResponses: responseCount,
+ startsPercentage: convertFloatTo2Decimal(startsPercentage),
+ completedResponses,
+ completedPercentage: convertFloatTo2Decimal(completedPercentage),
+ dropOffCount,
+ dropOffPercentage: convertFloatTo2Decimal(dropOffPercentage),
+ ttcAverage: convertFloatTo2Decimal(ttcAverage),
+ };
+};
+
+export const getSurveySummaryDropOff = (
+ survey: TSurvey,
+ responses: TResponse[],
+ displayCount: number
+): TSurveySummary["dropOff"] => {
+ const initialTtc = survey.questions.reduce((acc: Record, question) => {
+ acc[question.id] = 0;
+ return acc;
+ }, {});
+
+ let totalTtc = { ...initialTtc };
+ let responseCounts = { ...initialTtc };
+
+ let dropOffArr = new Array(survey.questions.length).fill(0) as number[];
+ let viewsArr = new Array(survey.questions.length).fill(0) as number[];
+ let dropOffPercentageArr = new Array(survey.questions.length).fill(0) as number[];
+
+ responses.forEach((response) => {
+ // Calculate total time-to-completion
+ Object.keys(totalTtc).forEach((questionId) => {
+ if (response.ttc && response.ttc[questionId]) {
+ totalTtc[questionId] += response.ttc[questionId];
+ responseCounts[questionId]++;
+ }
+ });
+
+ let currQuesIdx = 0;
+
+ while (currQuesIdx < survey.questions.length) {
+ const currQues = survey.questions[currQuesIdx];
+ if (!currQues) break;
+
+ if (!currQues.required) {
+ if (!response.data[currQues.id]) {
+ viewsArr[currQuesIdx]++;
+
+ if (currQuesIdx === survey.questions.length - 1 && !response.finished) {
+ dropOffArr[currQuesIdx]++;
+ break;
+ }
+
+ const questionHasCustomLogic = currQues.logic;
+ if (questionHasCustomLogic) {
+ let didLogicPass = false;
+ for (let logic of questionHasCustomLogic) {
+ if (!logic.destination) continue;
+ if (evaluateCondition(logic, response.data[currQues.id] ?? null)) {
+ didLogicPass = true;
+ currQuesIdx = survey.questions.findIndex((q) => q.id === logic.destination);
+ break;
+ }
+ }
+ if (!didLogicPass) currQuesIdx++;
+ } else {
+ currQuesIdx++;
+ }
+ continue;
+ }
+ }
+
+ if (
+ (response.data[currQues.id] === undefined && !response.finished) ||
+ (currQues.required && !response.data[currQues.id])
+ ) {
+ dropOffArr[currQuesIdx]++;
+ viewsArr[currQuesIdx]++;
+ break;
+ }
+
+ viewsArr[currQuesIdx]++;
+
+ let nextQuesIdx = currQuesIdx + 1;
+ const questionHasCustomLogic = currQues.logic;
+
+ if (questionHasCustomLogic) {
+ for (let logic of questionHasCustomLogic) {
+ if (!logic.destination) continue;
+ if (evaluateCondition(logic, response.data[currQues.id])) {
+ nextQuesIdx = survey.questions.findIndex((q) => q.id === logic.destination);
+ break;
+ }
+ }
+ }
+
+ if (!response.data[survey.questions[nextQuesIdx]?.id] && !response.finished) {
+ dropOffArr[nextQuesIdx]++;
+ viewsArr[nextQuesIdx]++;
+ break;
+ }
+
+ currQuesIdx = nextQuesIdx;
+ }
+ });
+
+ // Calculate the average time for each question
+ Object.keys(totalTtc).forEach((questionId) => {
+ totalTtc[questionId] =
+ responseCounts[questionId] > 0 ? totalTtc[questionId] / responseCounts[questionId] : 0;
+ });
+
+ if (!survey.welcomeCard.enabled) {
+ dropOffArr[0] = displayCount - viewsArr[0];
+ if (viewsArr[0] > displayCount) dropOffPercentageArr[0] = 0;
+
+ dropOffPercentageArr[0] =
+ viewsArr[0] - displayCount >= 0 ? 0 : ((displayCount - viewsArr[0]) / displayCount) * 100 || 0;
+
+ viewsArr[0] = displayCount;
+ } else {
+ dropOffPercentageArr[0] = (dropOffArr[0] / viewsArr[0]) * 100;
+ }
+
+ for (let i = 1; i < survey.questions.length; i++) {
+ if (viewsArr[i] !== 0) {
+ dropOffPercentageArr[i] = (dropOffArr[i] / viewsArr[i]) * 100;
+ }
+ }
+
+ const dropOff = survey.questions.map((question, index) => {
+ return {
+ questionId: question.id,
+ headline: question.headline,
+ ttc: convertFloatTo2Decimal(totalTtc[question.id]) || 0,
+ views: viewsArr[index] || 0,
+ dropOffCount: dropOffArr[index] || 0,
+ dropOffPercentage: convertFloatTo2Decimal(dropOffPercentageArr[index]) || 0,
+ };
+ });
+
+ return dropOff;
+};
+
+export const getQuestionWiseSummary = (
+ survey: TSurvey,
+ responses: TResponse[]
+): TSurveySummary["summary"] => {
+ const VALUES_LIMIT = 10;
+ let summary: TSurveySummary["summary"] = [];
+
+ survey.questions.forEach((question) => {
+ switch (question.type) {
+ case TSurveyQuestionType.OpenText: {
+ let values: TSurveySummaryOpenText["samples"] = [];
+ responses.forEach((response) => {
+ const answer = response.data[question.id];
+ if (answer && typeof answer === "string") {
+ values.push({
+ id: response.id,
+ updatedAt: response.updatedAt,
+ value: answer,
+ person: response.person,
+ });
+ }
+ });
+
+ summary.push({
+ type: question.type,
+ question,
+ responseCount: values.length,
+ samples: values.slice(0, VALUES_LIMIT),
+ });
+
+ values = [];
+ break;
+ }
+ case TSurveyQuestionType.MultipleChoiceSingle:
+ case TSurveyQuestionType.MultipleChoiceMulti: {
+ let values: TSurveySummaryMultipleChoice["choices"] = [];
+ // check last choice is others or not
+ const lastChoice = question.choices[question.choices.length - 1];
+ const isOthersEnabled = lastChoice.id === "other";
+
+ const questionChoices = question.choices.map((choice) => choice.label);
+ if (isOthersEnabled) {
+ questionChoices.pop();
+ }
+
+ let totalResponseCount = 0;
+ const choiceCountMap = questionChoices.reduce((acc: Record, choice) => {
+ acc[choice] = 0;
+ return acc;
+ }, {});
+ const otherValues: { value: string; person: TPerson | null }[] = [];
+
+ responses.forEach((response) => {
+ const answer = response.data[question.id];
+
+ if (Array.isArray(answer)) {
+ answer.forEach((value) => {
+ totalResponseCount++;
+ if (questionChoices.includes(value)) {
+ choiceCountMap[value]++;
+ } else {
+ otherValues.push({
+ value,
+ person: response.person,
+ });
+ }
+ });
+ } else if (typeof answer === "string") {
+ totalResponseCount++;
+ if (questionChoices.includes(answer)) {
+ choiceCountMap[answer]++;
+ } else {
+ otherValues.push({
+ value: answer,
+ person: response.person,
+ });
+ }
+ }
+ });
+
+ Object.entries(choiceCountMap).map(([label, count]) => {
+ values.push({
+ value: label,
+ count,
+ percentage:
+ totalResponseCount > 0 ? convertFloatTo2Decimal((count / totalResponseCount) * 100) : 0,
+ });
+ });
+
+ if (isOthersEnabled) {
+ values.push({
+ value: lastChoice.label || "Other",
+ count: otherValues.length,
+ percentage: convertFloatTo2Decimal((otherValues.length / totalResponseCount) * 100),
+ others: otherValues.slice(0, VALUES_LIMIT),
+ });
+ }
+
+ summary.push({
+ type: question.type,
+ question,
+ responseCount: totalResponseCount,
+ choices: values,
+ });
+
+ values = [];
+ break;
+ }
+ case TSurveyQuestionType.PictureSelection: {
+ let values: TSurveySummaryPictureSelection["choices"] = [];
+ const choiceCountMap: Record = {};
+
+ question.choices.forEach((choice) => {
+ choiceCountMap[choice.id] = 0;
+ });
+ let totalResponseCount = 0;
+
+ responses.forEach((response) => {
+ const answer = response.data[question.id];
+ if (Array.isArray(answer)) {
+ answer.forEach((value) => {
+ totalResponseCount++;
+ choiceCountMap[value]++;
+ });
+ }
+ });
+
+ question.choices.forEach((choice) => {
+ values.push({
+ id: choice.id,
+ imageUrl: choice.imageUrl,
+ count: choiceCountMap[choice.id],
+ percentage:
+ totalResponseCount > 0
+ ? convertFloatTo2Decimal((choiceCountMap[choice.id] / totalResponseCount) * 100)
+ : 0,
+ });
+ });
+
+ summary.push({
+ type: question.type,
+ question,
+ responseCount: totalResponseCount,
+ choices: values,
+ });
+
+ values = [];
+ break;
+ }
+ case TSurveyQuestionType.Rating: {
+ let values: TSurveySummaryRating["choices"] = [];
+ const choiceCountMap: Record = {};
+ const range = question.range;
+
+ for (let i = 1; i <= range; i++) {
+ choiceCountMap[i] = 0;
+ }
+
+ let totalResponseCount = 0;
+ let totalRating = 0;
+ let dismissed = 0;
+
+ responses.forEach((response) => {
+ const answer = response.data[question.id];
+ if (typeof answer === "number") {
+ totalResponseCount++;
+ choiceCountMap[answer]++;
+ totalRating += answer;
+ } else if (response.ttc && response.ttc[question.id] > 0) {
+ totalResponseCount++;
+ dismissed++;
+ }
+ });
+
+ Object.entries(choiceCountMap).map(([label, count]) => {
+ values.push({
+ rating: parseInt(label),
+ count,
+ percentage:
+ totalResponseCount > 0 ? convertFloatTo2Decimal((count / totalResponseCount) * 100) : 0,
+ });
+ });
+
+ summary.push({
+ type: question.type,
+ question,
+ average: convertFloatTo2Decimal(totalRating / (totalResponseCount - dismissed)) || 0,
+ responseCount: totalResponseCount,
+ choices: values,
+ dismissed: {
+ count: dismissed,
+ percentage:
+ totalResponseCount > 0 ? convertFloatTo2Decimal((dismissed / totalResponseCount) * 100) : 0,
+ },
+ });
+
+ values = [];
+ break;
+ }
+ case TSurveyQuestionType.NPS: {
+ const data = {
+ promoters: 0,
+ passives: 0,
+ detractors: 0,
+ dismissed: 0,
+ total: 0,
+ score: 0,
+ };
+
+ responses.forEach((response) => {
+ const value = response.data[question.id];
+ if (typeof value === "number") {
+ data.total++;
+ if (value >= 9) {
+ data.promoters++;
+ } else if (value >= 7) {
+ data.passives++;
+ } else {
+ data.detractors++;
+ }
+ } else if (response.ttc && response.ttc[question.id] > 0) {
+ data.total++;
+ data.dismissed++;
+ }
+ });
+
+ data.score =
+ data.total > 0
+ ? convertFloatTo2Decimal(((data.promoters - data.detractors) / data.total) * 100)
+ : 0;
+
+ summary.push({
+ type: question.type,
+ question,
+ responseCount: data.total,
+ total: data.total,
+ score: data.score,
+ promoters: {
+ count: data.promoters,
+ percentage: data.total > 0 ? convertFloatTo2Decimal((data.promoters / data.total) * 100) : 0,
+ },
+ passives: {
+ count: data.passives,
+ percentage: data.total > 0 ? convertFloatTo2Decimal((data.passives / data.total) * 100) : 0,
+ },
+ detractors: {
+ count: data.detractors,
+ percentage: data.total > 0 ? convertFloatTo2Decimal((data.detractors / data.total) * 100) : 0,
+ },
+ dismissed: {
+ count: data.dismissed,
+ percentage: data.total > 0 ? convertFloatTo2Decimal((data.dismissed / data.total) * 100) : 0,
+ },
+ });
+ break;
+ }
+ case TSurveyQuestionType.CTA: {
+ const data = {
+ clicked: 0,
+ dismissed: 0,
+ };
+
+ responses.forEach((response) => {
+ const value = response.data[question.id];
+ if (value === "clicked") {
+ data.clicked++;
+ } else if (value === "dismissed") {
+ data.dismissed++;
+ }
+ });
+
+ const totalResponses = data.clicked + data.dismissed;
+
+ summary.push({
+ type: question.type,
+ question,
+ responseCount: totalResponses,
+ ctr: {
+ count: data.clicked,
+ percentage:
+ totalResponses > 0 ? convertFloatTo2Decimal((data.clicked / totalResponses) * 100) : 0,
+ },
+ });
+ break;
+ }
+ case TSurveyQuestionType.Consent: {
+ const data = {
+ accepted: 0,
+ dismissed: 0,
+ };
+
+ responses.forEach((response) => {
+ const value = response.data[question.id];
+ if (value === "accepted") {
+ data.accepted++;
+ } else if (response.ttc && response.ttc[question.id] > 0) {
+ data.dismissed++;
+ }
+ });
+
+ const totalResponses = data.accepted + data.dismissed;
+
+ summary.push({
+ type: question.type,
+ question,
+ responseCount: totalResponses,
+ accepted: {
+ count: data.accepted,
+ percentage:
+ totalResponses > 0 ? convertFloatTo2Decimal((data.accepted / totalResponses) * 100) : 0,
+ },
+ dismissed: {
+ count: data.dismissed,
+ percentage:
+ totalResponses > 0 ? convertFloatTo2Decimal((data.dismissed / totalResponses) * 100) : 0,
+ },
+ });
+
+ break;
+ }
+ case TSurveyQuestionType.Date: {
+ let values: TSurveySummaryDate["samples"] = [];
+ responses.forEach((response) => {
+ const answer = response.data[question.id];
+ if (answer && typeof answer === "string") {
+ values.push({
+ id: response.id,
+ updatedAt: response.updatedAt,
+ value: answer,
+ person: response.person,
+ });
+ }
+ });
+
+ summary.push({
+ type: question.type,
+ question,
+ responseCount: values.length,
+ samples: values.slice(0, VALUES_LIMIT),
+ });
+
+ values = [];
+ break;
+ }
+ case TSurveyQuestionType.FileUpload: {
+ let values: TSurveySummaryFileUpload["files"] = [];
+ responses.forEach((response) => {
+ const answer = response.data[question.id];
+ if (Array.isArray(answer)) {
+ values.push({
+ id: response.id,
+ updatedAt: response.updatedAt,
+ value: answer,
+ person: response.person,
+ });
+ }
+ });
+
+ summary.push({
+ type: question.type,
+ question,
+ responseCount: values.length,
+ files: values.slice(0, VALUES_LIMIT),
+ });
+
+ values = [];
+ break;
+ }
+ case TSurveyQuestionType.Cal: {
+ const data = {
+ booked: 0,
+ skipped: 0,
+ };
+
+ responses.forEach((response) => {
+ const value = response.data[question.id];
+ if (value === "booked") {
+ data.booked++;
+ } else if (response.ttc && response.ttc[question.id] > 0) {
+ data.skipped++;
+ }
+ });
+ const totalResponses = data.booked + data.skipped;
+
+ summary.push({
+ type: question.type,
+ question,
+ responseCount: totalResponses,
+ booked: {
+ count: data.booked,
+ percentage: totalResponses > 0 ? convertFloatTo2Decimal((data.booked / totalResponses) * 100) : 0,
+ },
+ skipped: {
+ count: data.skipped,
+ percentage:
+ totalResponses > 0 ? convertFloatTo2Decimal((data.skipped / totalResponses) * 100) : 0,
+ },
+ });
+
+ break;
+ }
+ }
+ });
+
+ survey.hiddenFields?.fieldIds?.forEach((question) => {
+ let values: TSurveySummaryHiddenField["samples"] = [];
+ responses.forEach((response) => {
+ const answer = response.data[question];
+ if (answer && typeof answer === "string") {
+ values.push({
+ updatedAt: response.updatedAt,
+ value: answer,
+ person: response.person,
+ });
+ }
+ });
+
+ summary.push({
+ type: "hiddenField",
+ question,
+ responseCount: values.length,
+ samples: values.slice(0, VALUES_LIMIT),
+ });
+
+ values = [];
+ });
+
+ return summary;
+};
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/evaluateLogic.ts b/packages/lib/utils/evaluateLogic.ts
similarity index 100%
rename from apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/evaluateLogic.ts
rename to packages/lib/utils/evaluateLogic.ts
diff --git a/packages/types/responses.ts b/packages/types/responses.ts
index 56d391fe75..9351c4280e 100644
--- a/packages/types/responses.ts
+++ b/packages/types/responses.ts
@@ -1,7 +1,21 @@
import { z } from "zod";
import { ZPerson, ZPersonAttributes } from "./people";
-import { ZSurvey, ZSurveyLogicCondition } from "./surveys";
+import {
+ ZSurvey,
+ ZSurveyCTAQuestion,
+ ZSurveyCalQuestion,
+ ZSurveyConsentQuestion,
+ ZSurveyDateQuestion,
+ ZSurveyFileUploadQuestion,
+ ZSurveyLogicCondition,
+ ZSurveyMultipleChoiceMultiQuestion,
+ ZSurveyMultipleChoiceSingleQuestion,
+ ZSurveyNPSQuestion,
+ ZSurveyOpenTextQuestion,
+ ZSurveyPictureSelectionQuestion,
+ ZSurveyRatingQuestion,
+} from "./surveys";
import { ZTag } from "./tags";
export const ZResponseData = z.record(z.union([z.string(), z.number(), z.array(z.string())]));
@@ -249,3 +263,235 @@ export const ZResponseUpdate = z.object({
});
export type TResponseUpdate = z.infer;
+
+export const ZSurveySummaryOpenText = z.object({
+ type: z.literal("openText"),
+ question: ZSurveyOpenTextQuestion,
+ responseCount: z.number(),
+ samples: z.array(
+ z.object({
+ id: z.string(),
+ updatedAt: z.date(),
+ value: z.string(),
+ person: ZPerson.nullable(),
+ })
+ ),
+});
+
+export type TSurveySummaryOpenText = z.infer;
+
+export const ZSurveySummaryMultipleChoice = z.object({
+ type: z.union([z.literal("multipleChoiceMulti"), z.literal("multipleChoiceSingle")]),
+ question: z.union([ZSurveyMultipleChoiceSingleQuestion, ZSurveyMultipleChoiceMultiQuestion]),
+ responseCount: z.number(),
+ choices: z.array(
+ z.object({
+ value: z.string(),
+ count: z.number(),
+ percentage: z.number(),
+ others: z
+ .array(
+ z.object({
+ value: z.string(),
+ person: ZPerson.nullable(),
+ })
+ )
+ .optional(),
+ })
+ ),
+});
+
+export type TSurveySummaryMultipleChoice = z.infer;
+
+export const ZSurveySummaryPictureSelection = z.object({
+ type: z.literal("pictureSelection"),
+ question: ZSurveyPictureSelectionQuestion,
+ responseCount: z.number(),
+ choices: z.array(
+ z.object({
+ id: z.string(),
+ imageUrl: z.string(),
+ count: z.number(),
+ percentage: z.number(),
+ })
+ ),
+});
+
+export type TSurveySummaryPictureSelection = z.infer;
+
+export const ZSurveySummaryRating = z.object({
+ type: z.literal("rating"),
+ question: ZSurveyRatingQuestion,
+ responseCount: z.number(),
+ average: z.number(),
+ choices: z.array(
+ z.object({
+ rating: z.number(),
+ count: z.number(),
+ percentage: z.number(),
+ })
+ ),
+ dismissed: z.object({
+ count: z.number(),
+ percentage: z.number(),
+ }),
+});
+
+export type TSurveySummaryRating = z.infer;
+
+export const ZSurveySummaryNps = z.object({
+ type: z.literal("nps"),
+ question: ZSurveyNPSQuestion,
+ responseCount: z.number(),
+ total: z.number(),
+ score: z.number(),
+ promoters: z.object({
+ count: z.number(),
+ percentage: z.number(),
+ }),
+ passives: z.object({
+ count: z.number(),
+ percentage: z.number(),
+ }),
+ detractors: z.object({
+ count: z.number(),
+ percentage: z.number(),
+ }),
+ dismissed: z.object({
+ count: z.number(),
+ percentage: z.number(),
+ }),
+});
+
+export type TSurveySummaryNps = z.infer;
+
+export const ZSurveySummaryCta = z.object({
+ type: z.literal("cta"),
+ question: ZSurveyCTAQuestion,
+ responseCount: z.number(),
+ ctr: z.object({
+ count: z.number(),
+ percentage: z.number(),
+ }),
+});
+
+export type TSurveySummaryCta = z.infer;
+
+export const ZSurveySummaryConsent = z.object({
+ type: z.literal("consent"),
+ question: ZSurveyConsentQuestion,
+ responseCount: z.number(),
+ accepted: z.object({
+ count: z.number(),
+ percentage: z.number(),
+ }),
+ dismissed: z.object({
+ count: z.number(),
+ percentage: z.number(),
+ }),
+});
+
+export type TSurveySummaryConsent = z.infer;
+
+export const ZSurveySummaryDate = z.object({
+ type: z.literal("date"),
+ question: ZSurveyDateQuestion,
+ responseCount: z.number(),
+ samples: z.array(
+ z.object({
+ id: z.string(),
+ updatedAt: z.date(),
+ value: z.string(),
+ person: ZPerson.nullable(),
+ })
+ ),
+});
+
+export type TSurveySummaryDate = z.infer;
+
+export const ZSurveySummaryFileUpload = z.object({
+ type: z.literal("fileUpload"),
+ question: ZSurveyFileUploadQuestion,
+ responseCount: z.number(),
+ files: z.array(
+ z.object({
+ id: z.string(),
+ updatedAt: z.date(),
+ value: z.array(z.string()),
+ person: ZPerson.nullable(),
+ })
+ ),
+});
+
+export type TSurveySummaryFileUpload = z.infer;
+
+export const ZSurveySummaryCal = z.object({
+ type: z.literal("cal"),
+ question: ZSurveyCalQuestion,
+ responseCount: z.number(),
+ booked: z.object({
+ count: z.number(),
+ percentage: z.number(),
+ }),
+ skipped: z.object({
+ count: z.number(),
+ percentage: z.number(),
+ }),
+});
+
+export type TSurveySummaryCal = z.infer;
+
+export const ZSurveySummaryHiddenField = z.object({
+ type: z.literal("hiddenField"),
+ question: z.string(),
+ responseCount: z.number(),
+ samples: z.array(
+ z.object({
+ updatedAt: z.date(),
+ value: z.string(),
+ person: ZPerson.nullable(),
+ })
+ ),
+});
+
+export type TSurveySummaryHiddenField = z.infer;
+
+export const ZSurveySummary = z.object({
+ meta: z.object({
+ displayCount: z.number(),
+ totalResponses: z.number(),
+ startsPercentage: z.number(),
+ completedResponses: z.number(),
+ completedPercentage: z.number(),
+ dropOffCount: z.number(),
+ dropOffPercentage: z.number(),
+ ttcAverage: z.number(),
+ }),
+ dropOff: z.array(
+ z.object({
+ questionId: z.string().cuid2(),
+ headline: z.string(),
+ ttc: z.number(),
+ views: z.number(),
+ dropOffCount: z.number(),
+ dropOffPercentage: z.number(),
+ })
+ ),
+ summary: z.array(
+ z.union([
+ ZSurveySummaryOpenText,
+ ZSurveySummaryMultipleChoice,
+ ZSurveySummaryPictureSelection,
+ ZSurveySummaryRating,
+ ZSurveySummaryNps,
+ ZSurveySummaryCta,
+ ZSurveySummaryConsent,
+ ZSurveySummaryDate,
+ ZSurveySummaryFileUpload,
+ ZSurveySummaryCal,
+ ZSurveySummaryHiddenField,
+ ])
+ ),
+});
+
+export type TSurveySummary = z.infer;
diff --git a/packages/ui/PictureSelectionResponse/index.tsx b/packages/ui/PictureSelectionResponse/index.tsx
index da5d8353e0..26d9f4865f 100644
--- a/packages/ui/PictureSelectionResponse/index.tsx
+++ b/packages/ui/PictureSelectionResponse/index.tsx
@@ -21,7 +21,7 @@ export const PictureSelectionResponse = ({ choices, selected }: PictureSelection
return (
{selected.map((id) => (
-
+
{choiceImageMapping[id] && (
void;
deleteResponse?: (responseId: string) => void;
+ isViewer: boolean;
}
interface TooltipRendererProps {
@@ -83,6 +81,7 @@ export default function SingleResponseCard({
environment,
updateResponse,
deleteResponse,
+ isViewer,
}: SingleResponseCardProps) {
const environmentId = survey.environmentId;
const router = useRouter();
@@ -95,8 +94,7 @@ export default function SingleResponseCard({
: isSubmissionTimeMoreThan5Minutes(response.updatedAt);
let skippedQuestions: string[][] = [];
let temp: string[] = [];
- const { membershipRole, isLoading, error } = useMembershipRole(environmentId);
- const { isViewer } = getAccessFlags(membershipRole);
+
const isFirstQuestionAnswered = response.data[survey.questions[0].id] ? true : false;
function isValidValue(value: any) {
@@ -424,18 +422,14 @@ export default function SingleResponseCard({
)}
- {user && (
-
- {!isViewer && (
- ({ tagId: tag.id, tagName: tag.name }))}
- environmentTags={environmentTags}
- updateFetchedResponses={updateFetchedResponses}
- />
- )}
-
+ {user && !isViewer && (
+
({ tagId: tag.id, tagName: tag.name }))}
+ environmentTags={environmentTags}
+ updateFetchedResponses={updateFetchedResponses}
+ />
)}