From 12f8e52c7c61cf94ff79d0097d8bf9df27876f93 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Fri, 21 Mar 2025 18:16:32 -0400 Subject: [PATCH 1/3] framework overview SSR and progress bar on overview/frameworks/[frameworkId] page --- .../components/FrameworkControls.tsx | 37 ++++++-- .../components/FrameworkOverview.tsx | 88 +++++++++++++------ .../table/FrameworkControlsTableColumns.tsx | 25 +----- .../[frameworkId]/data/getFramework.ts | 21 +++++ .../data/getFrameworkCategories.ts | 36 ++++++++ .../frameworks/[frameworkId]/lib/utils.ts | 26 ++++++ .../frameworks/[frameworkId]/page.tsx | 39 +++++++- 7 files changed, 210 insertions(+), 62 deletions(-) create mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/data/getFramework.ts create mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/data/getFrameworkCategories.ts create mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/lib/utils.ts 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 308d726b5..58f6c5a9d 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,18 +1,39 @@ "use client"; -import { FrameworkControlsTable } from "./table/FrameworkControlsTable"; -import { useOrganizationCategories } from "../hooks/useOrganizationCategories"; +import type { + Control, + OrganizationCategory, + OrganizationControl, + OrganizationControlRequirement, + OrganizationPolicy, + OrganizationEvidence, +} from "@bubba/db/types"; import { useMemo } from "react"; +import { FrameworkControlsTable } from "./table/FrameworkControlsTable"; import type { OrganizationControlType } from "./table/FrameworkControlsTableColumns"; -interface FrameworkControlsProps { - frameworkId: string; -} +export type FrameworkCategory = OrganizationCategory & { + organizationControl: (OrganizationControl & { + control: Control; + OrganizationControlRequirement: (OrganizationControlRequirement & { + organizationPolicy: OrganizationPolicy; + organizationEvidence: OrganizationEvidence; + })[]; + })[]; +}; -export function FrameworkControls({ frameworkId }: FrameworkControlsProps) { - const { data: organizationCategories } = - useOrganizationCategories(frameworkId); +export type FrameworkControlsProps = { + organizationCategories: FrameworkCategory[]; + frameworkId: string; +}; +export function FrameworkControls({ + organizationCategories, + frameworkId, +}: { + organizationCategories: FrameworkCategory[]; + frameworkId: string; +}) { const allControls = useMemo(() => { if (!organizationCategories) return []; 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 9701b3f8a..49485f0ff 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 { FrameworkCategory } from "./FrameworkControls"; interface FrameworkOverviewProps { - frameworkId: string; + organizationCategories: FrameworkCategory[]; + 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 e7b131ba7..b6972ad78 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 000000000..6e64ab383 --- /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 000000000..1f1a12bbe --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/data/getFrameworkCategories.ts @@ -0,0 +1,36 @@ +import { db } from "@bubba/db"; + +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: { + select: { + status: true, + }, + }, + organizationEvidence: { + select: { + published: 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 000000000..a4926a0fb --- /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 9fd0ed138..9a7392b2b 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 (
- - + +
); } From c16b567344b0ab05c651777eb71553189e7b4aff Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Fri, 21 Mar 2025 18:31:56 -0400 Subject: [PATCH 2/3] fully SSR now --- .../actions/getFrameworkCategoriesAction.ts | 76 ------------- .../[id]/actions/getOrganizationControl.ts | 60 ---------- .../actions/getOrganizationControlProgress.ts | 107 ------------------ .../getOrganizationControlRequirements.ts | 69 ----------- .../[id]/components/SingleControl.tsx | 28 ++--- .../table/ControlRequirementsTable.tsx | 6 - .../controls/[id]/data/getControl.ts | 30 +++++ .../data/getOrganizationControlProgress.ts | 88 ++++++++++++++ .../[id]/hooks/useOrganizationControl.ts | 37 ------ .../hooks/useOrganizationControlProgress.ts | 32 ------ .../useOrganizationControlRequirements.ts | 37 ------ .../frameworks/controls/[id]/page.tsx | 51 +++++---- 12 files changed, 159 insertions(+), 462 deletions(-) delete mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/actions/getFrameworkCategoriesAction.ts delete mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/actions/getOrganizationControl.ts delete mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/actions/getOrganizationControlProgress.ts delete mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/actions/getOrganizationControlRequirements.ts create mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/data/getControl.ts create mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/data/getOrganizationControlProgress.ts delete mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/hooks/useOrganizationControl.ts delete mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/hooks/useOrganizationControlProgress.ts delete mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/hooks/useOrganizationControlRequirements.ts 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 dcb62c2ec..000000000 --- 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/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 2774f87db..000000000 --- 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 2c185c7c7..000000000 --- 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 51762b52c..000000000 --- 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 5b89aa0e9..6f0d03d0c 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 0ae842f9f..af6c6ac1d 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 000000000..e0148889b --- /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 000000000..ed89c7c38 --- /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 8721f3dc5..000000000 --- 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 787753cfe..000000000 --- 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 6f37eb5d8..000000000 --- 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 97f18c0c6..35bc1e5ca 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; -}; From 9a9eeab1530f7bf810c19b886b49880f4b2f9831 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Fri, 21 Mar 2025 18:49:01 -0400 Subject: [PATCH 3/3] fixed type issues --- .../components/FrameworkControls.tsx | 40 +++++-------------- .../components/FrameworkOverview.tsx | 4 +- .../data/getFrameworkCategories.ts | 33 ++++++++++----- 3 files changed, 35 insertions(+), 42 deletions(-) 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 58f6c5a9d..7139e0e1a 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,52 +1,32 @@ "use client"; -import type { - Control, - OrganizationCategory, - OrganizationControl, - OrganizationControlRequirement, - OrganizationPolicy, - OrganizationEvidence, -} from "@bubba/db/types"; import { useMemo } from "react"; +import type { FrameworkCategories } from "../data/getFrameworkCategories"; import { FrameworkControlsTable } from "./table/FrameworkControlsTable"; import type { OrganizationControlType } from "./table/FrameworkControlsTableColumns"; -export type FrameworkCategory = OrganizationCategory & { - organizationControl: (OrganizationControl & { - control: Control; - OrganizationControlRequirement: (OrganizationControlRequirement & { - organizationPolicy: OrganizationPolicy; - organizationEvidence: OrganizationEvidence; - })[]; - })[]; -}; - export type FrameworkControlsProps = { - organizationCategories: FrameworkCategory[]; + organizationCategories: FrameworkCategories; frameworkId: string; }; export function FrameworkControls({ organizationCategories, frameworkId, -}: { - organizationCategories: FrameworkCategory[]; - frameworkId: string; -}) { +}: 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 49485f0ff..7c45b0520 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 @@ -6,9 +6,9 @@ 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 type { FrameworkCategory } from "./FrameworkControls"; +import type { FrameworkCategories } from "../data/getFrameworkCategories"; interface FrameworkOverviewProps { - organizationCategories: FrameworkCategory[]; + organizationCategories: FrameworkCategories; organizationFramework: OrganizationFramework & { framework: 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 index 1f1a12bbe..302a3021b 100644 --- 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 @@ -1,4 +1,25 @@ 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, @@ -15,16 +36,8 @@ export const getFrameworkCategories = async ( control: true, OrganizationControlRequirement: { include: { - organizationPolicy: { - select: { - status: true, - }, - }, - organizationEvidence: { - select: { - published: true, - }, - }, + organizationPolicy: true, + organizationEvidence: true, }, }, },