feat: add survey schedule option (#2386)

Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
Co-authored-by: Johannes <johannes@formbricks.com>
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
This commit is contained in:
Matti Nannt
2024-04-08 11:36:06 +02:00
committed by GitHub
parent c44c0b83e3
commit 08ce026c7a
28 changed files with 252 additions and 130 deletions

View File

@@ -1,4 +1,4 @@
name: Cron - reportUsageToStripe
name: Cron - Report usage to Stripe
on:
# "Scheduled workflows run on the latest commit on the default or base branch."

View File

@@ -1,4 +1,4 @@
name: Cron - closeOnDate
name: Cron - Survey status update
on:
# "Scheduled workflows run on the latest commit on the default or base branch."
@@ -16,7 +16,7 @@ jobs:
- name: cURL request
if: ${{ env.APP_URL && env.CRON_SECRET }}
run: |
curl ${{ env.APP_URL }}/api/cron/close_surveys \
curl ${{ env.APP_URL }}/api/cron/survey-status \
-X POST \
-H 'content-type: application/json' \
-H 'x-api-key: ${{ env.CRON_SECRET }}' \

View File

@@ -1,4 +1,4 @@
name: Cron - weeklySummary
name: Cron - Weekly summary
on:
# "Scheduled workflows run on the latest commit on the default or base branch."
@@ -16,7 +16,7 @@ jobs:
- name: cURL request
if: ${{ env.APP_URL && env.CRON_SECRET }}
run: |
curl ${{ env.APP_URL }}/api/cron/weekly_summary \
curl ${{ env.APP_URL }}/api/cron/weekly-summary \
-X POST \
-H 'content-type: application/json' \
-H 'x-api-key: ${{ env.CRON_SECRET }}' \

View File

@@ -119,6 +119,7 @@ const SummaryHeader = ({
<SurveyStatusIndicator status={survey.status} />
)}
<span className="ml-1 text-sm text-slate-700">
{survey.status === "scheduled" && "Scheduled"}
{survey.status === "inProgress" && "In-progress"}
{survey.status === "paused" && "Paused"}
{survey.status === "completed" && "Completed"}

View File

@@ -10,18 +10,22 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { SurveyStatusIndicator } from "@formbricks/ui/SurveyStatusIndicator";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/Tooltip";
interface SurveyStatusDropdownProps {
environment: TEnvironment;
updateLocalSurveyStatus?: (status: TSurvey["status"]) => void;
survey: TSurvey;
}
export default function SurveyStatusDropdown({
environment,
updateLocalSurveyStatus,
survey,
}: {
environment: TEnvironment;
updateLocalSurveyStatus?: (status: "draft" | "inProgress" | "paused" | "completed" | "archived") => void;
survey: TSurvey;
}) {
}: SurveyStatusDropdownProps) {
const isCloseOnDateEnabled = survey.closeOnDate !== null;
const closeOnDate = survey.closeOnDate ? new Date(survey.closeOnDate) : null;
const isStatusChangeDisabled = (isCloseOnDateEnabled && closeOnDate && closeOnDate < new Date()) ?? false;
const isStatusChangeDisabled =
(survey.status === "scheduled" || (isCloseOnDateEnabled && closeOnDate && closeOnDate < new Date())) ??
false;
return (
<>
@@ -34,7 +38,7 @@ export default function SurveyStatusDropdown({
value={survey.status}
disabled={isStatusChangeDisabled}
onValueChange={(value) => {
const castedValue = value as "draft" | "inProgress" | "paused" | "completed";
const castedValue = value as TSurvey["status"];
updateSurveyAction({ ...survey, status: castedValue })
.then(() => {
toast.success(
@@ -51,8 +55,7 @@ export default function SurveyStatusDropdown({
toast.error(`Error: ${error.message}`);
});
if (updateLocalSurveyStatus)
updateLocalSurveyStatus(value as "draft" | "inProgress" | "paused" | "completed" | "archived");
if (updateLocalSurveyStatus) updateLocalSurveyStatus(value as TSurvey["status"]);
}}>
<TooltipProvider delayDuration={50}>
<Tooltip open={isStatusChangeDisabled ? undefined : false}>
@@ -64,6 +67,7 @@ export default function SurveyStatusDropdown({
<SurveyStatusIndicator status={survey.status} />
)}
<span className="ml-2 text-sm text-slate-700">
{survey.status === "scheduled" && "Scheduled"}
{survey.status === "inProgress" && "In-progress"}
{survey.status === "paused" && "Paused"}
{survey.status === "completed" && "Completed"}
@@ -88,8 +92,8 @@ export default function SurveyStatusDropdown({
</SelectContent>
<TooltipContent>
To update the survey status, update the &ldquo;Close
<br /> survey on date&rdquo; setting in the Response Options.
To update the survey status, update the schedule and close setting in the survey response
options.
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

@@ -1,7 +1,8 @@
"use client";
import * as Collapsible from "@radix-ui/react-collapsible";
import { CheckIcon } from "lucide-react";
import { ArrowUpRight, CheckIcon } from "lucide-react";
import Link from "next/link";
import { KeyboardEventHandler, useEffect, useState } from "react";
import toast from "react-hot-toast";
@@ -27,7 +28,8 @@ export default function ResponseOptionsCard({
const [open, setOpen] = useState(localSurvey.type === "link" ? true : false);
const autoComplete = localSurvey.autoComplete !== null;
const [redirectToggle, setRedirectToggle] = useState(false);
const [surveyCloseOnDateToggle, setSurveyCloseOnDateToggle] = useState(false);
const [runOnDateToggle, setRunOnDateToggle] = useState(false);
const [closeOnDateToggle, setCloseOnDateToggle] = useState(false);
useState;
const [redirectUrl, setRedirectUrl] = useState<string | null>("");
const [surveyClosedMessageToggle, setSurveyClosedMessageToggle] = useState(false);
@@ -48,7 +50,8 @@ export default function ResponseOptionsCard({
name: "",
subheading: "",
});
const [closeOnDate, setCloseOnDate] = useState<Date>();
const [runOnDate, setRunOnDate] = useState<Date | null>(null);
const [closeOnDate, setCloseOnDate] = useState<Date | null>(null);
const isPinProtectionEnabled = localSurvey.pin !== null;
@@ -63,19 +66,28 @@ export default function ResponseOptionsCard({
}
};
const handleSurveyCloseOnDateToggle = () => {
if (surveyCloseOnDateToggle && localSurvey.closeOnDate) {
setSurveyCloseOnDateToggle(false);
setCloseOnDate(undefined);
setLocalSurvey({ ...localSurvey, closeOnDate: null });
return;
const handleRunOnDateToggle = () => {
if (runOnDateToggle) {
setRunOnDateToggle(false);
if (localSurvey.runOnDate) {
setRunOnDate(null);
setLocalSurvey({ ...localSurvey, runOnDate: null });
}
} else {
setRunOnDateToggle(true);
}
};
if (surveyCloseOnDateToggle) {
setSurveyCloseOnDateToggle(false);
return;
const handleCloseOnDateToggle = () => {
if (closeOnDateToggle) {
setCloseOnDateToggle(false);
if (localSurvey.closeOnDate) {
setCloseOnDate(null);
setLocalSurvey({ ...localSurvey, closeOnDate: null });
}
} else {
setCloseOnDateToggle(true);
}
setSurveyCloseOnDateToggle(true);
};
const handleProtectSurveyWithPinToggle = () => {
@@ -126,6 +138,15 @@ export default function ResponseOptionsCard({
}
};
const handleRunOnDateChange = (date: Date) => {
const equivalentDate = date?.getDate();
date?.setUTCHours(0, 0, 0, 0);
date?.setDate(equivalentDate);
setRunOnDate(date);
setLocalSurvey({ ...localSurvey, runOnDate: date ?? null });
};
const handleCloseOnDateChange = (date: Date) => {
const equivalentDate = date?.getDate();
date?.setUTCHours(0, 0, 0, 0);
@@ -143,7 +164,7 @@ export default function ResponseOptionsCard({
subheading?: string;
}) => {
const message = {
enabled: surveyCloseOnDateToggle,
enabled: closeOnDateToggle,
heading: heading ?? surveyClosedMessage.heading,
subheading: subheading ?? surveyClosedMessage.subheading,
};
@@ -245,9 +266,14 @@ export default function ResponseOptionsCard({
setVerifyEmailToggle(true);
}
if (localSurvey.runOnDate) {
setRunOnDate(localSurvey.runOnDate);
setRunOnDateToggle(true);
}
if (localSurvey.closeOnDate) {
setCloseOnDate(localSurvey.closeOnDate);
setSurveyCloseOnDateToggle(true);
setCloseOnDateToggle(true);
}
}, [
localSurvey,
@@ -318,7 +344,7 @@ export default function ResponseOptionsCard({
description="Automatically close the survey after a certain number of responses."
childBorder={true}>
<label htmlFor="autoCompleteResponses" className="cursor-pointer bg-slate-50 p-4">
<p className="text-sm font-semibold text-slate-700">
<p className="text-sm text-slate-700">
Automatically mark the survey as complete after
<Input
autoFocus
@@ -334,18 +360,27 @@ export default function ResponseOptionsCard({
</p>
</label>
</AdvancedOptionToggle>
{/* Run Survey on Date */}
<AdvancedOptionToggle
htmlId="runOnDate"
isChecked={runOnDateToggle}
onToggle={handleRunOnDateToggle}
title="Release survey on date"
description="Automatically release the survey at the beginning of the day (UTC)."
childBorder={true}>
<div className="p-4">
<DatePicker date={runOnDate} handleDateChange={handleRunOnDateChange} />
</div>
</AdvancedOptionToggle>
{/* Close Survey on Date */}
<AdvancedOptionToggle
htmlId="closeOnDate"
isChecked={surveyCloseOnDateToggle}
onToggle={handleSurveyCloseOnDateToggle}
isChecked={closeOnDateToggle}
onToggle={handleCloseOnDateToggle}
title="Close survey on date"
description="Automatically closes the survey at the beginning of the day (UTC)."
childBorder={true}>
<div className="flex cursor-pointer p-4">
<p className="mr-2 mt-3 text-sm font-semibold text-slate-700">
Automatically mark survey as complete on:
</p>
<div className="p-4">
<DatePicker date={closeOnDate} handleDateChange={handleCloseOnDateChange} />
</div>
</AdvancedOptionToggle>
@@ -356,22 +391,17 @@ export default function ResponseOptionsCard({
isChecked={redirectToggle}
onToggle={handleRedirectCheckMark}
title="Redirect on completion"
description="Redirect user to specified link on survey completion"
description="Redirect user to link destination when they completed the survey"
childBorder={true}>
<div className="w-full p-4">
<div className="flex w-full cursor-pointer items-center">
<p className="mr-2 w-[400px] text-sm font-semibold text-slate-700">
Redirect respondents here:
</p>
<Input
autoFocus
className="w-full bg-white"
type="url"
placeholder="https://www.example.com"
value={redirectUrl ? redirectUrl : ""}
onChange={(e) => handleRedirectUrlChange(e.target.value)}
/>
</div>
<Input
autoFocus
className="w-full bg-white"
type="url"
placeholder="https://www.example.com"
value={redirectUrl ? redirectUrl : ""}
onChange={(e) => handleRedirectUrlChange(e.target.value)}
/>
</div>
</AdvancedOptionToggle>
@@ -427,7 +457,15 @@ export default function ResponseOptionsCard({
Blocks survey if the survey URL has no Single Use Id (suId).
</li>
<li className="text-sm text-slate-600">
Blocks survey if a submission with the Single Use Id (suId) in the URL exists already.
Blocks survey if a submission with the Single Use Id (suId) exists already.
</li>
<li className="text-sm text-slate-600">
<Link
href="https://formbricks.com/docs/link-surveys/single-use-links"
target="_blank"
className="underline">
Docs <ArrowUpRight className="inline" size={16} />
</Link>
</li>
</ul>
<Label htmlFor="headline">&lsquo;Link Used&rsquo; Message</Label>
@@ -479,7 +517,7 @@ export default function ResponseOptionsCard({
childBorder={true}>
<div className="flex w-full items-center space-x-1 p-4 pb-4">
<div className="w-full cursor-pointer items-center bg-slate-50">
<p className="text-md font-semibold">How it works</p>
<Label htmlFor="howItWorks">How it works</Label>
<p className="mb-4 mt-2 text-sm text-slate-500">
Respondants will receive the survey link via email.
</p>
@@ -513,25 +551,25 @@ export default function ResponseOptionsCard({
title="Protect survey with a PIN"
description="Only users who have the PIN can access the survey."
childBorder={true}>
<div className="flex w-full items-center space-x-1 p-4 pb-4">
<div className="w-full cursor-pointer items-center bg-slate-50">
<Label htmlFor="headline">Add PIN</Label>
<Input
autoFocus
id="pin"
isInvalid={Boolean(verifyProtectWithPinError)}
className="mb-4 mt-2 bg-white"
name="pin"
placeholder="1234"
onBlur={handleProtectSurveyPinBlurEvent}
defaultValue={localSurvey.pin ? localSurvey.pin : undefined}
onKeyDown={handleSurveyPinInputKeyDown}
onChange={(e) => handleProtectSurveyPinChange(e.target.value)}
/>
{verifyProtectWithPinError && (
<p className="text-sm text-red-700">{verifyProtectWithPinError}</p>
)}
</div>
<div className="p-4">
<Label htmlFor="headline" className="sr-only">
Add PIN:
</Label>
<Input
autoFocus
id="pin"
isInvalid={Boolean(verifyProtectWithPinError)}
className="bg-white"
name="pin"
placeholder="Add a four digit PIN"
onBlur={handleProtectSurveyPinBlurEvent}
defaultValue={localSurvey.pin ? localSurvey.pin : undefined}
onKeyDown={handleSurveyPinInputKeyDown}
onChange={(e) => handleProtectSurveyPinChange(e.target.value)}
/>
{verifyProtectWithPinError && (
<p className="pt-1 text-sm text-red-700">{verifyProtectWithPinError}</p>
)}
</div>
</AdvancedOptionToggle>
</>

View File

@@ -370,7 +370,8 @@ export default function SurveyMenuBar({
setIsSurveyPublishing(false);
return;
}
await updateSurveyAction({ ...localSurvey, status: "inProgress" });
const status = localSurvey.runOnDate ? "scheduled" : "inProgress";
await updateSurveyAction({ ...localSurvey, status });
router.push(`/environments/${environment.id}/surveys/${localSurvey.id}/summary?success=true`);
} catch (error) {
toast.error("An error occured while publishing the survey.");

View File

@@ -2623,6 +2623,7 @@ export const minimalSurvey: TSurvey = {
delay: 0, // No delay
displayPercentage: null,
autoComplete: null,
runOnDate: null,
closeOnDate: null,
surveyClosedMessage: {
enabled: false,

View File

@@ -1,45 +0,0 @@
import { responses } from "@/app/lib/api/response";
import { headers } from "next/headers";
import { prisma } from "@formbricks/database";
import { CRON_SECRET } from "@formbricks/lib/constants";
export async function POST() {
const headersList = headers();
const apiKey = headersList.get("x-api-key");
if (!apiKey || apiKey !== CRON_SECRET) {
return responses.notAuthenticatedResponse();
}
const surveys = await prisma.survey.findMany({
where: {
status: "inProgress",
closeOnDate: {
lte: new Date(),
},
},
select: {
id: true,
},
});
if (!surveys.length) {
return responses.successResponse({ message: "No surveys to close" });
}
const mutationResp = await prisma.survey.updateMany({
where: {
id: {
in: surveys.map((survey) => survey.id),
},
},
data: {
status: "completed",
},
});
return responses.successResponse({
message: `Closed ${mutationResp.count} survey(s)`,
});
}

View File

@@ -0,0 +1,70 @@
import { responses } from "@/app/lib/api/response";
import { headers } from "next/headers";
import { prisma } from "@formbricks/database";
import { CRON_SECRET } from "@formbricks/lib/constants";
export async function POST() {
const headersList = headers();
const apiKey = headersList.get("x-api-key");
if (!apiKey || apiKey !== CRON_SECRET) {
return responses.notAuthenticatedResponse();
}
// close surveys that are in progress and have a closeOnDate in the past
const surveysToClose = await prisma.survey.findMany({
where: {
status: "inProgress",
closeOnDate: {
lte: new Date(),
},
},
select: {
id: true,
},
});
if (surveysToClose.length) {
await prisma.survey.updateMany({
where: {
id: {
in: surveysToClose.map((survey) => survey.id),
},
},
data: {
status: "completed",
},
});
}
// run surveys that are scheduled and have a runOnDate in the past
const scheduledSurveys = await prisma.survey.findMany({
where: {
status: "scheduled",
runOnDate: {
lte: new Date(),
},
},
select: {
id: true,
},
});
if (scheduledSurveys.length) {
await prisma.survey.updateMany({
where: {
id: {
in: scheduledSurveys.map((survey) => survey.id),
},
},
data: {
status: "inProgress",
},
});
}
return responses.successResponse({
message: `Updated ${surveysToClose.length} surveys to completed and ${scheduledSurveys.length} surveys to inProgress.`,
});
}

View File

@@ -11,7 +11,7 @@ const SurveyInactive = ({
status,
surveyClosedMessage,
}: {
status: "paused" | "completed" | "link invalid";
status: "paused" | "completed" | "link invalid" | "scheduled";
surveyClosedMessage?: TSurveyClosedMessage | null;
}) => {
const icons = {

View File

@@ -1,6 +1,6 @@
{
"functions": {
"app/api/cron/weekly_summary/*.ts": {
"app/api/cron/**/*.ts": {
"maxDuration": 30
},
"app/api/v1/client/**/*.ts": {

View File

@@ -1,3 +1,3 @@
0 0 * * * curl $WEBAPP_URL/api/cron/close_surveys -X POST -H 'content-type: application/json' -H 'x-api-key: '"$CRON_SECRET"''
0 8 * * 1 curl $WEBAPP_URL/api/cron/weekly_summary -X POST -H 'content-type: application/json' -H 'x-api-key: '"$CRON_SECRET"''
0 0 * * * curl $WEBAPP_URL/api/cron/survey-status -X POST -H 'content-type: application/json' -H 'x-api-key: '"$CRON_SECRET"''
0 8 * * 1 curl $WEBAPP_URL/api/cron/weekly-summary -X POST -H 'content-type: application/json' -H 'x-api-key: '"$CRON_SECRET"''
0 9 * * * curl $WEBAPP_URL/api/cron/ping -X POST -H 'content-type: application/json' -H 'x-api-key: '"$CRON_SECRET"''

View File

@@ -0,0 +1,5 @@
-- AlterEnum
ALTER TYPE "SurveyStatus" ADD VALUE 'scheduled';
-- AlterTable
ALTER TABLE "Survey" ADD COLUMN "runOnDate" TIMESTAMP(3);

View File

@@ -178,6 +178,7 @@ model TagsOnResponses {
enum SurveyStatus {
draft
scheduled
inProgress
paused
completed
@@ -284,8 +285,9 @@ model Survey {
attributeFilters SurveyAttributeFilter[]
displays Display[]
autoClose Int?
delay Int @default(0)
autoComplete Int?
delay Int @default(0)
runOnDate DateTime?
closeOnDate DateTime?
/// @zod.custom(imports.ZSurveyClosedMessage)
/// [SurveyClosedMessage]

View File

@@ -289,6 +289,7 @@ export const mockSurvey: TSurvey = {
displayOption: "displayOnce",
recontactDays: null,
autoClose: null,
runOnDate: null,
closeOnDate: null,
delay: 0,
displayPercentage: null,

View File

@@ -114,6 +114,7 @@ export const PREVIEW_SURVEY = {
displayOption: "displayOnce",
recontactDays: null,
autoClose: null,
runOnDate: null,
closeOnDate: null,
delay: 0,
displayPercentage: null,

View File

@@ -58,6 +58,7 @@ export const selectSurvey = {
displayOption: true,
recontactDays: true,
autoClose: true,
runOnDate: true,
closeOnDate: true,
delay: true,
displayPercentage: true,
@@ -469,11 +470,25 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> =>
}
surveyData.updatedAt = new Date();
data = {
...surveyData,
...data,
};
// Remove scheduled status when runOnDate is not set
if (data.status === "scheduled" && data.runOnDate === null) {
data.status = "inProgress";
}
// Set scheduled status when runOnDate is set and in the future on completed surveys
if (
(data.status === "completed" || data.status === "paused" || data.status === "inProgress") &&
data.runOnDate &&
data.runOnDate > new Date()
) {
data.status = "scheduled";
}
try {
const prismaSurvey = await prisma.survey.update({
where: { id: surveyId },

View File

@@ -160,6 +160,7 @@ const baseSurveyProperties = {
autoClose: 10,
delay: 0,
autoComplete: 7,
runOnDate: null,
closeOnDate: currentDate,
redirectUrl: "http://github.com/formbricks/formbricks",
recontactDays: 3,

View File

@@ -11,6 +11,9 @@ export const formatSurveyDateFields = (survey: TSurvey): TSurvey => {
if (typeof survey.updatedAt === "string") {
survey.updatedAt = new Date(survey.updatedAt);
}
if (typeof survey.runOnDate === "string") {
survey.runOnDate = new Date(survey.runOnDate);
}
if (typeof survey.closeOnDate === "string") {
survey.closeOnDate = new Date(survey.closeOnDate);
}

View File

@@ -424,7 +424,7 @@ export const ZSurveyType = z.enum(["web", "email", "link", "mobile"]);
export type TSurveyType = z.infer<typeof ZSurveyType>;
const ZSurveyStatus = z.enum(["draft", "inProgress", "paused", "completed"]);
const ZSurveyStatus = z.enum(["draft", "scheduled", "inProgress", "paused", "completed"]);
export type TSurveyStatus = z.infer<typeof ZSurveyStatus>;
@@ -473,6 +473,7 @@ export const ZSurvey = z.object({
hiddenFields: ZSurveyHiddenFields,
delay: z.number(),
autoComplete: z.number().nullable(),
runOnDate: z.date().nullable(),
closeOnDate: z.date().nullable(),
productOverwrites: ZSurveyProductOverwrites.nullable(),
styling: ZSurveyStyling.nullable(),
@@ -506,6 +507,7 @@ export const ZSurveyInput = z
hiddenFields: ZSurveyHiddenFields.optional(),
delay: z.number().optional(),
autoComplete: z.number().nullish(),
runOnDate: z.date().nullish(),
closeOnDate: z.date().nullish(),
styling: ZSurveyStyling.optional(),
surveyClosedMessage: ZSurveyClosedMessage.nullish(),
@@ -542,6 +544,7 @@ export type TSurvey = z.infer<typeof ZSurvey>;
export type TSurveyDates = {
createdAt: TSurvey["createdAt"];
updatedAt: TSurvey["updatedAt"];
runOnDate: TSurvey["runOnDate"];
closeOnDate: TSurvey["closeOnDate"];
};

View File

@@ -16,7 +16,7 @@ export function DatePicker({
date,
handleDateChange,
}: {
date?: Date;
date?: Date | null;
handleDateChange: (date?: Date) => void;
}) {
let formattedDate = date ? new Date(date) : undefined;

View File

@@ -1,6 +1,6 @@
"use client";
import { CheckIcon, PauseIcon, PencilIcon } from "lucide-react";
import { CheckIcon, ClockIcon, PauseIcon, PencilIcon } from "lucide-react";
import { TSurvey } from "@formbricks/types/surveys";
@@ -23,6 +23,11 @@ export function SurveyStatusIndicator({ status, tooltip }: SurveyStatusIndicator
<span className="relative inline-flex h-3 w-3 rounded-full bg-green-500"></span>
</span>
)}
{status === "scheduled" && (
<div className=" rounded-full bg-slate-300 p-1">
<ClockIcon className="h-3 w-3 text-slate-600" />
</div>
)}
{status === "paused" && (
<div className=" rounded-full bg-slate-300 p-1">
<PauseIcon className="h-3 w-3 text-slate-600" />
@@ -49,6 +54,13 @@ export function SurveyStatusIndicator({ status, tooltip }: SurveyStatusIndicator
<span className="relative inline-flex h-3 w-3 rounded-full bg-green-500"></span>
</span>
</>
) : status === "scheduled" ? (
<>
<span className="text-slate-800">Survey scheduled.</span>
<div className=" rounded-full bg-slate-300 p-1">
<ClockIcon className="h-3 w-3 text-slate-600" />
</div>
</>
) : status === "paused" ? (
<>
<span className="text-slate-800">Survey paused.</span>
@@ -78,6 +90,11 @@ export function SurveyStatusIndicator({ status, tooltip }: SurveyStatusIndicator
<span className="relative inline-flex h-3 w-3 rounded-full bg-green-500"></span>
</span>
)}
{status === "scheduled" && (
<div className="rounded-full bg-slate-300 p-1">
<ClockIcon className="h-3 w-3 text-slate-600" />
</div>
)}
{status === "paused" && (
<div className="rounded-full bg-slate-300 p-1">
<PauseIcon className="h-3 w-3 text-slate-600" />

View File

@@ -34,7 +34,8 @@ export default function SurveyCard({
const isSurveyCreationDeletionDisabled = isViewer;
const surveyStatusLabel = useMemo(() => {
if (survey.status === "inProgress") return "Active";
if (survey.status === "inProgress") return "In Progress";
else if (survey.status === "scheduled") return "Scheduled";
else if (survey.status === "completed") return "Completed";
else if (survey.status === "draft") return "Draft";
else if (survey.status === "paused") return "Paused";
@@ -98,7 +99,8 @@ export default function SurveyCard({
<div
className={cn(
"mt-3 flex w-fit items-center gap-2 rounded-full py-1 pl-1 pr-2 text-xs text-slate-800",
surveyStatusLabel === "Active" && "bg-emerald-50",
surveyStatusLabel === "Scheduled" && "bg-slate-200",
surveyStatusLabel === "In Progress" && "bg-emerald-50",
surveyStatusLabel === "Completed" && "bg-slate-200",
surveyStatusLabel === "Draft" && "bg-slate-100",
surveyStatusLabel === "Paused" && "bg-slate-100"
@@ -123,7 +125,8 @@ export default function SurveyCard({
<div
className={cn(
"flex w-fit items-center gap-2 rounded-full py-1 pl-1 pr-2 text-sm text-slate-800",
surveyStatusLabel === "Active" && "bg-emerald-50",
surveyStatusLabel === "Scheduled" && "bg-slate-200",
surveyStatusLabel === "In Progress" && "bg-emerald-50",
surveyStatusLabel === "Completed" && "bg-slate-200",
surveyStatusLabel === "Draft" && "bg-slate-100",
surveyStatusLabel === "Paused" && "bg-slate-100"

View File

@@ -35,6 +35,7 @@ interface FilterDropdownProps {
const statusOptions = [
{ label: "In Progress", value: "inProgress" },
{ label: "Scheduled", value: "scheduled" },
{ label: "Paused", value: "paused" },
{ label: "Completed", value: "completed" },
{ label: "Draft", value: "draft" },