diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/actions/getFrameworkCategoriesAction.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/actions/getFrameworkCategoriesAction.ts
deleted file mode 100644
index dcb62c2eca..0000000000
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/actions/getFrameworkCategoriesAction.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-"use server";
-
-import type { TransformedCategory } from "@/types/framework";
-import { db } from "@bubba/db";
-import { z } from "zod";
-import { authActionClient } from "@/actions/safe-action";
-
-const getCategoriesSchema = z.object({
- frameworkId: z.string(),
-});
-
-export const getFrameworkCategoriesAction = authActionClient
- .schema(getCategoriesSchema)
- .action(async ({ parsedInput, ctx }) => {
- const { frameworkId } = parsedInput;
- const { user } = ctx;
-
- if (!user.organizationId) {
- return {
- success: false,
- error: "No organization found",
- };
- }
-
- try {
- const categories = await db.frameworkCategory.findMany({
- where: {
- frameworkId,
- },
- include: {
- controls: {
- include: {
- organizationControls: {
- where: {
- organizationId: user.organizationId,
- },
- include: {
- artifacts: true,
- },
- },
- requirements: true,
- },
- },
- },
- });
-
- const transformedCategories: TransformedCategory[] = categories.map(
- (category) => ({
- ...category,
- controls: category.controls.map((control) => ({
- id: control.id,
- name: control.name,
- code: control.code,
- description: control.description,
- domain: control.domain,
- frameworkCategoryId: control.frameworkCategoryId,
- status: control.organizationControls[0]?.status || "not_started",
- artifacts: control.organizationControls[0]?.artifacts || [],
- requiredArtifactTypes:
- control.requirements?.map((req) => req.type) || [],
- })),
- })
- );
-
- return {
- success: true,
- data: transformedCategories,
- };
- } catch (error) {
- console.error("Error fetching framework categories:", error);
- return {
- success: false,
- error: "Failed to fetch framework categories",
- };
- }
- });
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/FrameworkControls.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/FrameworkControls.tsx
index 308d726b58..7139e0e1ad 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/FrameworkControls.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/FrameworkControls.tsx
@@ -1,31 +1,32 @@
"use client";
-import { FrameworkControlsTable } from "./table/FrameworkControlsTable";
-import { useOrganizationCategories } from "../hooks/useOrganizationCategories";
import { useMemo } from "react";
+import type { FrameworkCategories } from "../data/getFrameworkCategories";
+import { FrameworkControlsTable } from "./table/FrameworkControlsTable";
import type { OrganizationControlType } from "./table/FrameworkControlsTableColumns";
-interface FrameworkControlsProps {
+export type FrameworkControlsProps = {
+ organizationCategories: FrameworkCategories;
frameworkId: string;
-}
-
-export function FrameworkControls({ frameworkId }: FrameworkControlsProps) {
- const { data: organizationCategories } =
- useOrganizationCategories(frameworkId);
+};
+export function FrameworkControls({
+ organizationCategories,
+ frameworkId,
+}: FrameworkControlsProps) {
const allControls = useMemo(() => {
if (!organizationCategories) return [];
return organizationCategories.flatMap((category) =>
- category.organizationControl.map((control) => ({
- code: control.control.code,
- description: control.control.description,
- name: control.control.name,
- status: control.status,
- id: control.id,
+ category.organizationControl.map((orgControl) => ({
+ code: orgControl.control.code,
+ description: orgControl.control.description,
+ name: orgControl.control.name,
+ status: orgControl.status,
+ id: orgControl.id,
frameworkId,
category: category.name,
- requirements: control.OrganizationControlRequirement,
+ requirements: orgControl.OrganizationControlRequirement,
})),
);
}, [organizationCategories, frameworkId]) as OrganizationControlType[];
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/FrameworkOverview.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/FrameworkOverview.tsx
index 9701b3f8a9..7c45b0520b 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/FrameworkOverview.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/FrameworkOverview.tsx
@@ -1,51 +1,87 @@
"use client";
+import type { Framework, OrganizationFramework } from "@bubba/db/types";
import { Badge } from "@bubba/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card";
import { Progress } from "@bubba/ui/progress";
import { format } from "date-fns";
import { CalendarIcon } from "lucide-react";
-import { useOrganizationCategories } from "../hooks/useOrganizationCategories";
-import { useOrganizationFramework } from "../hooks/useOrganizationFramework";
-
+import type { FrameworkCategories } from "../data/getFrameworkCategories";
interface FrameworkOverviewProps {
- frameworkId: string;
+ organizationCategories: FrameworkCategories;
+ organizationFramework: OrganizationFramework & { framework: Framework };
}
-export function FrameworkOverview({ frameworkId }: FrameworkOverviewProps) {
- const { data } = useOrganizationCategories(frameworkId);
- const { data: framework } = useOrganizationFramework(frameworkId);
+export function FrameworkOverview({
+ organizationCategories,
+ organizationFramework,
+}: FrameworkOverviewProps) {
+ const requirements = organizationCategories.flatMap((category) =>
+ category.organizationControl.flatMap(
+ (control) => control.OrganizationControlRequirement,
+ ),
+ );
+
+ const totalRequirements = requirements.length;
+ const completedRequirements = requirements.filter((req) => {
+ switch (req.type) {
+ case "policy":
+ return req.organizationPolicy?.status === "published";
+ case "file":
+ return !!req.fileUrl;
+ case "evidence":
+ return req.organizationEvidence?.published === true;
+ default:
+ return req.published;
+ }
+ }).length;
// Calculate compliance metrics
- const totalControls = data?.reduce(
- (acc, cat) => acc + cat.organizationControl.length,
- 0,
- );
+ const compliancePercentage =
+ totalRequirements > 0
+ ? Math.round((completedRequirements / totalRequirements) * 100)
+ : 0;
- const compliantControls = data?.reduce(
- (acc, cat) =>
- acc +
- cat.organizationControl.filter((oc) => oc.status === "compliant").length,
- 0,
+ // Count controls
+ const allControls = organizationCategories.flatMap(
+ (category) => category.organizationControl,
);
+ const totalControls = allControls.length;
- const compliancePercentage = Math.round(
- (compliantControls ?? 0 / (totalControls ?? 0)) * 100,
- );
+ // Calculate compliant controls (all requirements completed)
+ const compliantControls = allControls.filter((control) => {
+ const controlRequirements = control.OrganizationControlRequirement;
+ if (controlRequirements.length === 0) return false;
+
+ const completedControlRequirements = controlRequirements.filter((req) => {
+ switch (req.type) {
+ case "policy":
+ return req.organizationPolicy?.status === "published";
+ case "file":
+ return !!req.fileUrl;
+ case "evidence":
+ return req.organizationEvidence?.published === true;
+ default:
+ return req.published;
+ }
+ }).length;
+
+ return completedControlRequirements === controlRequirements.length;
+ }).length;
return (
- {framework?.framework.name}
+ {organizationFramework?.framework.name}
- {framework?.framework.description}
+ {organizationFramework?.framework.description}
- Version {framework?.framework.version}
+ Version {organizationFramework?.framework.version}
@@ -75,8 +111,8 @@ export function FrameworkOverview({ frameworkId }: FrameworkOverviewProps) {
Last assessed:{" "}
- {framework?.lastAssessed
- ? format(framework?.lastAssessed, "MMM d, yyyy")
+ {organizationFramework?.lastAssessed
+ ? format(organizationFramework?.lastAssessed, "MMM d, yyyy")
: "Never"}
@@ -84,8 +120,8 @@ export function FrameworkOverview({ frameworkId }: FrameworkOverviewProps) {
Next assessment:{" "}
- {framework?.nextAssessment
- ? format(framework?.nextAssessment, "MMM d, yyyy")
+ {organizationFramework?.nextAssessment
+ ? format(organizationFramework?.nextAssessment, "MMM d, yyyy")
: "Not scheduled"}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTableColumns.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTableColumns.tsx
index e7b131ba7d..b6972ad789 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTableColumns.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTableColumns.tsx
@@ -18,6 +18,7 @@ import {
TooltipTrigger,
} from "@bubba/ui/tooltip";
import { useParams } from "next/navigation";
+import { getControlStatus } from "../../lib/utils";
export type OrganizationControlType = {
code: string;
description: string | null;
@@ -36,30 +37,6 @@ export type OrganizationControlType = {
})[];
};
-function getControlStatus(
- requirements: OrganizationControlType["requirements"],
-): StatusType {
- if (!requirements || requirements.length === 0) return "not_started";
-
- const totalRequirements = requirements.length;
- const completedRequirements = requirements.filter((req) => {
- switch (req.type) {
- case "policy":
- return req.organizationPolicy?.status === "published";
- case "file":
- return !!req.fileUrl;
- case "evidence":
- return req.organizationEvidence?.published === true;
- default:
- return req.published;
- }
- }).length;
-
- if (completedRequirements === 0) return "not_started";
- if (completedRequirements === totalRequirements) return "completed";
- return "in_progress";
-}
-
export function FrameworkControlsTableColumns(): ColumnDef[] {
const t = useI18n();
const { orgId } = useParams<{ orgId: string }>();
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/data/getFramework.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/data/getFramework.ts
new file mode 100644
index 0000000000..6e64ab3839
--- /dev/null
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/data/getFramework.ts
@@ -0,0 +1,21 @@
+import { db } from "@bubba/db";
+
+export const getFramework = async (
+ frameworkId: string,
+ organizationId: string
+) => {
+ const framework = await db.organizationFramework.findUnique({
+ where: {
+ organizationId_frameworkId: {
+ organizationId,
+ frameworkId,
+ },
+ },
+ include: {
+ framework: true,
+ organizationControl: true,
+ },
+ });
+
+ return framework;
+};
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/data/getFrameworkCategories.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/data/getFrameworkCategories.ts
new file mode 100644
index 0000000000..302a3021bb
--- /dev/null
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/data/getFrameworkCategories.ts
@@ -0,0 +1,49 @@
+import { db } from "@bubba/db";
+import type {
+ OrganizationControl,
+ OrganizationControlRequirement,
+ Control,
+ OrganizationCategory,
+ OrganizationPolicy,
+ OrganizationEvidence,
+} from "@bubba/db/types";
+
+export type FrameworkCategories = (OrganizationCategory & {
+ name: string;
+ organizationControl: (OrganizationControl & {
+ id: string;
+ status: any; // ComplianceStatus enum
+ control: Control;
+ OrganizationControlRequirement: (OrganizationControlRequirement & {
+ organizationPolicy: OrganizationPolicy | null;
+ organizationEvidence: OrganizationEvidence | null;
+ })[];
+ })[];
+})[];
+
+export const getFrameworkCategories = async (
+ frameworkId: string,
+ organizationId: string
+) => {
+ const organizationCategories = await db.organizationCategory.findMany({
+ where: {
+ organizationId,
+ frameworkId,
+ },
+ include: {
+ organizationControl: {
+ include: {
+ control: true,
+ OrganizationControlRequirement: {
+ include: {
+ organizationPolicy: true,
+ organizationEvidence: true,
+ },
+ },
+ },
+ },
+ },
+ });
+
+ return organizationCategories;
+};
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/lib/utils.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/lib/utils.ts
new file mode 100644
index 0000000000..a4926a0fb1
--- /dev/null
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/lib/utils.ts
@@ -0,0 +1,26 @@
+import type { OrganizationControlType } from "../components/table/FrameworkControlsTableColumns";
+import type { StatusType } from "@/components/frameworks/framework-status";
+
+export function getControlStatus(
+ requirements: OrganizationControlType["requirements"]
+): StatusType {
+ if (!requirements || requirements.length === 0) return "not_started";
+
+ const totalRequirements = requirements.length;
+ const completedRequirements = requirements.filter((req) => {
+ switch (req.type) {
+ case "policy":
+ return req.organizationPolicy?.status === "published";
+ case "file":
+ return !!req.fileUrl;
+ case "evidence":
+ return req.organizationEvidence?.published === true;
+ default:
+ return req.published;
+ }
+ }).length;
+
+ if (completedRequirements === 0) return "not_started";
+ if (completedRequirements === totalRequirements) return "completed";
+ return "in_progress";
+}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/page.tsx
index 9fd0ed1385..9a7392b2b6 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/page.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/page.tsx
@@ -1,8 +1,10 @@
+import { auth } from "@/auth";
import { setStaticParamsLocale } from "next-international/server";
import { redirect } from "next/navigation";
-import { FrameworkOverview } from "./components/FrameworkOverview";
import { FrameworkControls } from "./components/FrameworkControls";
-
+import { FrameworkOverview } from "./components/FrameworkOverview";
+import { getFramework } from "./data/getFramework";
+import { getFrameworkCategories } from "./data/getFrameworkCategories";
interface PageProps {
params: Promise<{
frameworkId: string;
@@ -14,14 +16,43 @@ export default async function FrameworkPage({ params }: PageProps) {
const { frameworkId, locale } = await params;
setStaticParamsLocale(locale);
+ const session = await auth();
+
+ if (!session) {
+ redirect("/");
+ }
+
+ const organizationId = session.user.organizationId;
+
if (!frameworkId) {
redirect("/");
}
+ if (!organizationId) {
+ redirect("/");
+ }
+
+ const organizationCategories = await getFrameworkCategories(
+ frameworkId,
+ organizationId,
+ );
+
+ const organizationFramework = await getFramework(frameworkId, organizationId);
+
+ if (!organizationFramework) {
+ redirect("/");
+ }
+
return (
-
-
+
+
);
}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/actions/getOrganizationControl.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/actions/getOrganizationControl.ts
deleted file mode 100644
index 2774f87db8..0000000000
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/actions/getOrganizationControl.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-"use server";
-
-import { authActionClient } from "@/actions/safe-action";
-import { db } from "@bubba/db";
-import { z } from "zod";
-
-export const getOrganizationControl = authActionClient
- .schema(z.object({ controlId: z.string() }))
- .metadata({
- name: "getOrganizationControl",
- track: {
- event: "get-organization-control",
- channel: "server",
- },
- })
- .action(async ({ ctx, parsedInput }) => {
- const { user } = ctx;
- const { controlId } = parsedInput;
-
- if (!user.organizationId) {
- return {
- error: "Not authorized - no organization found",
- };
- }
-
- try {
- const organizationControl = await db.organizationControl.findUnique({
- where: {
- organizationId: user.organizationId,
- id: controlId,
- },
- include: {
- control: true,
- OrganizationControlRequirement: {
- include: {
- organizationPolicy: true,
- organizationEvidence: true,
- },
- },
- },
- });
-
- if (!organizationControl) {
- return {
- error: "Organization control not found",
- };
- }
-
- return {
- data: {
- organizationControl,
- },
- };
- } catch (error) {
- console.error("Error fetching organization control:", error);
- return {
- error: "Failed to fetch organization control",
- };
- }
- });
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/actions/getOrganizationControlProgress.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/actions/getOrganizationControlProgress.ts
deleted file mode 100644
index 2c185c7c7b..0000000000
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/actions/getOrganizationControlProgress.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-"use server";
-
-import { authActionClient } from "@/actions/safe-action";
-import { db } from "@bubba/db";
-import { z } from "zod";
-
-export interface ControlProgressResponse {
- total: number;
- completed: number;
- progress: number;
- byType: {
- [key: string]: {
- total: number;
- completed: number;
- };
- };
-}
-
-export const getOrganizationControlProgress = authActionClient
- .schema(z.object({ controlId: z.string() }))
- .metadata({
- name: "getOrganizationControlProgress",
- track: {
- event: "get-organization-control-progress",
- channel: "server",
- },
- })
- .action(async ({ ctx, parsedInput }) => {
- const { user } = ctx;
- const { controlId } = parsedInput;
-
- if (!user.organizationId) {
- return {
- error: "Not authorized - no organization found",
- };
- }
-
- try {
- const requirements = await db.organizationControlRequirement.findMany({
- where: {
- organizationControlId: controlId,
- },
- include: {
- organizationPolicy: true,
- organizationEvidence: true,
- },
- });
-
- const progress: ControlProgressResponse = {
- total: requirements.length,
- completed: 0,
- progress: 0,
- byType: {},
- };
-
- for (const requirement of requirements) {
- // Initialize type counters if not exists
- if (!progress.byType[requirement.type]) {
- progress.byType[requirement.type] = {
- total: 0,
- completed: 0,
- };
- }
-
- progress.byType[requirement.type].total++;
-
- // Check completion based on requirement type
- let isCompleted = false;
- switch (requirement.type) {
- case "policy":
- isCompleted =
- requirement.organizationPolicy?.status === "published";
- break;
- case "file":
- isCompleted = !!requirement.fileUrl;
- break;
- case "evidence":
- isCompleted = requirement.organizationEvidence?.published === true;
- break;
- default:
- isCompleted = requirement.published;
- }
-
- if (isCompleted) {
- progress.completed++;
- progress.byType[requirement.type].completed++;
- }
- }
-
- // Calculate overall progress percentage
- progress.progress =
- progress.total > 0
- ? Math.round((progress.completed / progress.total) * 100)
- : 0;
-
- return {
- data: {
- progress,
- },
- };
- } catch (error) {
- console.error("Error fetching control progress:", error);
- return {
- error: "Failed to fetch control progress",
- };
- }
- });
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/actions/getOrganizationControlRequirements.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/actions/getOrganizationControlRequirements.ts
deleted file mode 100644
index 51762b52cf..0000000000
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/actions/getOrganizationControlRequirements.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-"use server";
-
-import { authActionClient } from "@/actions/safe-action";
-import { db } from "@bubba/db";
-import { z } from "zod";
-
-export const getOrganizationControlRequirements = authActionClient
- .schema(z.object({ controlId: z.string() }))
- .metadata({
- name: "getOrganizationControlRequirements",
- track: {
- event: "get-organization-control-requirements",
- channel: "server",
- },
- })
- .action(async ({ ctx, parsedInput }) => {
- const { user } = ctx;
- const { controlId } = parsedInput;
-
- if (!user.organizationId) {
- return {
- error: "Not authorized - no organization found",
- };
- }
-
- try {
- const organizationControlRequirements =
- await db.organizationControlRequirement.findMany({
- where: {
- organizationControlId: controlId,
- },
- include: {
- organizationControl: {
- include: {
- control: true,
- },
- },
- controlRequirement: {
- include: {
- evidence: true,
- },
- },
- organizationPolicy: {
- include: {
- policy: true,
- },
- },
- organizationEvidence: true,
- },
- });
-
- if (!organizationControlRequirements) {
- return {
- error: "Organization control requirements not found",
- };
- }
-
- return {
- data: {
- organizationControlRequirements,
- },
- };
- } catch (error) {
- console.error("Error fetching organization control:", error);
- return {
- error: "Failed to fetch organization control",
- };
- }
- });
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/SingleControl.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/SingleControl.tsx
index 5b89aa0e90..6f0d03d0c2 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/SingleControl.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/SingleControl.tsx
@@ -10,7 +10,7 @@ import type {
} from "@bubba/db/types";
import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card";
import { useMemo } from "react";
-import { useOrganizationControlProgress } from "../hooks/useOrganizationControlProgress";
+import type { ControlProgressResponse } from "../data/getOrganizationControlProgress";
import { SingleControlSkeleton } from "./SingleControlSkeleton";
import { ControlRequirementsTable } from "./table/ControlRequirementsTable";
@@ -22,23 +22,25 @@ interface SingleControlProps {
organizationEvidence: OrganizationEvidence | null;
})[];
};
+ organizationControlProgress: ControlProgressResponse;
}
-export const SingleControl = ({ organizationControl }: SingleControlProps) => {
- const { data: controlProgress, isLoading: isControlProgressLoading } =
- useOrganizationControlProgress(organizationControl.id);
-
+export const SingleControl = ({
+ organizationControl,
+ organizationControlProgress,
+}: SingleControlProps) => {
const progressStatus = useMemo(() => {
- if (!controlProgress) return "not_started";
+ if (!organizationControlProgress) return "not_started";
- return controlProgress.progress?.completed > 0
- ? "in_progress"
- : controlProgress.progress?.completed === 0
- ? "not_started"
- : "completed";
- }, [controlProgress]);
+ return organizationControlProgress.total ===
+ organizationControlProgress.completed
+ ? "completed"
+ : organizationControlProgress.completed > 0
+ ? "in_progress"
+ : "not_started";
+ }, [organizationControlProgress]);
- if (!organizationControl || (!controlProgress && isControlProgressLoading)) {
+ if (!organizationControl || !organizationControlProgress) {
return ;
}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/table/ControlRequirementsTable.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/table/ControlRequirementsTable.tsx
index 0ae842f9f2..af6c6ac1d7 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/table/ControlRequirementsTable.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/table/ControlRequirementsTable.tsx
@@ -37,9 +37,6 @@ export function ControlRequirementsTable({ data }: DataTableProps) {
});
const onRowClick = (requirement: RequirementTableData) => {
- console.log({
- requirement,
- });
switch (requirement.type) {
case "policy":
if (requirement.organizationPolicyId) {
@@ -49,9 +46,6 @@ export function ControlRequirementsTable({ data }: DataTableProps) {
}
break;
case "evidence":
- console.log({
- requirement,
- });
if (requirement.organizationEvidenceId) {
router.push(
`/${orgId}/evidence/${requirement.organizationEvidenceId}`,
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/data/getControl.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/data/getControl.ts
new file mode 100644
index 0000000000..e0148889b2
--- /dev/null
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/data/getControl.ts
@@ -0,0 +1,30 @@
+import { auth } from "@/auth";
+import { db } from "@bubba/db";
+
+export const getControl = async (id: string) => {
+ const session = await auth();
+
+ if (!session) {
+ return {
+ error: "Unauthorized",
+ };
+ }
+
+ const organizationControl = await db.organizationControl.findUnique({
+ where: {
+ organizationId: session.user.organizationId,
+ id,
+ },
+ include: {
+ control: true,
+ OrganizationControlRequirement: {
+ include: {
+ organizationPolicy: true,
+ organizationEvidence: true,
+ },
+ },
+ },
+ });
+
+ return organizationControl;
+};
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/data/getOrganizationControlProgress.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/data/getOrganizationControlProgress.ts
new file mode 100644
index 0000000000..ed89c7c383
--- /dev/null
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/data/getOrganizationControlProgress.ts
@@ -0,0 +1,88 @@
+"use server";
+
+import { auth } from "@/auth";
+import { db } from "@bubba/db";
+
+export interface ControlProgressResponse {
+ total: number;
+ completed: number;
+ progress: number;
+ byType: {
+ [key: string]: {
+ total: number;
+ completed: number;
+ };
+ };
+}
+
+export const getOrganizationControlProgress = async (controlId: string) => {
+ const session = await auth();
+
+ if (!session) {
+ return {
+ error: "Unauthorized",
+ };
+ }
+
+ const requirements = await db.organizationControlRequirement.findMany({
+ where: {
+ organizationControlId: controlId,
+ },
+ include: {
+ organizationPolicy: true,
+ organizationEvidence: true,
+ },
+ });
+
+ const progress: ControlProgressResponse = {
+ total: requirements.length,
+ completed: 0,
+ progress: 0,
+ byType: {},
+ };
+
+ for (const requirement of requirements) {
+ // Initialize type counters if not exists
+ if (!progress.byType[requirement.type]) {
+ progress.byType[requirement.type] = {
+ total: 0,
+ completed: 0,
+ };
+ }
+
+ progress.byType[requirement.type].total++;
+
+ // Check completion based on requirement type
+ let isCompleted = false;
+ switch (requirement.type) {
+ case "policy":
+ isCompleted = requirement.organizationPolicy?.status === "published";
+ break;
+ case "file":
+ isCompleted = !!requirement.fileUrl;
+ break;
+ case "evidence":
+ isCompleted = requirement.organizationEvidence?.published ?? false;
+ break;
+ default:
+ isCompleted = requirement.published;
+ }
+
+ if (isCompleted) {
+ progress.completed++;
+ progress.byType[requirement.type].completed++;
+ }
+ }
+
+ // Calculate overall progress percentage
+ progress.progress =
+ progress.total > 0
+ ? Math.round((progress.completed / progress.total) * 100)
+ : 0;
+
+ return {
+ data: {
+ progress,
+ },
+ };
+};
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/hooks/useOrganizationControl.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/hooks/useOrganizationControl.ts
deleted file mode 100644
index 8721f3dc58..0000000000
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/hooks/useOrganizationControl.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-"use client";
-
-import useSWR from "swr";
-import { getOrganizationControl } from "../actions/getOrganizationControl";
-
-async function fetchOrganizationControl(controlId: string) {
- const result = await getOrganizationControl({ controlId });
-
- if (!result) {
- throw new Error("Failed to fetch control");
- }
-
- const data = result.data?.data;
- if (!data) {
- throw new Error("Invalid response from server");
- }
-
- return data;
-}
-
-export function useOrganizationControl(controlId: string) {
- const { data, error, isLoading, mutate } = useSWR(
- ["organization-control", controlId],
- () => fetchOrganizationControl(controlId),
- {
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- },
- );
-
- return {
- data: data?.organizationControl,
- isLoading,
- error,
- mutate,
- };
-}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/hooks/useOrganizationControlProgress.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/hooks/useOrganizationControlProgress.ts
deleted file mode 100644
index 787753cfea..0000000000
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/hooks/useOrganizationControlProgress.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-"use client";
-
-import useSWR from "swr";
-import { getOrganizationControlProgress } from "../actions/getOrganizationControlProgress";
-
-async function fetchOrganizationControlProgress(controlId: string) {
- const result = await getOrganizationControlProgress({ controlId });
-
- if (!result || "error" in result || !result.data) {
- throw new Error("Failed to fetch control progress");
- }
-
- return result.data.data;
-}
-
-export function useOrganizationControlProgress(controlId: string) {
- const { data, error, isLoading, mutate } = useSWR(
- ["organization-control-progress", controlId],
- () => fetchOrganizationControlProgress(controlId),
- {
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- },
- );
-
- return {
- data,
- isLoading,
- error,
- mutate,
- };
-}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/hooks/useOrganizationControlRequirements.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/hooks/useOrganizationControlRequirements.ts
deleted file mode 100644
index 6f37eb5d8b..0000000000
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/hooks/useOrganizationControlRequirements.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-"use client";
-
-import useSWR from "swr";
-import { getOrganizationControlRequirements } from "../actions/getOrganizationControlRequirements";
-
-async function fetchOrganizationControlRequirements(controlId: string) {
- const result = await getOrganizationControlRequirements({ controlId });
-
- if (!result) {
- throw new Error("Failed to fetch control");
- }
-
- const data = result.data?.data;
- if (!data) {
- throw new Error("Invalid response from server");
- }
-
- return data.organizationControlRequirements;
-}
-
-export function useOrganizationControlRequirements(controlId: string) {
- const { data, error, isLoading, mutate } = useSWR(
- ["organization-control-requirements", controlId],
- () => fetchOrganizationControlRequirements(controlId),
- {
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- },
- );
-
- return {
- data,
- isLoading,
- error,
- mutate,
- };
-}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/page.tsx
index 97f18c0c60..35bc1e5cab 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/page.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/page.tsx
@@ -1,7 +1,9 @@
-import { db } from "@bubba/db";
-import { SingleControl } from "./components/SingleControl";
import { auth } from "@/auth";
import { redirect } from "next/navigation";
+import { SingleControl } from "./components/SingleControl";
+import { getControl } from "./data/getControl";
+import { getOrganizationControlProgress } from "./data/getOrganizationControlProgress";
+import type { ControlProgressResponse } from "./data/getOrganizationControlProgress";
interface PageProps {
params: Promise<{ id: string }>;
@@ -16,31 +18,30 @@ export default async function SingleControlPage({ params }: PageProps) {
redirect("/");
}
- const organizationControl = await getControl(id, session.user.organizationId);
+ const organizationControlResult = await getControl(id);
- if (!organizationControl) {
+ // If we get an error or no result, redirect
+ if (!organizationControlResult || "error" in organizationControlResult) {
redirect("/");
}
- return ;
+ const organizationControlProgressResult =
+ await getOrganizationControlProgress(id);
+
+ // Extract the progress data from the result or create default data if there's an error
+ const progressData: ControlProgressResponse = ("data" in
+ (organizationControlProgressResult || {}) &&
+ organizationControlProgressResult?.data?.progress) || {
+ total: 0,
+ completed: 0,
+ progress: 0,
+ byType: {},
+ };
+
+ return (
+
+ );
}
-
-const getControl = async (id: string, organizationId: string) => {
- const organizationControl = await db.organizationControl.findUnique({
- where: {
- organizationId,
- id,
- },
- include: {
- control: true,
- OrganizationControlRequirement: {
- include: {
- organizationPolicy: true,
- organizationEvidence: true,
- },
- },
- },
- });
-
- return organizationControl;
-};