Skip to content

Commit ec8c49a

Browse files
authored
Merge pull request #473 from trycompai/mariano/questionaire
Add company details wizard with multi-step form and database integration
2 parents e040851 + e7e0bd3 commit ec8c49a

File tree

22 files changed

+1184
-110
lines changed

22 files changed

+1184
-110
lines changed

apps/app/src/actions/organization/create-organization-action.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,9 +232,16 @@ export const createOrganizationAction = authActionClient
232232
revalidatePath(`/${org.organizationId}`);
233233
}
234234

235+
await auth.api.setActiveOrganization({
236+
headers: await headers(),
237+
body: {
238+
organizationId,
239+
},
240+
});
241+
235242
return {
236243
success: true,
237-
organizationId: session.session.activeOrganizationId,
244+
organizationId,
238245
};
239246
} catch (error) {
240247
console.error("Error during organization creation/update:", error);

apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/implementation/actions.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ export async function getOnboardingStatus(
4949
}
5050

5151
const checklistItems = generateChecklistItems(onboarding, orgId);
52-
const completedItems = checklistItems.filter((item) => item.completed).length;
52+
const completedItems = checklistItems.filter(
53+
(item) => item.completed,
54+
).length;
5355
const totalItems = checklistItems.length;
5456

5557
return { checklistItems, completedItems, totalItems };
@@ -95,4 +97,4 @@ export async function markOnboardingStep({
9597
// Return the error key directly
9698
return { success: false, error: appErrors.UNEXPECTED_ERROR };
9799
}
98-
}
100+
}

apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/implementation/checklist-items.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,31 @@
11
import type { Onboarding } from "@comp/db/types";
22
import { Icons } from "@comp/ui/icons";
3-
import { ListCheck, NotebookText, Store, Users } from "lucide-react";
3+
import { Briefcase, ListCheck, NotebookText, Store, Users } from "lucide-react";
44
import type { ChecklistItemProps } from "./types";
5+
import { companyDetailsObjectSchema } from "./lib/models/CompanyDetails";
6+
import { z } from "zod";
57

68
export function generateChecklistItems(
79
onboarding: Onboarding,
810
orgId: string,
911
): ChecklistItemProps[] {
1012
return [
13+
{
14+
title: "Fill out company details",
15+
description:
16+
"In order to get started, you need to provide some basic details about how your company operates.",
17+
wizardPath: `/${orgId}/implementation/wizard/company-details`,
18+
completed:
19+
(
20+
onboarding.companyDetails as z.infer<
21+
typeof companyDetailsObjectSchema
22+
>
23+
)?.isCompleted || false,
24+
docs: "https://trycomp.ai/docs/details",
25+
buttonLabel: "Fill out details",
26+
icon: <Briefcase className="h-5 w-5" />,
27+
type: "wizard",
28+
},
1129
{
1230
title: "Check & Publish Policies",
1331
description:
@@ -64,4 +82,4 @@ export function generateChecklistItems(
6482
icon: <ListCheck className="h-5 w-5" />,
6583
},
6684
];
67-
}
85+
}

apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/implementation/components/Checklist.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { ChecklistItemProps } from "../types";
44
import { ChecklistItem } from "./ChecklistItem";
55

66
export function Checklist({ items }: { items: ChecklistItemProps[] }) {
7-
return (
8-
<div className="flex flex-col gap-4">
9-
{items.map((item) => (
10-
<ChecklistItem key={item.dbColumn} {...item} />
11-
))}
12-
</div>
13-
);
14-
}
7+
return (
8+
<div className="flex flex-col gap-4">
9+
{items.map((item) => (
10+
<ChecklistItem key={item.dbColumn} {...item} />
11+
))}
12+
</div>
13+
);
14+
}

apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/implementation/components/ChecklistItem.tsx

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
CardDescription,
77
CardFooter,
88
CardHeader,
9-
CardTitle
9+
CardTitle,
1010
} from "@comp/ui/card";
1111
import { Separator } from "@comp/ui/separator";
1212
import { ArrowRight, CheckCheck, Circle, Loader2 } from "lucide-react";
@@ -25,14 +25,19 @@ export function ChecklistItem({
2525
completed,
2626
buttonLabel,
2727
icon,
28+
type,
29+
wizardPath,
2830
}: ChecklistItemProps) {
2931
const { orgId } = useParams<{ orgId: string }>();
30-
const linkWithOrgReplaced = href.replace(":organizationId", orgId);
32+
const linkWithOrgReplaced = href
33+
? href.replace(":organizationId", orgId)
34+
: undefined;
3135
const [isUpdating, setIsUpdating] = useState(false);
3236
const [isAnimating, setIsAnimating] = useState(false);
3337
const router = useRouter();
3438

3539
const handleMarkAsDone = async () => {
40+
if (!dbColumn) return;
3641
try {
3742
setIsUpdating(true);
3843
setIsAnimating(true);
@@ -44,7 +49,7 @@ export function ChecklistItem({
4449
}
4550

4651
setTimeout(() => setIsAnimating(false), 600);
47-
router.push(linkWithOrgReplaced);
52+
router.push(linkWithOrgReplaced ?? "/");
4853
} catch (error) {
4954
toast.error(
5055
error instanceof Error
@@ -56,9 +61,15 @@ export function ChecklistItem({
5661
}
5762
};
5863

64+
const handleWizardRedirect = () => {
65+
if (wizardPath) {
66+
router.push(wizardPath.replace(":organizationId", orgId));
67+
}
68+
};
69+
5970
return (
6071
<Card>
61-
<div className={completed ? "opacity-40" : ""}>
72+
<div>
6273
<CardHeader>
6374
<CardTitle className="flex items-center gap-2 w-full">
6475
{completed && (
@@ -68,6 +79,21 @@ export function ChecklistItem({
6879
<span className={completed ? "line-through" : ""}>
6980
{title}
7081
</span>
82+
{completed && (
83+
<Button
84+
className="ml-auto"
85+
variant="outline"
86+
onClick={() => {
87+
if (type === "wizard") {
88+
handleWizardRedirect();
89+
} else {
90+
handleMarkAsDone();
91+
}
92+
}}
93+
>
94+
View again <ArrowRight className="h-4 w-4" />
95+
</Button>
96+
)}
7197
{/* {completed && (
7298
<Badge variant="marketing">Completed</Badge>
7399
)} */}
@@ -81,12 +107,22 @@ export function ChecklistItem({
81107
{!completed && <Separator className="my-6" />}
82108
{!completed && (
83109
<CardFooter className="justify-end">
84-
{!completed && (
110+
{type === "wizard" ? (
85111
<Button
86112
variant={"secondary"}
87113
className="w-full sm:w-fit"
88-
onClick={handleMarkAsDone}
114+
onClick={handleWizardRedirect}
89115
disabled={isUpdating}
116+
>
117+
{buttonLabel}
118+
<ArrowRight className="ml-1 h-4 w-4" />
119+
</Button>
120+
) : (
121+
<Button
122+
variant={"secondary"}
123+
className="w-full sm:w-fit"
124+
onClick={handleMarkAsDone}
125+
disabled={isUpdating || !dbColumn}
90126
>
91127
{completed ? (
92128
"Completed"
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { z } from "zod";
2+
3+
const companyDetailsSchemaV1 = z.object({
4+
companyName: z.string().default(""),
5+
companyWebsite: z.string().default(""),
6+
vendors: z.array(z.string()).default([]),
7+
headcount: z.number().default(1),
8+
workStyle: z.string().default(""),
9+
dataCategories: z.array(z.string()).default([]),
10+
storageRegions: z.string().default(""),
11+
identityProviders: z.string().default(""),
12+
laptopOS: z.array(z.string()).default([]),
13+
mobileDevice: z.boolean().default(false),
14+
});
15+
16+
enum Versions {
17+
V1 = "v1",
18+
}
19+
20+
interface CompanyDetailsSchemas {
21+
[Versions.V1]: typeof companyDetailsSchemaV1;
22+
}
23+
24+
const versionToSchema: CompanyDetailsSchemas = {
25+
[Versions.V1]: companyDetailsSchemaV1,
26+
};
27+
28+
type CompanyDetailsData = {
29+
[K in Versions]: z.infer<CompanyDetailsSchemas[K]>;
30+
};
31+
32+
type CompanyDetailsConstructorArgs<V extends Versions> = {
33+
version: V;
34+
isCompleted: boolean;
35+
data: CompanyDetailsData[V];
36+
};
37+
38+
export const companyDetailsObjectSchema = z.object({
39+
version: z.nativeEnum(Versions),
40+
isCompleted: z.boolean(),
41+
data: companyDetailsSchemaV1,
42+
});
43+
44+
export const companyDetailslatestVersionSchema = companyDetailsSchemaV1;
45+
export const companyDetailsLatestVersion = Versions.V1;
46+
47+
export class CompanyDetails {
48+
readonly version: Versions;
49+
readonly isCompleted: boolean;
50+
readonly data: CompanyDetailsData[Versions];
51+
52+
constructor(input: unknown) {
53+
// If for some reason the input is undefined or null, create an empty latest version.
54+
if (input === undefined || input === null) {
55+
const latest = CompanyDetails.createEmptyLatest();
56+
this.version = latest.version;
57+
this.isCompleted = latest.isCompleted;
58+
this.data = latest.data;
59+
return;
60+
}
61+
const parsed = CompanyDetails.validateAndUpgrade(input);
62+
this.version = parsed.version;
63+
this.isCompleted = parsed.isCompleted;
64+
const schema = versionToSchema[parsed.version];
65+
this.data = schema.parse(parsed.data);
66+
}
67+
68+
private static createEmptyLatest(): CompanyDetailsConstructorArgs<Versions> {
69+
const latestVersion: Versions = companyDetailsLatestVersion;
70+
const schema = versionToSchema[latestVersion];
71+
const emptyData = schema.parse({});
72+
73+
return {
74+
version: latestVersion,
75+
isCompleted: false,
76+
data: emptyData,
77+
};
78+
}
79+
80+
private static validateAndUpgrade(
81+
input: unknown,
82+
): CompanyDetailsConstructorArgs<Versions> {
83+
try {
84+
const parsed = z
85+
.object({
86+
version: z.nativeEnum(Versions),
87+
isCompleted: z.boolean(),
88+
data: companyDetailsSchemaV1,
89+
})
90+
.parse(input);
91+
return CompanyDetails.upgradeToLatestCompanyDetails(parsed);
92+
} catch (err) {
93+
console.error("Failed to validate CompanyDetails:", err);
94+
throw err;
95+
}
96+
}
97+
98+
private static upgradeToLatestCompanyDetails<V extends Versions>(input: {
99+
version: V;
100+
data: CompanyDetailsData[V];
101+
isCompleted: boolean;
102+
}): CompanyDetailsConstructorArgs<Versions> {
103+
return input as CompanyDetailsConstructorArgs<Versions>;
104+
}
105+
}

apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/implementation/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { redirect } from "next/navigation";
21
import { Checklist } from "./components/Checklist";
32
import { OnboardingProgress } from "./components/OnboardingProgress";
43
import { getOnboardingStatus } from "./actions";
@@ -12,7 +11,9 @@ export default async function Page({
1211
const checklistData = await getOnboardingStatus(orgId);
1312

1413
if ("error" in checklistData) {
15-
return <div>Error loading onboarding status: {checklistData.error}</div>;
14+
return (
15+
<div>Error loading onboarding status: {checklistData.error}</div>
16+
);
1617
}
1718

1819
return (
Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import type { Onboarding } from "@comp/db/types";
1+
import { Onboarding } from "@comp/db/types";
22

3-
// Define valid onboarding steps based on the Onboarding model keys
43
export const onboardingSteps = [
54
"policies",
65
"employees",
@@ -13,11 +12,15 @@ export type OnboardingStep = (typeof onboardingSteps)[number];
1312

1413
export interface ChecklistItemProps {
1514
title: string;
16-
description: string;
17-
href: string;
18-
dbColumn: OnboardingStep; // Use the derived literal type
19-
completed: boolean;
15+
description?: string;
16+
href?: string;
2017
docs: string;
18+
dbColumn?: Exclude<keyof Onboarding, "organizationId">;
19+
completed?: boolean;
2120
buttonLabel: string;
2221
icon: React.ReactNode;
23-
}
22+
type?: "default" | "wizard";
23+
wizardPath?: string;
24+
// For wizards, allow specifying a completion boolean directly
25+
wizardCompleted?: boolean;
26+
}

0 commit comments

Comments
 (0)