Repository: trycompai/comp
Pull request #307: feat: implement comments feature
Author: login: carhartlewis
Created: 2025-04-06T01:36:13Z | Merged: 2025-04-06T02:37:58Z

- Added `addCommentAction` for server-side comment creation with validation using Zod.
- Introduced `Comments` component for displaying and adding comments in various pages.
- Updated relevant pages (Policy, Risk, Vendor) to include comments functionality.
- Created `CreateCommentForm` for user input and submission of comments.
- Added database schema for `Comment` and updated related models in Prisma.
- Ensured comments are fetched and displayed correctly in the UI.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
	- Integrated a robust commenting system across policy, risk, and vendor pages, enabling users to easily add and view comments.
	- Introduced an intuitive comment submission form and dynamic display interface to enhance user interaction.
	- Added validation and error handling for comment submissions to improve user experience.
	- Enhanced comment display with author details and timestamps for better context.
	- Streamlined data fetching for comments, improving performance and responsiveness.

- **Bug Fixes**
	- Resolved issues related to unauthorized access during comment submission.

- **Chores**
	- Updated internal schemas and database structures to support the new commenting features.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/apps/app/src/actions/add-comment.ts b/apps/app/src/actions/add-comment.ts
new file mode 100644
index 0000000000..86202e5c9d
--- /dev/null
+++ b/apps/app/src/actions/add-comment.ts
@@ -0,0 +1,75 @@
+"use server";
+
+import { authActionClient } from "./safe-action";
+import { z } from "zod";
+import { db } from "@comp/db";
+import { AppError, appErrors } from "@/lib/errors";
+import { headers } from "next/headers";
+import { revalidatePath } from "next/cache";
+
+export const addCommentAction = authActionClient
+	.schema(
+		z.object({
+			content: z.string(),
+			entityId: z.string(),
+		}),
+	)
+	.metadata({
+		name: "add-comment",
+		track: {
+			event: "add-comment",
+			channel: "server",
+		},
+	})
+	.action(async ({ parsedInput, ctx }) => {
+		const { content, entityId } = parsedInput;
+		const { user, session } = ctx;
+
+		if (!session || !session.activeOrganizationId) {
+			return {
+				success: false,
+				error: appErrors.UNAUTHORIZED,
+			};
+		}
+
+		try {
+			const member = await db.member.findFirst({
+				where: {
+					userId: session.userId,
+					organizationId: session.activeOrganizationId,
+				},
+			});
+
+			if (!member) {
+				return {
+					success: false,
+					error: appErrors.UNAUTHORIZED,
+				};
+			}
+
+			const comment = await db.comment.create({
+				data: {
+					content,
+					entityId,
+					organizationId: session.activeOrganizationId,
+					authorId: member.id,
+				},
+			});
+
+			const headersList = await headers();
+			let path =
+				headersList.get("x-pathname") || headersList.get("referer") || "";
+			path = path.replace(/\/[a-z]{2}\//, "/");
+
+			if (path) {
+				revalidatePath(path);
+			}
+
+			return { success: true, data: comment };
+		} catch (error) {
+			return {
+				success: false,
+				error: error instanceof AppError ? error : appErrors.UNEXPECTED_ERROR,
+			};
+		}
+	});
diff --git a/apps/app/src/actions/schema.ts b/apps/app/src/actions/schema.ts
index 44748a37da..65a7a3278a 100644
--- a/apps/app/src/actions/schema.ts
+++ b/apps/app/src/actions/schema.ts
@@ -1,284 +1,296 @@
 import {
-  Departments,
-  FrameworkId,
-  Frequency,
-  Impact,
-  Likelihood,
-  PolicyStatus,
-  RiskCategory,
-  RiskStatus,
-  TaskStatus,
+	Departments,
+	FrameworkId,
+	Frequency,
+	Impact,
+	Likelihood,
+	PolicyStatus,
+	RiskCategory,
+	RiskStatus,
+	TaskStatus,
 } from "@comp/db/types";
 import { z } from "zod";
 
 export const organizationSchema = z.object({
-  name: z.string().min(1, "Organization name is required"),
-  frameworks: z
-    .array(z.nativeEnum(FrameworkId))
-    .min(1, "Please select at least one framework to get started with"),
+	name: z.string().min(1, "Organization name is required"),
+	frameworks: z
+		.array(z.nativeEnum(FrameworkId))
+		.min(1, "Please select at least one framework to get started with"),
 });
 
 export type OrganizationSchema = z.infer<typeof organizationSchema>;
 
 export const organizationNameSchema = z.object({
-  name: z
-    .string()
-    .min(1, "Organization name is required")
-    .max(255, "Organization name cannot exceed 255 characters"),
+	name: z
+		.string()
+		.min(1, "Organization name is required")
+		.max(255, "Organization name cannot exceed 255 characters"),
 });
 
 export const subdomainAvailabilitySchema = z.object({
-  subdomain: z
-    .string()
-    .min(1, "Subdomain is required")
-    .max(255, "Subdomain cannot exceed 255 characters")
-    .regex(/^[a-z0-9-]+$/, {
-      message:
-        "Subdomain can only contain lowercase letters, numbers, and hyphens",
-    }),
+	subdomain: z
+		.string()
+		.min(1, "Subdomain is required")
+		.max(255, "Subdomain cannot exceed 255 characters")
+		.regex(/^[a-z0-9-]+$/, {
+			message:
+				"Subdomain can only contain lowercase letters, numbers, and hyphens",
+		}),
 });
 
 export const uploadSchema = z.object({
-  file: z.instanceof(File),
-  organizationId: z.string(),
+	file: z.instanceof(File),
+	organizationId: z.string(),
 });
 
 export const deleteOrganizationSchema = z.object({
-  id: z.string(),
-  organizationId: z.string(),
+	id: z.string(),
+	organizationId: z.string(),
 });
 
 export const sendFeedbackSchema = z.object({
-  feedback: z.string(),
+	feedback: z.string(),
 });
 
 export const updaterMenuSchema = z.array(
-  z.object({
-    path: z.string(),
-    name: z.string(),
-  })
+	z.object({
+		path: z.string(),
+		name: z.string(),
+	}),
 );
 
 export const organizationWebsiteSchema = z.object({
-  website: z.string().url().max(255),
+	website: z.string().url().max(255),
 });
 
 // Risks
 export const createRiskSchema = z.object({
-  title: z
-    .string({
-      required_error: "Risk name is required",
-    })
-    .min(1, {
-      message: "Risk name should be at least 1 character",
-    })
-    .max(100, {
-      message: "Risk name should be at most 100 characters",
-    }),
-  description: z
-    .string({
-      required_error: "Risk description is required",
-    })
-    .min(1, {
-      message: "Risk description should be at least 1 character",
-    })
-    .max(255, {
-      message: "Risk description should be at most 255 characters",
-    }),
-  category: z.nativeEnum(RiskCategory, {
-    required_error: "Risk category is required",
-  }),
-  department: z.nativeEnum(Departments, {
-    required_error: "Risk department is required",
-  }),
-  assigneeId: z.string().optional().nullable(),
+	title: z
+		.string({
+			required_error: "Risk name is required",
+		})
+		.min(1, {
+			message: "Risk name should be at least 1 character",
+		})
+		.max(100, {
+			message: "Risk name should be at most 100 characters",
+		}),
+	description: z
+		.string({
+			required_error: "Risk description is required",
+		})
+		.min(1, {
+			message: "Risk description should be at least 1 character",
+		})
+		.max(255, {
+			message: "Risk description should be at most 255 characters",
+		}),
+	category: z.nativeEnum(RiskCategory, {
+		required_error: "Risk category is required",
+	}),
+	department: z.nativeEnum(Departments, {
+		required_error: "Risk department is required",
+	}),
+	assigneeId: z.string().optional().nullable(),
 });
 
 export const updateRiskSchema = z.object({
-  id: z.string().min(1, {
-    message: "Risk ID is required",
-  }),
-  title: z.string().min(1, {
-    message: "Risk title is required",
-  }),
-  description: z.string().min(1, {
-    message: "Risk description is required",
-  }),
-  category: z.nativeEnum(RiskCategory, {
-    required_error: "Risk category is required",
-  }),
-  department: z.nativeEnum(Departments, {
-    required_error: "Risk department is required",
-  }),
-  assigneeId: z.string().optional().nullable(),
-  status: z.nativeEnum(RiskStatus, {
-    required_error: "Risk status is required",
-  }),
+	id: z.string().min(1, {
+		message: "Risk ID is required",
+	}),
+	title: z.string().min(1, {
+		message: "Risk title is required",
+	}),
+	description: z.string().min(1, {
+		message: "Risk description is required",
+	}),
+	category: z.nativeEnum(RiskCategory, {
+		required_error: "Risk category is required",
+	}),
+	department: z.nativeEnum(Departments, {
+		required_error: "Risk department is required",
+	}),
+	assigneeId: z.string().optional().nullable(),
+	status: z.nativeEnum(RiskStatus, {
+		required_error: "Risk status is required",
+	}),
 });
 
 export const createRiskCommentSchema = z.object({
-  riskId: z.string().min(1, {
-    message: "Risk ID is required",
-  }),
-  content: z
-    .string()
-    .min(1, {
-      message: "Comment content is required",
-    })
-    .max(1000, {
-      message: "Comment content should be at most 1000 characters",
-    }),
+	riskId: z.string().min(1, {
+		message: "Risk ID is required",
+	}),
+	content: z
+		.string()
+		.min(1, {
+			message: "Comment content is required",
+		})
+		.max(1000, {
+			message: "Comment content should be at most 1000 characters",
+		}),
 });
 
 export const createTaskSchema = z.object({
-  riskId: z.string().min(1, {
-    message: "Risk ID is required",
-  }),
-  title: z.string().min(1, {
-    message: "Task title is required",
-  }),
-  description: z.string().min(1, {
-    message: "Task description is required",
-  }),
-  dueDate: z.date().optional(),
-  assigneeId: z.string().optional().nullable(),
+	riskId: z.string().min(1, {
+		message: "Risk ID is required",
+	}),
+	title: z.string().min(1, {
+		message: "Task title is required",
+	}),
+	description: z.string().min(1, {
+		message: "Task description is required",
+	}),
+	dueDate: z.date().optional(),
+	assigneeId: z.string().optional().nullable(),
 });
 
 export const updateTaskSchema = z.object({
-  id: z.string().min(1, {
-    message: "Task ID is required",
-  }),
-  title: z.string().optional(),
-  description: z.string().optional(),
-  dueDate: z.date().optional(),
-  status: z.nativeEnum(TaskStatus, {
-    required_error: "Task status is required",
-  }),
-  assigneeId: z.string().optional().nullable(),
+	id: z.string().min(1, {
+		message: "Task ID is required",
+	}),
+	title: z.string().optional(),
+	description: z.string().optional(),
+	dueDate: z.date().optional(),
+	status: z.nativeEnum(TaskStatus, {
+		required_error: "Task status is required",
+	}),
+	assigneeId: z.string().optional().nullable(),
 });
 
 export const createTaskCommentSchema = z.object({
-  riskId: z.string().min(1, {
-    message: "Risk ID is required",
-  }),
-  taskId: z.string().min(1, {
-    message: "Task ID is required",
-  }),
-  content: z
-    .string()
-    .min(1, {
-      message: "Comment content is required",
-    })
-    .max(1000, {
-      message: "Comment content should be at most 1000 characters",
-    }),
+	riskId: z.string().min(1, {
+		message: "Risk ID is required",
+	}),
+	taskId: z.string().min(1, {
+		message: "Task ID is required",
+	}),
+	content: z
+		.string()
+		.min(1, {
+			message: "Comment content is required",
+		})
+		.max(1000, {
+			message: "Comment content should be at most 1000 characters",
+		}),
 });
 
 export const uploadTaskFileSchema = z.object({
-  riskId: z.string().min(1, {
-    message: "Risk ID is required",
-  }),
-  taskId: z.string().min(1, {
-    message: "Task ID is required",
-  }),
+	riskId: z.string().min(1, {
+		message: "Risk ID is required",
+	}),
+	taskId: z.string().min(1, {
+		message: "Task ID is required",
+	}),
 });
 
 // Integrations
 export const deleteIntegrationConnectionSchema = z.object({
-  integrationId: z.string().min(1, {
-    message: "Integration ID is required",
-  }),
+	integrationId: z.string().min(1, {
+		message: "Integration ID is required",
+	}),
 });
 
 export const createIntegrationSchema = z.object({
-  integrationId: z.string().min(1, {
-    message: "Integration ID is required",
-  }),
+	integrationId: z.string().min(1, {
+		message: "Integration ID is required",
+	}),
 });
 
 // Seed Data
 export const seedDataSchema = z.object({
-  organizationId: z.string(),
+	organizationId: z.string(),
 });
 
 export const updateInherentRiskSchema = z.object({
-  id: z.string().min(1, {
-    message: "Risk ID is required",
-  }),
-  probability: z.nativeEnum(Likelihood),
-  impact: z.nativeEnum(Impact),
+	id: z.string().min(1, {
+		message: "Risk ID is required",
+	}),
+	probability: z.nativeEnum(Likelihood),
+	impact: z.nativeEnum(Impact),
 });
 
 export const updateResidualRiskSchema = z.object({
-  id: z.string().min(1, {
-    message: "Risk ID is required",
-  }),
-  probability: z.number().min(1).max(10),
-  impact: z.number().min(1).max(10),
+	id: z.string().min(1, {
+		message: "Risk ID is required",
+	}),
+	probability: z.number().min(1).max(10),
+	impact: z.number().min(1).max(10),
 });
 
 // Policies
 export const createPolicySchema = z.object({
-  title: z.string(),
-  description: z.string(),
-  frameworkIds: z.array(z.string()),
-  controlIds: z.array(z.string()),
+	title: z.string(),
+	description: z.string(),
+	frameworkIds: z.array(z.string()),
+	controlIds: z.array(z.string()),
 });
 
 export const updatePolicySchema = z.object({
-  id: z.string(),
-  content: z.any(),
+	id: z.string(),
+	content: z.any(),
 });
 
 export const assistantSettingsSchema = z.object({
-  enabled: z.boolean().optional(),
+	enabled: z.boolean().optional(),
 });
 
 export const createEmployeeSchema = z.object({
-  name: z.string().min(1, "Name is required"),
-  email: z.string().email("Invalid email address"),
-  department: z.nativeEnum(Departments, {
-    required_error: "Department is required",
-  }),
-  externalEmployeeId: z.string().optional(),
-  isActive: z.boolean().default(true),
+	name: z.string().min(1, "Name is required"),
+	email: z.string().email("Invalid email address"),
+	department: z.nativeEnum(Departments, {
+		required_error: "Department is required",
+	}),
+	externalEmployeeId: z.string().optional(),
+	isActive: z.boolean().default(true),
 });
 
 export const updatePolicyOverviewSchema = z.object({
-  id: z.string(),
-  title: z.string(),
-  description: z.string(),
-  isRequiredToSign: z.enum(["required", "not_required"]).optional(),
+	id: z.string(),
+	title: z.string(),
+	description: z.string(),
+	isRequiredToSign: z.enum(["required", "not_required"]).optional(),
 });
 
 export const updatePolicyFormSchema = z.object({
-  id: z.string(),
-  status: z.nativeEnum(PolicyStatus),
-  assigneeId: z.string().optional().nullable(),
-  department: z.nativeEnum(Departments),
-  review_frequency: z.nativeEnum(Frequency),
-  review_date: z.date(),
-  isRequiredToSign: z.enum(["required", "not_required"]),
+	id: z.string(),
+	status: z.nativeEnum(PolicyStatus),
+	assigneeId: z.string().optional().nullable(),
+	department: z.nativeEnum(Departments),
+	review_frequency: z.nativeEnum(Frequency),
+	review_date: z.date(),
+	isRequiredToSign: z.enum(["required", "not_required"]),
 });
 
 export const apiKeySchema = z.object({
-  name: z
-    .string()
-    .min(1, { message: "Name is required" })
-    .max(64, { message: "Name must be less than 64 characters" }),
-  expiresAt: z.enum(["30days", "90days", "1year", "never"]),
+	name: z
+		.string()
+		.min(1, { message: "Name is required" })
+		.max(64, { message: "Name must be less than 64 characters" }),
+	expiresAt: z.enum(["30days", "90days", "1year", "never"]),
 });
 
 export const createPolicyCommentSchema = z.object({
-  policyId: z.string().min(1, {
-    message: "Policy ID is required",
-  }),
-  content: z
-    .string()
-    .min(1, {
-      message: "Comment content is required",
-    })
-    .max(1000, {
-      message: "Comment content should be at most 1000 characters",
-    }),
+	policyId: z.string().min(1, {
+		message: "Policy ID is required",
+	}),
+	content: z
+		.string()
+		.min(1, {
+			message: "Comment content is required",
+		})
+		.max(1000, {
+			message: "Comment content should be at most 1000 characters",
+		}),
+});
+
+export const addCommentSchema = z.object({
+	content: z
+		.string()
+		.min(1, "Comment content is required")
+		.max(1000, "Comment content should be at most 1000 characters")
+		.transform((val) => {
+			// Remove any HTML tags
+			return val.replace(/<[^>]*>/g, "");
+		}),
+	entityId: z.string().min(1, "Entity ID is required"),
 });
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/page.tsx
index 151043b345..2231ad9989 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/page.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/page.tsx
@@ -9,93 +9,133 @@ import { headers } from "next/headers";
 import { JSONContent } from "novel";
 import { cache } from "react";
 import { PolicyPageEditor } from "./editor/components/PolicyDetails";
+import { Comments } from "@/components/comments";
 
 export default async function PolicyDetails({
-	params,
+  params,
 }: {
-	params: Promise<{ locale: string; policyId: string; orgId: string }>;
+  params: Promise<{ locale: string; policyId: string; orgId: string }>;
 }) {
-	const { locale, policyId, orgId } = await params;
-
-	setStaticParamsLocale(locale);
-	const policy = await getPolicy(policyId);
-	const assignees = await getAssignees();
-
-	return (
-		<PageWithBreadcrumb
-			breadcrumbs={[
-				{ label: "Policies", href: `/${orgId}/policies/all` },
-				{ label: policy?.name ?? "Policy", current: true },
-			]}
-		>
-			<PolicyOverview policy={policy ?? null} assignees={assignees} />
-			<PolicyPageEditor
-				policyId={policyId}
-				policyContent={policy?.content ? (policy.content as JSONContent[]) : []}
-			/>
-		</PageWithBreadcrumb>
-	);
+  const { locale, policyId, orgId } = await params;
+
+  setStaticParamsLocale(locale);
+  const policy = await getPolicy(policyId);
+  const assignees = await getAssignees();
+  const comments = await getComments({ policyId });
+
+  return (
+    <PageWithBreadcrumb
+      breadcrumbs={[
+        { label: "Policies", href: `/${orgId}/policies/all` },
+        { label: policy?.name ?? "Policy", current: true },
+      ]}
+    >
+      <PolicyOverview policy={policy ?? null} assignees={assignees} />
+      <PolicyPageEditor
+        policyId={policyId}
+        policyContent={policy?.content ? (policy.content as JSONContent[]) : []}
+      />
+      <Comments entityId={policyId} comments={comments} />
+    </PageWithBreadcrumb>
+  );
 }
 
 export async function generateMetadata({
-	params,
+  params,
 }: {
-	params: Promise<{ locale: string; policyId: string }>;
+  params: Promise<{ locale: string; policyId: string }>;
 }): Promise<Metadata> {
-	const { locale } = await params;
+  const { locale } = await params;
 
-	setStaticParamsLocale(locale);
-	const t = await getI18n();
+  setStaticParamsLocale(locale);
+  const t = await getI18n();
 
-	return {
-		title: t("policies.overview.title"),
-	};
+  return {
+    title: t("policies.overview.title"),
+  };
 }
 
 const getPolicy = cache(async (policyId: string) => {
-	const session = await auth.api.getSession({
-		headers: await headers(),
-	});
+  const session = await auth.api.getSession({
+    headers: await headers(),
+  });
 
-	const organizationId = session?.session.activeOrganizationId;
+  const organizationId = session?.session.activeOrganizationId;
 
-	if (!organizationId) {
-		return null;
-	}
+  if (!organizationId) {
+    return null;
+  }
 
-	const policy = await db.policy.findUnique({
-		where: { id: policyId, organizationId },
-	});
+  const policy = await db.policy.findUnique({
+    where: { id: policyId, organizationId },
+  });
 
-	if (!policy) {
-		return null;
-	}
+  if (!policy) {
+    return null;
+  }
 
-	return policy;
+  return policy;
 });
 
 const getAssignees = cache(async () => {
-	const session = await auth.api.getSession({
-		headers: await headers(),
-	});
-
-	const organizationId = session?.session.activeOrganizationId;
-
-	if (!organizationId) {
-		return [];
-	}
-
-	const assignees = await db.member.findMany({
-		where: {
-			organizationId,
-			role: {
-				notIn: ["employee"],
-			},
-		},
-		include: {
-			user: true,
-		},
-	});
-
-	return assignees;
+  const session = await auth.api.getSession({
+    headers: await headers(),
+  });
+
+  const organizationId = session?.session.activeOrganizationId;
+
+  if (!organizationId) {
+    return [];
+  }
+
+  const assignees = await db.member.findMany({
+    where: {
+      organizationId,
+      role: {
+        notIn: ["employee"],
+      },
+    },
+    include: {
+      user: true,
+    },
+  });
+
+  return assignees;
 });
+
+const getComments = cache(async ({
+  policyId,
+}: {
+  policyId: string;
+}) => {
+  const session = await auth.api.getSession({
+    headers: await headers(),
+  });
+
+  const organizationId = session?.session.activeOrganizationId;
+
+  if (!organizationId) {
+    return [];
+  }
+
+  try {
+    const comments = await db.comment.findMany({
+      where: { entityId: policyId, organizationId },
+      include: {
+        author: {
+          include: {
+            user: true,
+          },
+        },
+      },
+      orderBy: {
+        createdAt: "desc",
+      },
+    });
+
+    return comments;
+  } catch (error) {
+    console.error(error);
+    return [];
+  }
+});
\ No newline at end of file
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/page.tsx
index 7de52f34c5..560c29ec84 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/page.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/page.tsx
@@ -1,3 +1,4 @@
+import { cache } from "react";
 import PageWithBreadcrumb from "@/components/pages/PageWithBreadcrumb";
 import { InherentRiskChart } from "@/components/risks/charts/inherent-risk-chart";
 import { ResidualRiskChart } from "@/components/risks/charts/residual-risk-chart";
@@ -12,6 +13,7 @@ import type { Metadata } from "next";
 import { setStaticParamsLocale } from "next-international/server";
 import { headers } from "next/headers";
 import { redirect } from "next/navigation";
+import { Comments } from "@/components/comments";
 
 interface PageProps {
   searchParams: Promise<{
@@ -27,31 +29,10 @@ interface PageProps {
 export default async function RiskPage({ searchParams, params }: PageProps) {
   const { riskId, orgId } = await params;
   const risk = await getRisk(riskId);
-
+  const comments = await getComments({ riskId });
   const assignees = await getAssignees();
   const t = await getI18n();
 
-  const {
-    search,
-    status,
-    sort,
-    page = "1",
-    per_page = "5",
-  } = await searchParams;
-
-  const columnHeaders = await getServerColumnHeaders();
-  const [column, order] = sort?.split(":") ?? [];
-  const hasFilters = !!(search || status);
-  const { tasks: loadedTasks, total } = await getTasks({
-    riskId,
-    search,
-    status: status as TaskStatus,
-    column,
-    order,
-    page: Number.parseInt(page),
-    per_page: Number.parseInt(per_page),
-  });
-
   if (!risk) {
     redirect("/");
   }
@@ -69,12 +50,13 @@ export default async function RiskPage({ searchParams, params }: PageProps) {
           <InherentRiskChart risk={risk} />
           <ResidualRiskChart risk={risk} />
         </div>
+        <Comments entityId={riskId} comments={comments} />
       </div>
     </PageWithBreadcrumb>
   );
 }
 
-async function getRisk(riskId: string) {
+const getRisk = cache(async (riskId: string) => {
   const session = await auth.api.getSession({
     headers: await headers(),
   });
@@ -98,113 +80,47 @@ async function getRisk(riskId: string) {
   });
 
   return risk;
-}
+});
 
-async function getTasks({
+const getComments = cache(async ({
   riskId,
-  search,
-  status,
-  column,
-  order,
-  page = 1,
-  per_page = 10,
 }: {
   riskId: string;
-  search?: string;
-  status?: TaskStatus;
-  column?: string;
-  order?: string;
-  page?: number;
-  per_page?: number;
-}) {
+}) => {
   const session = await auth.api.getSession({
     headers: await headers(),
   });
 
   if (!session || !session.session.activeOrganizationId) {
-    return { tasks: [], total: 0 };
+    return [];
   }
 
-  const skip = (page - 1) * per_page;
-
-  const [tasks, total] = await Promise.all([
-    db.task
-      .findMany({
-        where: {
-          relatedId: riskId,
-          relatedType: "risk",
-          organizationId: session.session.activeOrganizationId,
-          AND: [
-            search
-              ? {
-                OR: [
-                  { title: { contains: search, mode: "insensitive" } },
-                  {
-                    description: { contains: search, mode: "insensitive" },
-                  },
-                ],
-              }
-              : {},
-            status ? { status } : {},
-          ],
-        },
-        orderBy: column
-          ? {
-            [column]: order === "asc" ? "asc" : "desc",
-          }
-          : {
-            createdAt: "desc",
-          },
-        skip,
-        take: per_page,
-        include: {
-          assignee: {
-            include: {
-              user: true,
-            },
-          },
-        },
-      })
-      .then((tasks) =>
-        tasks.map(
-          (task): RiskTaskType => ({
-            id: task.id,
-            riskId: task.relatedId,
-            title: task.title,
-            status: task.status,
-            dueDate: task.dueDate.toISOString(),
-            assigneeId: task.assigneeId ?? "",
-            assignee: {
-              name: task.assignee?.user.name ?? "",
-              image: task.assignee?.user.image ?? "",
-            },
-          }),
-        ),
-      ),
-    db.task.count({
+  try {
+    const comments = await db.comment.findMany({
       where: {
-        relatedId: riskId,
-        relatedType: "risk",
+        entityId: riskId,
         organizationId: session.session.activeOrganizationId,
-        AND: [
-          search
-            ? {
-              OR: [
-                { title: { contains: search, mode: "insensitive" } },
-                { description: { contains: search, mode: "insensitive" } },
-              ],
-            }
-            : {},
-          status ? { status } : {},
-        ],
       },
-    }),
-  ]);
+      include: {
+        author: {
+          include: {
+            user: true,
+          },
+        },
+      },
+      orderBy: {
+        createdAt: "desc",
+      },
+    });
 
-  return { tasks, total };
-}
+    return comments;
+  } catch (error) {
+    console.error(error);
+    return [];
+  }
+});
 
-async function getAssignees() {
+const getAssignees = cache(async () => {
   const session = await auth.api.getSession({
     headers: await headers(),
   });
@@ -226,7 +142,7 @@ async function getAssignees() {
   });
 
   return assignees;
-}
+});
 
 export async function generateMetadata({
   params,
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/page.tsx
index 711a671e07..d1a311d221 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/page.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/page.tsx
@@ -13,222 +13,141 @@ import { ResidualRiskVendorChart } from "./components/residual-risk-vendor-chart
 import { SecondaryFields } from "./components/secondary-fields/secondary-fields";
 import { TitleAndDescription } from "./components/title-and-description/title-and-description";
 import PageWithBreadcrumb from "@/components/pages/PageWithBreadcrumb";
+import { cache } from "react";
+import { Comments } from "@/components/comments";
 
 interface PageProps {
-	searchParams: Promise<{
-		search?: string;
-		status?: string;
-		sort?: string;
-		page?: string;
-		per_page?: string;
-	}>;
-	params: Promise<{ vendorId: string; locale: string; orgId: string }>;
+  params: Promise<{ vendorId: string; locale: string; orgId: string }>;
 }
 
-export default async function VendorPage({ searchParams, params }: PageProps) {
-	const { vendorId, orgId } = await params;
-	const vendor = await getVendor(vendorId);
-	const assignees = await getAssignees();
-	const t = await getI18n();
-
-	const {
-		search,
-		status,
-		sort,
-		page = "1",
-		per_page = "5",
-	} = await searchParams;
-
-	const [column, order] = sort?.split(":") ?? [];
-	const hasFilters = !!(search || status);
-
-	const { tasks: loadedTasks, total } = await getTasks({
-		vendorId,
-		search,
-		status: status as TaskStatus,
-		column,
-		order,
-		page: Number.parseInt(page),
-		per_page: Number.parseInt(per_page),
-	});
-
-	if (!vendor) {
-		redirect("/");
-	}
-
-	return (
-		<PageWithBreadcrumb
-			breadcrumbs={[
-				{ label: "Vendors", href: `/${orgId}/vendors` },
-				{ label: vendor.name, current: true },
-			]}
-		>
-			<div className="flex flex-col gap-4">
-				<TitleAndDescription vendor={vendor} />
-				<SecondaryFields vendor={vendor} assignees={assignees} />
-				<div className="grid grid-cols-1 gap-4">
-					<InherentRiskVendorChart vendor={vendor} />
-					<ResidualRiskVendorChart vendor={vendor} />
-				</div>
-			</div>
-		</PageWithBreadcrumb>
-	);
+export default async function VendorPage({ params }: PageProps) {
+  const { vendorId, orgId } = await params;
+  const vendor = await getVendor(vendorId);
+  const assignees = await getAssignees();
+  const comments = await getComments({ vendorId });
+
+  if (!vendor) {
+    redirect("/");
+  }
+
+  return (
+    <PageWithBreadcrumb
+      breadcrumbs={[
+        { label: "Vendors", href: `/${orgId}/vendors` },
+        { label: vendor.name, current: true },
+      ]}
+    >
+      <div className="flex flex-col gap-4">
+        <TitleAndDescription vendor={vendor} />
+        <SecondaryFields vendor={vendor} assignees={assignees} />
+        <div className="grid grid-cols-1 gap-4">
+          <InherentRiskVendorChart vendor={vendor} />
+          <ResidualRiskVendorChart vendor={vendor} />
+        </div>
+        <Comments entityId={vendorId} comments={comments} />
+      </div>
+    </PageWithBreadcrumb>
+  );
 }
 
-async function getVendor(vendorId: string) {
-	const session = await auth.api.getSession({
-		headers: await headers(),
-	});
-
-	if (!session || !session.session.activeOrganizationId) {
-		return null;
-	}
-
-	const vendor = await db.vendor.findUnique({
-		where: {
-			id: vendorId,
-			organizationId: session.session.activeOrganizationId,
-		},
-		include: {
-			assignee: {
-				include: {
-					user: true,
-				},
-			},
-		},
-	});
-
-	return vendor;
-}
-
-async function getAssignees() {
-	const session = await auth.api.getSession({
-		headers: await headers(),
-	});
-
-	if (!session || !session.session.activeOrganizationId) {
-		return [];
-	}
-
-	const assignees = await db.member.findMany({
-		where: {
-			organizationId: session.session.activeOrganizationId,
-			role: {
-				notIn: ["employee"],
-			},
-		},
-		include: {
-			user: true,
-		},
-	});
-
-	return assignees;
-}
-
-async function getTasks({
-	vendorId,
-	search,
-	status,
-	column,
-	order,
-	page = 1,
-	per_page = 10,
+const getVendor = cache(async (vendorId: string) => {
+  const session = await auth.api.getSession({
+    headers: await headers(),
+  });
+
+  if (!session || !session.session.activeOrganizationId) {
+    return null;
+  }
+
+  const vendor = await db.vendor.findUnique({
+    where: {
+      id: vendorId,
+      organizationId: session.session.activeOrganizationId,
+    },
+    include: {
+      assignee: {
+        include: {
+          user: true,
+        },
+      },
+    },
+  });
+
+  return vendor;
+});
+
+const getComments = cache(async ({
+  vendorId,
 }: {
-	vendorId: string;
-	search?: string;
-	status?: TaskStatus;
-	column?: string;
-	order?: string;
-	page?: number;
-	per_page?: number;
-}) {
-	const session = await auth.api.getSession({
-		headers: await headers(),
-	});
-
-	if (!session || !session.session.activeOrganizationId) {
-		return { tasks: [], total: 0 };
-	}
-
-	const skip = (page - 1) * per_page;
-
-	const [tasks, total] = await Promise.all([
-		db.task
-			.findMany({
-				where: {
-					relatedId: vendorId,
-					AND: [
-						search
-							? {
-									OR: [
-										{ title: { contains: search, mode: "insensitive" } },
-										{
-											description: { contains: search, mode: "insensitive" },
-										},
-									],
-								}
-							: {},
-						status ? { status } : {},
-					],
-				},
-				orderBy: column
-					? {
-							[column]: order === "asc" ? "asc" : "desc",
-						}
-					: {
-							createdAt: "desc",
-						},
-				skip,
-				take: per_page,
-				include: {
-					assignee: {
-						include: {
-							user: true,
-						},
-					},
-				},
-			})
-			.then((tasks) =>
-				tasks.map((task) => ({
-					...task,
-					dueDate: task.dueDate ?? new Date(),
-					assignee: {
-						name: task.assignee?.user.name ?? "",
-						image: task.assignee?.user.image ?? "",
-					},
-				})),
-			),
-		db.task.count({
-			where: {
-				relatedId: vendorId,
-				AND: [
-					search
-						? {
-								OR: [
-									{ title: { contains: search, mode: "insensitive" } },
-									{ description: { contains: search, mode: "insensitive" } },
-								],
-							}
-						: {},
-					status ? { status } : {},
-				],
-			},
-		}),
-	]);
-
-	return { tasks, total };
-}
+  vendorId: string;
+}) => {
+  const session = await auth.api.getSession({
+    headers: await headers(),
+  });
+
+  if (!session || !session.session.activeOrganizationId) {
+    return [];
+  }
+
+  try {
+    const comments = await db.comment.findMany({
+      where: {
+        entityId: vendorId,
+        organizationId: session.session.activeOrganizationId,
+      },
+      include: {
+        author: {
+          include: {
+            user: true,
+          },
+        },
+      },
+      orderBy: {
+        createdAt: "desc",
+      },
+    });
+
+    return comments;
+  } catch (error) {
+    console.error(error);
+    return [];
+  }
+});
+
+const getAssignees = cache(async () => {
+  const session = await auth.api.getSession({
+    headers: await headers(),
+  });
+
+  if (!session || !session.session.activeOrganizationId) {
+    return [];
+  }
+
+  const assignees = await db.member.findMany({
+    where: {
+      organizationId: session.session.activeOrganizationId,
+      role: {
+        notIn: ["employee"],
+      },
+    },
+    include: {
+      user: true,
+    },
+  });
+
+  return assignees;
+});
 
 export async function generateMetadata({
-	params,
+  params,
 }: {
-	params: Promise<{ locale: string }>;
+  params: Promise<{ locale: string }>;
 }): Promise<Metadata> {
-	const { locale } = await params;
-	setStaticParamsLocale(locale);
-	const t = await getI18n();
+  const { locale } = await params;
+  setStaticParamsLocale(locale);
+  const t = await getI18n();
 
-	return {
-		title: t("sidebar.vendors"),
-	};
+  return {
+    title: t("sidebar.vendors"),
+  };
 }
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/components/create-vendor-form.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/components/create-vendor-form.tsx
index ad47988ce0..5b477e80f2 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/components/create-vendor-form.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/components/create-vendor-form.tsx
@@ -1,7 +1,6 @@
 "use client";
 
 import { useI18n } from "@/locales/client";
-import { useSession } from "@/utils/auth-client";
 import { Member, User, VendorCategory, VendorStatus } from "@comp/db/types";
 import {
   Accordion,
@@ -30,7 +29,6 @@ import { Textarea } from "@comp/ui/textarea";
 import { zodResolver } from "@hookform/resolvers/zod";
 import { ArrowRightIcon } from "lucide-react";
 import { useAction } from "next-safe-action/hooks";
-import { useRouter } from "next/navigation";
 import { useQueryState } from "nuqs";
 import { useForm } from "react-hook-form";
 import { toast } from "sonner";
@@ -51,45 +49,12 @@ export function CreateVendorForm({
   assignees,
 }: { assignees: (Member & { user: User })[] }) {
   const t = useI18n();
-  const session = useSession();
-
-  // Get the same query parameters as the table
-  const [search] = useQueryState("search");
-  const [page] = useQueryState("page", {
-    defaultValue: 1,
-    parse: Number.parseInt,
-  });
-  const [pageSize] = useQueryState("pageSize", {
-    defaultValue: 10,
-    parse: Number,
-  });
-  const [status] = useQueryState<VendorStatus | null>("status", {
-    defaultValue: null,
-    parse: (value) => value as VendorStatus | null,
-  });
-  const [category] = useQueryState<VendorCategory | null>("category", {
-    defaultValue: null,
-    parse: (value) => value as VendorCategory | null,
-  });
-  const [assigneeId] = useQueryState<string | null>("assigneeId", {
-    defaultValue: null,
-    parse: (value) => value,
-  });
-
-  const router = useRouter();
+  const [_, setCreateVendorSheet] = useQueryState("create-vendor-sheet");
 
   const createVendor = useAction(createVendorAction, {
     onSuccess: async (data) => {
-      const organizationId = session.data?.session.activeOrganizationId;
-
-      if (!organizationId) {
-        toast.error(t("vendors.form.create_vendor_error"));
-        return;
-      }
-
       toast.success(t("vendors.form.create_vendor_success"));
-
-      router.push(`/${organizationId}/vendors/${data.data?.data?.id}`);
+      setCreateVendorSheet(null);
     },
     onError: () => {
       toast.error(t("vendors.form.create_vendor_error"));
diff --git a/apps/app/src/components/comments.tsx b/apps/app/src/components/comments.tsx
new file mode 100644
index 0000000000..d6599c32d2
--- /dev/null
+++ b/apps/app/src/components/comments.tsx
@@ -0,0 +1,103 @@
+"use client";
+
+import { useState } from "react";
+import { Card, CardContent, CardHeader, CardTitle } from "@comp/ui/card";
+import { Button } from "@comp/ui/button";
+import { CreateCommentForm } from "./forms/create-comment-form";
+import { useI18n } from "@/locales/client";
+import { MessageSquarePlus } from "lucide-react";
+import { formatDistanceToNow } from "date-fns";
+import { Avatar, AvatarImage, AvatarFallback } from "@comp/ui/avatar";
+import { Member, User } from "@comp/db/types";
+import { cn } from "@comp/ui/cn";
+
+interface Comment {
+  id: string;
+  content: string;
+  authorId: string;
+  createdAt: Date;
+  author: Member & {
+    user: User;
+  };
+}
+
+interface CommentsProps {
+  entityId: string;
+  comments: Comment[];
+}
+
+export function Comments({ entityId, comments }: CommentsProps) {
+  const [isAddingComment, setIsAddingComment] = useState(false);
+  const t = useI18n();
+
+  return (
+    <Card>
+      <CardHeader className="flex flex-row items-center justify-between space-y-0 border-b">
+        <CardTitle>{t("common.comments.title")}</CardTitle>
+        <Button
+          variant="outline"
+          size="sm"
+          onClick={() => setIsAddingComment(!isAddingComment)}
+          className="gap-2"
+        >
+          <MessageSquarePlus className="h-4 w-4" />
+          {t("common.comments.new")}
+        </Button>
+      </CardHeader>
+      <CardContent className="pt-6">
+        {isAddingComment && (
+          <div className="mb-6">
+            <CreateCommentForm entityId={entityId} />
+          </div>
+        )}
+        <div className="space-y-6">
+          {comments.map((comment, index) => (
+            <div
+              key={comment.id}
+              className={cn(
+                "space-y-3",
+                index !== comments.length - 1 && "pb-6 border-b"
+              )}
+            >
+              <div className="flex items-center justify-between">
+                <div className="flex items-center gap-3">
+                  <Avatar className="h-8 w-8">
+                    <AvatarImage
+                      src={comment.author.user.image || undefined}
+                      alt={comment.author.user.name || "User"}
+                    />
+                    <AvatarFallback className="bg-primary/10">
+                      {comment.author.user.name?.charAt(0) || "?"}
+                    </AvatarFallback>
+                  </Avatar>
+                  <div className="flex flex-col">
+                    <span className="text-sm font-medium leading-none">
+                      {comment.author.user.name || "Unknown User"}
+                    </span>
+                    <span className="text-xs text-muted-foreground">
+                      {formatDistanceToNow(new Date(comment.createdAt), {
+                        addSuffix: true,
+                      })}
+                    </span>
+                  </div>
+                </div>
+              </div>
+              <div className="pl-11">
+                <p className="text-sm text-muted-foreground whitespace-pre-wrap break-words">
+                  {comment.content}
+                </p>
+              </div>
+            </div>
+          ))}
+          {comments.length === 0 && (
+            <div className="text-center py-8">
+              <p className="text-sm text-muted-foreground">
+                {t("common.comments.empty.description")}
+              </p>
+            </div>
+          )}
+        </div>
+      </CardContent>
+    </Card>
+  );
+}
diff --git a/apps/app/src/components/forms/create-comment-form.tsx b/apps/app/src/components/forms/create-comment-form.tsx
new file mode 100644
index 0000000000..a131722c8e
--- /dev/null
+++ b/apps/app/src/components/forms/create-comment-form.tsx
@@ -0,0 +1,105 @@
+"use client";
+
+import { useAction } from "next-safe-action/hooks";
+import { addCommentAction } from "@/actions/add-comment";
+import { Textarea } from "@comp/ui/textarea";
+import { useForm } from "react-hook-form";
+import {
+  Form,
+  FormControl,
+  FormField,
+  FormItem,
+  FormLabel,
+  FormMessage,
+} from "@comp/ui/form";
+import { toast } from "sonner";
+import { z } from "zod";
+import { addCommentSchema } from "@/actions/schema";
+import { zodResolver } from "@hookform/resolvers/zod";
+import {
+  Accordion,
+  AccordionContent,
+  AccordionItem,
+  AccordionTrigger,
+} from "@comp/ui/accordion";
+import { Button } from "@comp/ui/button";
+import { useI18n } from "@/locales/client";
+import { ArrowRightIcon } from "lucide-react";
+
+export function CreateCommentForm({
+  entityId,
+}: {
+  entityId: string;
+}) {
+  const addComment = useAction(addCommentAction, {
+    onSuccess: () => {
+      toast.success("Comment added successfully");
+      form.reset();
+    },
+    onError: () => {
+      toast.error("Error adding comment");
+    },
+  });
+
+  const onSubmit = (data: z.infer<typeof addCommentSchema>) => {
+    addComment.execute({
+      ...data,
+      entityId,
+    });
+  };
+
+  const form = useForm<z.infer<typeof addCommentSchema>>({
+    resolver: zodResolver(addCommentSchema),
+    defaultValues: {
+      content: "",
+      entityId,
+    },
+    mode: "onChange",
+  });
+
+  const t = useI18n();
+
+  return (
+    <Form {...form}>
+      <form onSubmit={form.handleSubmit(onSubmit)}>
+        <div className="scrollbar-hide overflow-auto">
+          <div>
+            <Accordion type="multiple" defaultValue={["comment"]}>
+              <AccordionItem value="comment">
+                <AccordionTrigger>Comment</AccordionTrigger>
+                <AccordionContent>
+                  <div className="space-y-4">
+                    <FormField
+                      control={form.control}
+                      name="content"
+                      render={({ field }) => (
+                        <FormItem>
+                          <FormControl>
+                            <Textarea {...field} />
+                          </FormControl>
+                          <FormMessage />
+                        </FormItem>
+                      )}
+                    />
+                  </div>
+                  <div className="flex justify-end mt-4">
+                    <Button
+                      type="submit"
+                      variant="action"
+                      disabled={addComment.status === "executing"}
+                    >
+                      <div className="flex items-center justify-center">
+                        {t("common.actions.create")}
+                        <ArrowRightIcon className="ml-2 h-4 w-4" />
+                      </div>
+                    </Button>
+                  </div>
+                </AccordionContent>
+              </AccordionItem>
+            </Accordion>
+          </div>
+        </div>
+      </form>
+    </Form>
+  );
+}
diff --git a/packages/db/prisma/migrations/20250406004306_added_common_comment_table/migration.sql b/packages/db/prisma/migrations/20250406004306_added_common_comment_table/migration.sql
new file mode 100644
index 0000000000..31fce5e7e8
--- /dev/null
+++ b/packages/db/prisma/migrations/20250406004306_added_common_comment_table/migration.sql
@@ -0,0 +1,20 @@
+-- CreateTable
+CREATE TABLE "Comment" (
+    "id" TEXT NOT NULL DEFAULT generate_prefixed_cuid('cmt'::text),
+    "content" TEXT NOT NULL,
+    "entityId" TEXT NOT NULL,
+    "authorId" TEXT NOT NULL,
+    "organizationId" TEXT NOT NULL,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+    CONSTRAINT "Comment_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE INDEX "Comment_entityId_idx" ON "Comment"("entityId");
+
+-- AddForeignKey
+ALTER TABLE "Comment" ADD CONSTRAINT "Comment_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "Member"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Comment" ADD CONSTRAINT "Comment_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/packages/db/prisma/schema/auth.prisma b/packages/db/prisma/schema/auth.prisma
index 736e75478a..8df398144b 100644
--- a/packages/db/prisma/schema/auth.prisma
+++ b/packages/db/prisma/schema/auth.prisma
@@ -88,6 +88,7 @@ model Member {
     Risk                            Risk[]
     Task                            Task[]
     Vendor                          Vendor[]
+    comments                        Comment[]
 }
 
 model Invitation {
diff --git a/packages/db/prisma/schema/common.prisma b/packages/db/prisma/schema/common.prisma
new file mode 100644
index 0000000000..6f9e8cc829
--- /dev/null
+++ b/packages/db/prisma/schema/common.prisma
@@ -0,0 +1,12 @@
+model Comment {
+  id String @id @default(dbgenerated("generate_prefixed_cuid('cmt'::text)"))
+  content String
+  entityId String
+  authorId String
+  author Member @relation(fields: [authorId], references: [id])
+  organizationId String
+  organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
+  createdAt DateTime @default(now())
+
+  @@index([entityId])
+}
\ No newline at end of file
diff --git a/packages/db/prisma/schema/organization.prisma b/packages/db/prisma/schema/organization.prisma
index 065f34da52..55684dba86 100644
--- a/packages/db/prisma/schema/organization.prisma
+++ b/packages/db/prisma/schema/organization.prisma
@@ -20,6 +20,7 @@ model Organization {
   risk               Risk[]
   vendors            Vendor[]
   tasks              Task[]
+  comments           Comment[]
 
   @@index([slug])
 }


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (15)</summary><blockquote>

<details>
<summary>packages/db/prisma/schema/common.prisma (1)</summary><blockquote>

`1-12`: **Comment model design looks solid**

The Comment model is well-structured with appropriate fields and relationships. The index on `entityId` is a good performance optimization for querying comments by entity.


**Consider adding an updatedAt field for future extensibility**

While the current implementation may not require comment editing, adding an `updatedAt` field would follow the pattern of other models and provide flexibility for future feature enhancements.

```diff
 model Comment {
   id String @id @default(dbgenerated("generate_prefixed_cuid('cmt'::text)"))
   content String
   entityId String
   authorId String
   author Member @relation(fields: [authorId], references: [id])
   organizationId String
   organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
   createdAt DateTime @default(now())
+  updatedAt DateTime @updatedAt

   @@index([entityId])
 }
```

**Consider adding a type field for comment categorization**

If you anticipate different types of comments in the future (e.g., questions, feedback, notes), a `type` field could be valuable.

```diff
 model Comment {
   id String @id @default(dbgenerated("generate_prefixed_cuid('cmt'::text)"))
   content String
   entityId String
+  type String? // Optional field for comment categorization
   authorId String
   author Member @relation(fields: [authorId], references: [id])
   organizationId String
   organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
   createdAt DateTime @default(now())

   @@index([entityId])
 }
```

</blockquote></details>
<details>
<summary>apps/app/src/actions/schema.ts (1)</summary><blockquote>

`286-296`: **Consider more robust HTML sanitization.**
Currently, this `.replace(/<[^>]*>/g, "")` transform strips out basic HTML tags. If malicious scripting or other HTML attacks are a concern, you may want to use specialized libraries (e.g., DOMPurify) or perform additional sanitization measures server-side.

</blockquote></details>
<details>
<summary>apps/app/src/actions/add-comment.ts (1)</summary><blockquote>

`24-75`: **Ensure the entity existence is verified if needed.**
While organization membership validation is performed, consider whether you also need to check that the referenced entity (policy, risk, etc.) exists to prevent ghost comments pointing to invalid entities. The rest of the logic, including revalidatePath usage, looks solid.

</blockquote></details>
<details>
<summary>apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/page.tsx (1)</summary><blockquote>

`106-136`: **Consider pagination for large comment sets.**
If a policy might have a large number of comments, returning all comments in a single query could degrade performance. Consider lazy-loading or paginating them for an enhanced user experience.

</blockquote></details>
<details>
<summary>apps/app/src/components/comments.tsx (2)</summary><blockquote>

`29-52`: **Consider improving state management for comment creation.**

The toggle mechanism for showing/hiding the comment form works, but consider enhancing the UX by automatically hiding the form after successful submission.

```diff
-  const [isAddingComment, setIsAddingComment] = useState(false);
+  const [isAddingComment, setIsAddingComment] = useState(false);
+
+  // Add this to CreateCommentForm props
+  const onCommentAdded = useCallback(() => {
+    setIsAddingComment(false);
+  }, []);
```

And pass the callback to CreateCommentForm:
```diff
-            <CreateCommentForm entityId={entityId} />
+            <CreateCommentForm entityId={entityId} onSuccess={onCommentAdded} />
```

---

`29-103`: **Consider adding pagination for large comment volumes.**

The current implementation displays all comments at once, which could lead to performance issues if there are many comments. Consider implementing pagination or infinite scrolling for better performance.


For large volumes of comments, consider implementing pagination:

```diff
interface CommentsProps {
  entityId: string;
  comments: Comment[];
+ totalComments?: number;
+ currentPage?: number;
+ onPageChange?: (page: number) => void;
}

export function Comments({ 
  entityId, 
  comments,
+ totalComments = comments.length,
+ currentPage = 1,
+ onPageChange,
}: CommentsProps) {
  // ...existing code

  return (
    <Card>
      {/* ...existing code */}
      <CardContent className="pt-6">
        {/* ...existing code */}
        <div className="space-y-6">
          {/* ...existing comment rendering */}
          
+         {totalComments > 10 && (
+           <div className="flex justify-center mt-4">
+             <Pagination
+               currentPage={currentPage}
+               totalPages={Math.ceil(totalComments / 10)}
+               onPageChange={onPageChange}
+             />
+           </div>
+         )}
        </div>
      </CardContent>
    </Card>
  );
}
```

</blockquote></details>
<details>
<summary>packages/db/prisma/migrations/20250406004306_added_common_comment_table/migration.sql (1)</summary><blockquote>

`5-6`: **Consider adding a type field for comment categorization.**

The current schema doesn't include a way to categorize comments (e.g., feedback, question, bug report). Adding a type field could enhance filtering and display capabilities.

```diff
CREATE TABLE "Comment" (
    "id" TEXT NOT NULL DEFAULT generate_prefixed_cuid('cmt'::text),
    "content" TEXT NOT NULL,
    "entityId" TEXT NOT NULL,
+   "type" TEXT DEFAULT 'general',
    "authorId" TEXT NOT NULL,
    "organizationId" TEXT NOT NULL,
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT "Comment_pkey" PRIMARY KEY ("id")
);
```

</blockquote></details>
<details>
<summary>apps/app/src/components/forms/create-comment-form.tsx (4)</summary><blockquote>

`69-69`: **Internationalize the accordion trigger text.**

The "Comment" text in the AccordionTrigger is hardcoded, while other text in the form uses the internationalization system.

```diff
-                <AccordionTrigger>Comment</AccordionTrigger>
+                <AccordionTrigger>{t("common.comments.form.title")}</AccordionTrigger>
```

---

`72-84`: **Consider adding character count feedback.**

Since the schema limits comments to 1000 characters, it would be helpful to provide visual feedback about the remaining character count.

```diff
<FormItem>
  <FormControl>
    <Textarea {...field} />
  </FormControl>
+ <div className="text-xs text-muted-foreground text-right mt-1">
+   {field.value.length}/1000
+ </div>
  <FormMessage />
</FormItem>
```

---

`86-96`: **Consider adding a cancel button.**

Adding a cancel button would improve the user experience by allowing users to easily dismiss the form without having to toggle the add comment button again.

```diff
<div className="flex justify-end mt-4">
+  <Button
+    type="button"
+    variant="outline"
+    className="mr-2"
+    onClick={() => form.reset()}
+  >
+    {t("common.actions.cancel")}
+  </Button>
  <Button
    type="submit"
    variant="action"
    disabled={addComment.status === "executing"}
  >
    <div className="flex items-center justify-center">
      {t("common.actions.create")}
      <ArrowRightIcon className="ml-2 h-4 w-4" />
    </div>
  </Button>
</div>
```

---

`62-104`: **Add loading indication during form submission.**

Currently, the button is disabled during submission, but there's no visual indicator that something is happening. Consider adding a loading spinner to improve user feedback.

```diff
import { Button } from "@comp/ui/button";
import { useI18n } from "@/locales/client";
-import { ArrowRightIcon } from "lucide-react";
+import { ArrowRightIcon, Loader2 } from "lucide-react";

// ...

<Button
  type="submit"
  variant="action"
  disabled={addComment.status === "executing"}
>
  <div className="flex items-center justify-center">
-   {t("common.actions.create")}
-   <ArrowRightIcon className="ml-2 h-4 w-4" />
+   {addComment.status === "executing" ? (
+     <>
+       {t("common.states.submitting")}
+       <Loader2 className="ml-2 h-4 w-4 animate-spin" />
+     </>
+   ) : (
+     <>
+       {t("common.actions.create")}
+       <ArrowRightIcon className="ml-2 h-4 w-4" />
+     </>
+   )}
  </div>
</Button>
```

</blockquote></details>
<details>
<summary>apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/page.tsx (4)</summary><blockquote>

`23-30`: **Validate redirect behavior.**  
Currently, when no vendor is found, the code redirects to the homepage (`"/"`). This may not be ideal if you want to inform users that the vendor doesn't exist or if they lack permissions. Consider redirecting to a dedicated 404 or 403 page for better user feedback.

---

`53-77`: **Fetching vendor data with caching is fine.**  
The logic is straightforward, and the caching mechanism should help performance by preventing repeated DB calls. Optionally, you could consolidate fetching the vendor and comments if you prefer a single DB call. However, keeping them separate is equally valid for maintainability.

---

`79-110`: **Consider pagination for large comment volumes.**  
While this implementation works well, if vendors can accumulate many comments, returning them all at once may impact performance. Adding pagination or limiting results can keep the UI responsive.

---

`137-147`: **Metadata generation is correct; consider adding the vendor name.**  
You’re setting a generic title for the page. If you’d like a more descriptive title, prepend vendor-specific details (e.g., `Vendor - {vendor.name}`). Otherwise, this is fine as is.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 2a7099a98253676424b360184f79519c981df164 and 2f871fce3f97a14302418b353c5ab08781a83315.

</details>

<details>
<summary>📒 Files selected for processing (12)</summary>

* `apps/app/src/actions/add-comment.ts` (1 hunks)
* `apps/app/src/actions/schema.ts` (1 hunks)
* `apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/page.tsx` (1 hunks)
* `apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/page.tsx` (6 hunks)
* `apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/page.tsx` (1 hunks)
* `apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/components/create-vendor-form.tsx` (1 hunks)
* `apps/app/src/components/comments.tsx` (1 hunks)
* `apps/app/src/components/forms/create-comment-form.tsx` (1 hunks)
* `packages/db/prisma/migrations/20250406004306_added_common_comment_table/migration.sql` (1 hunks)
* `packages/db/prisma/schema/auth.prisma` (1 hunks)
* `packages/db/prisma/schema/common.prisma` (1 hunks)
* `packages/db/prisma/schema/organization.prisma` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (5)</summary>

<details>
<summary>apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/page.tsx (1)</summary><blockquote>

<details>
<summary>apps/app/src/components/comments.tsx (1)</summary>

* `Comments` (29-103)

</details>

</blockquote></details>
<details>
<summary>apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/components/create-vendor-form.tsx (1)</summary><blockquote>

<details>
<summary>apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/actions/create-vendor-action.ts (1)</summary>

* `createVendorAction` (21-54)

</details>

</blockquote></details>
<details>
<summary>apps/app/src/components/forms/create-comment-form.tsx (2)</summary><blockquote>

<details>
<summary>apps/app/src/actions/add-comment.ts (1)</summary>

* `addCommentAction` (10-75)

</details>
<details>
<summary>apps/app/src/actions/schema.ts (1)</summary>

* `addCommentSchema` (286-296)

</details>

</blockquote></details>
<details>
<summary>apps/app/src/components/comments.tsx (1)</summary><blockquote>

<details>
<summary>apps/app/src/components/forms/create-comment-form.tsx (1)</summary>

* `CreateCommentForm` (29-105)

</details>

</blockquote></details>
<details>
<summary>apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/page.tsx (2)</summary><blockquote>

<details>
<summary>apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/title-and-description/title-and-description.tsx (1)</summary>

* `TitleAndDescription` (12-44)

</details>
<details>
<summary>apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/secondary-fields/secondary-fields.tsx (1)</summary>

* `SecondaryFields` (8-33)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (30)</summary><blockquote>

<details>
<summary>packages/db/prisma/schema/auth.prisma (1)</summary>

`91-91`: **Relationship effectively established for comments and members**

The addition of the `comments Comment[]` field to the `Member` model correctly establishes a one-to-many relationship, enabling members to have multiple associated comments.

</details>
<details>
<summary>packages/db/prisma/schema/organization.prisma (1)</summary>

`23-23`: **Relationship effectively established for comments and organizations**

The addition of the `comments Comment[]` field to the `Organization` model correctly establishes a one-to-many relationship, enabling organizations to have multiple associated comments.

</details>
<details>
<summary>apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/components/create-vendor-form.tsx (1)</summary>

`52-52`: **Code simplification looks good**

The changes simplify the form's behavior by removing unnecessary session checks and complex navigation, focusing solely on closing the vendor creation sheet upon success.



Also applies to: 57-57

</details>
<details>
<summary>apps/app/src/actions/schema.ts (1)</summary>

`2-285`: **No functional changes aside from indentation.**

</details>
<details>
<summary>apps/app/src/actions/add-comment.ts (1)</summary>

`1-9`: **Initial setup looks good.**
No issues found in import statements or server directive.

</details>
<details>
<summary>apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/page.tsx (4)</summary>

`12-39`: **Integration of Comments component is well-structured.**
Fetching comments alongside policy data and displaying them in the `Comments` component appears seamless. Great job unifying the policy overview, editor, and comments into a single page.

---

`43-56`: **No material changes aside from formatting in generateMetadata.**

---

`58-78`: **Policy retrieval logic remains consistent.**
No concerns found.

---

`80-104`: **No notable changes aside from indentation in getAssignees.**

</details>
<details>
<summary>apps/app/src/components/comments.tsx (5)</summary>

`1-13`: **Comprehensive imports and setup.**

All the necessary components and utilities are correctly imported for handling comments rendering and functionality.

---

`14-27`: **Clean interface definitions for comment data structures.**

The Comment and CommentsProps interfaces are well-structured, providing type safety for the component. The interfaces accurately capture the relationship between comments, authors, and users.

---

`53-91`: **Good implementation of comment list with fallbacks.**

The comment rendering implementation properly handles user information with fallbacks for missing data. The avatar component gracefully handles missing images, and the UI design for each comment is clean with appropriate spacing and borders.

---

`92-98`: **Good empty state handling.**

The component properly handles the case when there are no comments by displaying a helpful message, which maintains a good user experience.

---

`65-71`: <details>
<summary>✅ Verification successful</summary>

**Consider additional security for user content.**

The content of comments is rendered directly without sanitization, which could potentially lead to XSS attacks if the backend validation is bypassed.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there's any client-side sanitization in the app for user content

# Look for sanitization libraries in package.json
cat package.json | grep -E 'sanitize|xss|dompurify'

# Check if the addCommentSchema in schema.ts includes sanitization
rg -A 10 "addCommentSchema" --type ts
```

Length of output: 3102

---



</details>

**Security Verification Summary:**  
Our investigation confirms that user comment input is sanitized server-side in the `addCommentSchema`. The schema applies a transformation that strips out HTML tags using a regex, which mitigates the XSS concern for comment content. Additionally, the displayed user data (avatar image and name) is rendered as plain text, with React handling proper escaping, so no additional client-side sanitization is necessary for these elements.

- **File:** `apps/app/src/components/comments.tsx`  
- **Verification Details:**  
  - The `addCommentSchema` in `apps/app/src/actions/schema.ts` transforms the `content` field with `val.replace(/<[^>]*>/g, "")` to remove HTML tags.
  - React's default behavior escapes interpolated strings, ensuring safe rendering of user names and other text data.

While the current sanitization strategy effectively addresses XSS risks, consider evaluating dedicated sanitization libraries (e.g., DOMPurify) in the future for more comprehensive security, especially if comment rendering requirements become more complex.

</details>
<details>
<summary>apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/page.tsx (5)</summary>

`16-16`: **Appropriately imported Comments component.**

The Comments component is correctly imported to support the new comment feature on the risk page.

---

`32-32`: **Proper data fetching for comments.**

The page correctly fetches comments related to the risk by awaiting the getComments function with the appropriate riskId.

---

`53-53`: **Good integration of the Comments component.**

The Comments component is well-integrated into the page layout, passing the required props (entityId and comments) correctly.

---

`59-83`: **Effective use of caching for the getRisk function.**

Using the cache function will optimize performance by preventing redundant database queries, which is particularly valuable for frequently accessed risk data.

---

`118-140`: **Effective use of caching for the getAssignees function.**

The cache implementation for getAssignees will help improve performance by preventing unnecessary database queries.

</details>
<details>
<summary>packages/db/prisma/migrations/20250406004306_added_common_comment_table/migration.sql (4)</summary>

`1-11`: **Well-structured Comment table creation.**

The Comment table is well-designed with appropriate fields for content, entity references, author information, and timestamps. The use of generate_prefixed_cuid for ID generation aligns with best practices for creating human-readable unique identifiers.

---

`13-14`: **Good use of indexing for query optimization.**

Creating an index on the entityId field is a good practice that will improve query performance when filtering comments by entity.

---

`19-20`: **Consistent foreign key constraint for Organization.**

The CASCADE constraint for organizationId is appropriate, ensuring that when an organization is deleted, all associated comments are also deleted, which helps maintain data integrity.

---

`16-17`: <details>
<summary>❓ Verification inconclusive</summary>

**Consider the implications of ON DELETE RESTRICT.**

The RESTRICT constraint on authorId means that a Member cannot be deleted if they have created comments. Consider whether this is the intended behavior or if a CASCADE or SET NULL approach might be more appropriate for your application's requirements.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there are other places in the codebase where similar foreign key constraints are defined

# Look for similar constraint patterns in other migration files
rg -A 1 "ON DELETE RESTRICT" --type sql --glob "*/migrations/*"

# Look for member deletion logic in the codebase
rg -A 5 "member.*delete" --type ts
```

Length of output: 611

---



</details>

**Reassess the deletion behavior for Member records**

The migration currently uses `ON DELETE RESTRICT` on the foreign key from `Comment.authorId` to `Member.id`, which prevents deleting a Member if they have any associated comments. We found that the application deletion endpoint in `apps/app/src/app/api/v1/employees/[id]/route.ts` calls `db.member.delete`, which could lead to errors if the Member being deleted has comments. Please review whether this restriction is intentional. If members should be removable regardless of their comments, consider using `CASCADE` (to delete related comments) or `SET NULL` (to disassociate them) instead, or adjust the deletion flow to handle the presence of comments appropriately.

</details>
<details>
<summary>apps/app/src/components/forms/create-comment-form.tsx (3)</summary>

`1-28`: **Comprehensive import list for form functionality.**

All necessary components and utilities for form handling, validation, and UI are properly imported.

---

`29-42`: **Good error and success handling.**

The form provides appropriate feedback to the user through toast notifications and resets the form upon successful submission, which contributes to a good user experience.

---

`44-58`: **Form submission and validation setup is well-implemented.**

The form correctly uses Zod for validation, sets appropriate default values, and handles the submission process effectively.

</details>
<details>
<summary>apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/page.tsx (4)</summary>

`16-17`: **Confirm compatibility of the `cache` API usage.**  
`cache` is a relatively new server component API introduced in Next.js 13. Please verify that your project environment and React/Next.js versions support this usage.  

Would you like me to run a script to confirm that your Next.js version supports `cache`?

---

`20-20`: **Ensure that `params` as a Promise is intentional.**  
Typically, route parameters are passed synchronously in Next.js. Make sure that passing a `Promise` type is indeed required in your use case and is functioning as expected.

---

`33-50`: **Layout integration for comments looks good.**  
The inclusion of the `<Comments>` component under the vendor details effectively groups relevant data. Overall, this improves user experience by making comments easily discoverable.

---

`112-134`: **Assignees retrieval logic appears correct.**  
The `getAssignees` function is clean, concise, and follows the same session validation pattern.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-06T01:42:09Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/page.tsx (1)</summary><blockquote>

`79-115`: **Error handling in getComments function could be improved.**

While error handling has been implemented correctly, the error logging could be more descriptive to provide better context for debugging.

```diff
  } catch (error) {
-    console.error(error);
+    console.error("Error fetching comments for vendor:", error);
    return [];
  }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 2f871fce3f97a14302418b353c5ab08781a83315 and 4a4f729050419b6b073167eeb3c0bb56a27978ec.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/page.tsx` (1 hunks)
* `apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/page.tsx` (6 hunks)
* `apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/page.tsx` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/page.tsx

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (2)</summary>

<details>
<summary>apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/page.tsx (2)</summary><blockquote>

<details>
<summary>apps/app/src/components/comments.tsx (1)</summary>

* `Comments` (29-103)

</details>
<details>
<summary>apps/app/src/utils/auth.ts (1)</summary>

* `auth` (11-86)

</details>

</blockquote></details>
<details>
<summary>apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/page.tsx (2)</summary><blockquote>

<details>
<summary>apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/title-and-description/title-and-description.tsx (1)</summary>

* `TitleAndDescription` (12-44)

</details>
<details>
<summary>apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/secondary-fields/secondary-fields.tsx (1)</summary>

* `SecondaryFields` (8-33)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (9)</summary><blockquote>

<details>
<summary>apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/page.tsx (4)</summary>

`1-1`: **LGTM! New comment feature properly integrated.**

The imports for React's cache utility and the Comments component are correctly added. The page now fetches comments using the new getComments function and correctly renders the Comments component with the required props.



Also applies to: 16-16, 32-32, 53-53

---

`59-83`: **Good optimization with cache implementation.**

Converting getRisk to use React's cache function is a good optimization that will prevent redundant database calls when the same risk is requested multiple times.

---

`85-121`: **Error handling in getComments function could be improved.**

While error handling has been added as suggested in the previous review, the error logging could be more descriptive to provide better context for debugging.

```diff
  } catch (error) {
-    console.error(error);
+    console.error("Error fetching comments for risk:", error);
    return [];
  }
```

---

`123-145`: **LGTM! Cache implementation for getAssignees.**

Adding cache to the getAssignees function is a good optimization that will prevent redundant database calls.

</details>
<details>
<summary>apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/page.tsx (5)</summary>

`16-20`: **LGTM! Proper imports and interface simplification.**

The imports for React's cache and Comments component are correctly added. The PageProps interface has been appropriately simplified to remove unused searchParams.

---

`23-51`: **LGTM! Clean implementation of VendorPage with comments.**

The VendorPage component has been streamlined to focus on displaying vendor details and comments, which aligns well with the PR objectives. The Comments component is properly integrated with the required props.

---

`53-77`: **Good optimization with cache implementation for getVendor.**

Converting getVendor to use React's cache function is a good optimization that will prevent redundant database calls.

---

`117-139`: **LGTM! Cache implementation for getAssignees.**

Adding cache to the getAssignees function is a good optimization that will prevent redundant database calls.

---

`141-153`: **LGTM! No functional changes to the metadata generation.**

The formatting changes to generateMetadata are minor and don't affect functionality.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-06T01:48:54Z

Review comments:
diff_hunk: @@ -0,0 +1,75 @@
+"use server";
+
+import { authActionClient } from "./safe-action";
+import { z } from "zod";
+import { db } from "@comp/db";
+import { AppError, appErrors } from "@/lib/errors";
+import { headers } from "next/headers";
+import { revalidatePath } from "next/cache";
+
+export const addCommentAction = authActionClient
+	.schema(
+		z.object({
+			content: z.string(),
+			entityId: z.string(),
+		}),
+	)
+	.metadata({
+		name: "add-comment",
+		track: {
+			event: "add-comment",
+			channel: "server",
+		},
+	})
path: apps/app/src/actions/add-comment.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Avoid duplicating the schema definition.**
This inline schema definition repeats the fields from `addCommentSchema` in `schema.ts`. Consider importing `addCommentSchema` here to keep the code DRY and centralize schema maintenance.  

```diff
-import { z } from "zod";
...
.schema(
  z.object({
    content: z.string(),
    entityId: z.string(),
  })
)
+import { addCommentSchema } from "./schema";
...
.schema(addCommentSchema)
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
import { addCommentSchema } from "./schema";

export const addCommentAction = authActionClient
	.schema(addCommentSchema)
	.metadata({
		name: "add-comment",
		track: {
			event: "add-comment",
			channel: "server",
		},
	})
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-06T01:42:08Z
author_association: NONE
diff_hunk: @@ -98,113 +80,42 @@ async function getRisk(riskId: string) {
   });
 
   return risk;
-}
+});
 
-async function getTasks({
+const getComments = cache(async ({
   riskId,
-  search,
-  status,
-  column,
-  order,
-  page = 1,
-  per_page = 10,
 }: {
   riskId: string;
-  search?: string;
-  status?: TaskStatus;
-  column?: string;
-  order?: string;
-  page?: number;
-  per_page?: number;
-}) {
+}) => {
   const session = await auth.api.getSession({
     headers: await headers(),
   });
 
   if (!session || !session.session.activeOrganizationId) {
-    return { tasks: [], total: 0 };
+    return [];
   }
 
-  const skip = (page - 1) * per_page;
-
-  const [tasks, total] = await Promise.all([
-    db.task
-      .findMany({
-        where: {
-          relatedId: riskId,
-          relatedType: "risk",
-          organizationId: session.session.activeOrganizationId,
-          AND: [
-            search
-              ? {
-                OR: [
-                  { title: { contains: search, mode: "insensitive" } },
-                  {
-                    description: { contains: search, mode: "insensitive" },
-                  },
-                ],
-              }
-              : {},
-            status ? { status } : {},
-          ],
-        },
-        orderBy: column
-          ? {
-            [column]: order === "asc" ? "asc" : "desc",
-          }
-          : {
-            createdAt: "desc",
-          },
-        skip,
-        take: per_page,
+  const comments = await db.comment.findMany({
+    where: {
+      entityId: riskId,
+      organizationId: session.session.activeOrganizationId,
+    },
+    include: {
+      author: {
         include: {
-          assignee: {
-            include: {
-              user: true,
-            },
-          },
+          user: true,
         },
-      })
-      .then((tasks) =>
-        tasks.map(
-          (task): RiskTaskType => ({
-            id: task.id,
-            riskId: task.relatedId,
-            title: task.title,
-            status: task.status,
-            dueDate: task.dueDate.toISOString(),
-            assigneeId: task.assigneeId ?? "",
-            assignee: {
-              name: task.assignee?.user.name ?? "",
-              image: task.assignee?.user.image ?? "",
-            },
-          }),
-        ),
-      ),
-    db.task.count({
-      where: {
-        relatedId: riskId,
-        relatedType: "risk",
-        organizationId: session.session.activeOrganizationId,
-        AND: [
-          search
-            ? {
-              OR: [
-                { title: { contains: search, mode: "insensitive" } },
-                { description: { contains: search, mode: "insensitive" } },
-              ],
-            }
-            : {},
-          status ? { status } : {},
-        ],
       },
-    }),
-  ]);
+    },
+    orderBy: {
+      createdAt: "desc",
+    },
+  });
 
-  return { tasks, total };
-}
+  return comments;
+});
path: apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/page.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling to the getComments function.**

The getComments function lacks error handling, which could lead to unexpected behavior if database queries fail. Consider adding try/catch blocks to handle potential errors gracefully.

```diff
const getComments = cache(async ({
  riskId,
}: {
  riskId: string;
}) => {
  const session = await auth.api.getSession({
    headers: await headers(),
  });

  if (!session || !session.session.activeOrganizationId) {
    return [];
  }

+  try {
    const comments = await db.comment.findMany({
      where: {
        entityId: riskId,
        organizationId: session.session.activeOrganizationId,
      },
      include: {
        author: {
          include: {
            user: true,
          },
        },
      },
      orderBy: {
        createdAt: "desc",
      },
    });

    return comments;
+  } catch (error) {
+    console.error("Error fetching comments:", error);
+    return [];
+  }
});
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
const getComments = cache(async ({
  riskId,
}: {
  riskId: string;
}) => {
  const session = await auth.api.getSession({
    headers: await headers(),
  });

  if (!session || !session.session.activeOrganizationId) {
    return [];
  }

  try {
    const comments = await db.comment.findMany({
      where: {
        entityId: riskId,
        organizationId: session.session.activeOrganizationId,
      },
      include: {
        author: {
          include: {
            user: true,
          },
        },
      },
      orderBy: {
        createdAt: "desc",
      },
    });

    return comments;
  } catch (error) {
    console.error("Error fetching comments:", error);
    return [];
  }
});
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 4a4f729
created_at: 2025-04-06T01:42:09Z
author_association: NONE

PR comments:
user: login: vercel[bot]
created_at: 2025-04-06T01:36:17Z
body: [vc]: #DGZsM/M7qNjWWo5IgZrsEB7ZbArv/bmByG5gVAFVOoU=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJhcHAiLCJyb290RGlyZWN0b3J5IjoiYXBwcy9hcHAiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vY29tcGFpL2FwcC9IeTN6SlR1RHA4Q3RYdW1uZVJwY2lLaEVSdjhmIiwicHJldmlld1VybCI6ImFwcC1naXQtbGV3aXMtY29tbW9uLWNvbW1lbnRzLWNvbXBhaS52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImFwcC1naXQtbGV3aXMtY29tbW9uLWNvbW1lbnRzLWNvbXBhaS52ZXJjZWwuYXBwIn19LHsibmFtZSI6ImNvbXAtcG9ydGFsIiwicm9vdERpcmVjdG9yeSI6ImFwcHMvcG9ydGFsIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vY29tcGFpL2NvbXAtcG9ydGFsLzczeEtzOGdKWFRRSmsxVGlOdzFpNE16a3AyZ1oiLCJwcmV2aWV3VXJsIjoiIiwibmV4dENvbW1pdFN0YXR1cyI6IlNLSVBQRUQifV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **app** | ✅ Ready ([Inspect](https://vercel.com/compai/app/Hy3zJTuDp8CtXumneRpciKhERv8f)) | [Visit Preview](https://app-git-lewis-common-comments-compai.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/app-git-lewis-common-comments-compai.vercel.app?via=pr-comment-feedback-link) | Apr 6, 2025 1:50am |

<details><summary>1 Skipped Deployment</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **comp-portal** | ⬜️ Skipped ([Inspect](https://vercel.com/compai/comp-portal/73xKs8gJXTQJk1TiNw1i4Mzkp2gZ)) |  |  | Apr 6, 2025 1:50am |
</details>



author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-04-06T01:36:22Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
This pull request introduces a comment management feature across the application. It adds a new action (`addCommentAction`) and a corresponding schema (`addCommentSchema`) for comment validation. Changes include new UI components for displaying and submitting comments, modifications to policy, risk, and vendor pages to fetch and render comments, and updates to the Prisma schema and migrations to support a new Comment model. The PR also updates caching and error handling logic, and removes unused session and navigation logic in certain components.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `apps/app/src/actions/add-comment.ts`<br>`apps/app/src/actions/schema.ts` | Added `addCommentAction` and `addCommentSchema`; improved formatting in existing schema definitions. |
| `apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/[policyId]/page.tsx`<br>`apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/page.tsx`<br>`apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/page.tsx` | Integrated comment fetching and display by adding `getComments` functions, updating caching for data retrieval, removing obsolete task/search logic, and incorporating the `Comments` component. |
| `apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/components/create-vendor-form.tsx` | Removed session management and router navigation logic; simplified vendor creation success flow by eliminating unused variables. |
| `apps/app/src/components/comments.tsx`<br>`apps/app/src/components/forms/create-comment-form.tsx` | Introduced new UI components: `Comments` for displaying a list of comments and `CreateCommentForm` for submitting comments using `addCommentSchema` and `useAction`. |
| `packages/db/prisma/migrations/20250406004306_added_common_comment_table/migration.sql`<br>`packages/db/prisma/schema/auth.prisma`<br>`packages/db/prisma/schema/common.prisma`<br>`packages/db/prisma/schema/organization.prisma` | Added a new `Comment` table via SQL migration; updated Prisma schemas to define the `Comment` model and its relationships with `Member` and `Organization`. |

## Sequence Diagram(s)
```mermaid
sequenceDiagram
    participant U as User
    participant F as CreateCommentForm
    participant A as addCommentAction
    participant DB as Database
    participant R as Revalidation

    U->>F: Enter comment and submit
    F->>A: Send comment data + entityId
    A->>A: Validate session and organization
    A->>DB: Insert new comment record
    DB-->>A: Return confirmation or error
    A->>R: Trigger cache revalidation
    A-->>F: Return created comment data or error
    F-->>U: Display success/error message
```

## Possibly related PRs
- **trycompai/comp#271:** Introduced the new `CreateCommentForm` component that utilizes the `addCommentSchema`, aligning with the new comment action functionality.  
- **trycompai/comp#153:** Related changes involve the management of comments, specifically through the addition of new comment-related structures in the database schema.  
- **trycompai/comp#273:** Changes include the handling of comments through the new `Comments` component and the `CreateCommentForm`, designed to work with the comment functionality introduced in the main PR.

## Poem
> I’m a rabbit with paws so fleet,  
> Hopping through code on a rapid beat.  
> Comments now blossom in every file,  
> From schema to UI, making us smile.  
> I nibble on changes, both swift and neat,  
> Celebrating code with a joyful tweet!  
> 🐇🥕 Happy coding!

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=trycompai/comp&utm_content=307):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: anamhira47
created_at: 2025-04-06T21:32:46Z
body: Elite PR 🙂
author_association: NONE