Skip to content

Commit 1bbd0ff

Browse files
committed
Add company details wizard with multi-step form and database integration
1 parent d5e6151 commit 1bbd0ff

21 files changed

Lines changed: 1164 additions & 129 deletions

File tree

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

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

220+
await auth.api.setActiveOrganization({
221+
headers: await headers(),
222+
body: {
223+
organizationId,
224+
},
225+
});
226+
220227
return {
221228
success: true,
222-
organizationId: session.session.activeOrganizationId,
229+
organizationId,
223230
};
224231
} catch (error) {
225232
console.error("Error during organization creation/update:", error);

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

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

66
export function Checklist({ items }: ChecklistProps) {
7-
return (
8-
<div className="flex flex-col gap-4">
9-
{items.map((item) => (
10-
<ChecklistItem key={item.dbColumn as string} {...item} />
11-
))}
12-
</div>
13-
);
14-
}
7+
return (
8+
<div className="flex flex-col gap-4">
9+
{items.map((item) => (
10+
<ChecklistItem
11+
key={
12+
item.dbColumn
13+
? item.dbColumn
14+
: item.title.replace(/\s+/g, "-").toLowerCase()
15+
}
16+
{...item}
17+
/>
18+
))}
19+
</div>
20+
);
21+
}

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

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
11
"use client";
22

3-
import { Badge } from "@comp/ui/badge";
43
import { Button } from "@comp/ui/button";
54
import {
65
Card,
7-
CardContent,
86
CardDescription,
97
CardFooter,
108
CardHeader,
119
CardTitle,
1210
} from "@comp/ui/card";
11+
import { Separator } from "@comp/ui/separator";
1312
import { ArrowRight, CheckCheck, Circle, Loader2 } from "lucide-react";
1413
import { useParams, useRouter } from "next/navigation";
1514
import { useState } from "react";
1615
import { toast } from "sonner";
1716
import { updateOnboardingItem } from "../actions/update-onboarding-item";
1817
import type { ChecklistItemProps } from "../types/ChecklistProps.types";
19-
import { Separator } from "@comp/ui/separator";
20-
import { cn } from "../../../../../../../../../../packages/ui/src/utils";
18+
import Link from "next/link";
2119

2220
export function ChecklistItem({
2321
title,
@@ -28,14 +26,19 @@ export function ChecklistItem({
2826
completed,
2927
buttonLabel,
3028
icon,
29+
type,
30+
wizardPath,
3131
}: ChecklistItemProps) {
3232
const { orgId } = useParams<{ orgId: string }>();
33-
const linkWithOrgReplaced = href.replace(":organizationId", orgId);
33+
const linkWithOrgReplaced = href
34+
? href.replace(":organizationId", orgId)
35+
: undefined;
3436
const [isUpdating, setIsUpdating] = useState(false);
3537
const [isAnimating, setIsAnimating] = useState(false);
3638
const router = useRouter();
3739

3840
const handleMarkAsDone = async () => {
41+
if (!dbColumn) return;
3942
try {
4043
setIsUpdating(true);
4144
setIsAnimating(true);
@@ -47,7 +50,7 @@ export function ChecklistItem({
4750
}
4851

4952
setTimeout(() => setIsAnimating(false), 600);
50-
router.push(linkWithOrgReplaced);
53+
router.push(linkWithOrgReplaced ?? "/");
5154
} catch (error) {
5255
toast.error(
5356
error instanceof Error
@@ -59,9 +62,15 @@ export function ChecklistItem({
5962
}
6063
};
6164

65+
const handleWizardRedirect = () => {
66+
if (wizardPath) {
67+
router.push(wizardPath.replace(":organizationId", orgId));
68+
}
69+
};
70+
6271
return (
6372
<Card>
64-
<div className={completed ? "opacity-40" : ""}>
73+
<div>
6574
<CardHeader>
6675
<CardTitle className="flex items-center gap-2 w-full">
6776
{completed && (
@@ -71,6 +80,21 @@ export function ChecklistItem({
7180
<span className={completed ? "line-through" : ""}>
7281
{title}
7382
</span>
83+
{completed && (
84+
<Button
85+
className="ml-auto"
86+
variant="outline"
87+
onClick={() => {
88+
if (type === "wizard") {
89+
handleWizardRedirect();
90+
} else {
91+
handleMarkAsDone();
92+
}
93+
}}
94+
>
95+
View again <ArrowRight className="h-4 w-4" />
96+
</Button>
97+
)}
7498
{/* {completed && (
7599
<Badge variant="marketing">Completed</Badge>
76100
)} */}
@@ -84,12 +108,22 @@ export function ChecklistItem({
84108
{!completed && <Separator className="my-6" />}
85109
{!completed && (
86110
<CardFooter className="justify-end">
87-
{!completed && (
111+
{type === "wizard" ? (
88112
<Button
89113
variant={"secondary"}
90114
className="w-full sm:w-fit"
91-
onClick={handleMarkAsDone}
115+
onClick={handleWizardRedirect}
92116
disabled={isUpdating}
117+
>
118+
{buttonLabel}
119+
<ArrowRight className="ml-1 h-4 w-4" />
120+
</Button>
121+
) : (
122+
<Button
123+
variant={"secondary"}
124+
className="w-full sm:w-fit"
125+
onClick={handleMarkAsDone}
126+
disabled={isUpdating || !dbColumn}
93127
>
94128
{completed ? (
95129
"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: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { db } from "@comp/db";
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 { redirect } from "next/navigation";
55
import { cache } from "react";
66
import { Checklist } from "./components/Checklist";
77
import { OnboardingProgress } from "./components/OnboardingProgress";
88
import { ChecklistItemProps } from "./types/ChecklistProps.types";
9+
import { companyDetailsObjectSchema } from "./lib/models/CompanyDetails";
10+
import { z } from "zod";
911

1012
const getChecklistItems = cache(
1113
async (
@@ -29,6 +31,22 @@ const getChecklistItems = cache(
2931
}
3032

3133
const checklistItems: ChecklistItemProps[] = [
34+
{
35+
title: "Fill out company details",
36+
description:
37+
"In order to get started, you need to provide some basic details about how your company operates.",
38+
wizardPath: `/${orgId}/implementation/wizard/company-details`,
39+
completed:
40+
(
41+
onboarding.companyDetails as z.infer<
42+
typeof companyDetailsObjectSchema
43+
>
44+
)?.isCompleted || false,
45+
docs: "https://trycomp.ai/docs/details",
46+
buttonLabel: "Fill out details",
47+
icon: <Briefcase className="h-5 w-5" />,
48+
type: "wizard",
49+
},
3250
{
3351
title: "Check & Publish Policies",
3452
description:
@@ -87,7 +105,7 @@ const getChecklistItems = cache(
87105
];
88106

89107
const completedItems = checklistItems.filter(
90-
(item) => item.completed,
108+
(item) => item.completed || item.wizardCompleted,
91109
).length;
92110
const totalItems = checklistItems.length;
93111

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ export interface ChecklistProps {
77
export interface ChecklistItemProps {
88
title: string;
99
description?: string;
10-
href: string;
10+
href?: string;
1111
docs: string;
12-
dbColumn: Exclude<keyof Onboarding, "organizationId">;
13-
completed: boolean;
12+
dbColumn?: Exclude<keyof Onboarding, "organizationId">;
13+
completed?: boolean;
1414
buttonLabel: string;
1515
icon: React.ReactNode;
16+
type?: "default" | "wizard";
17+
wizardPath?: string;
18+
// For wizards, allow specifying a completion boolean directly
19+
wizardCompleted?: boolean;
1620
}

0 commit comments

Comments
 (0)