Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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[];
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<Card>
<CardHeader>
<CardTitle>{framework?.framework.name}</CardTitle>
<CardTitle>{organizationFramework?.framework.name}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
{framework?.framework.description}
{organizationFramework?.framework.description}
</p>
<div className="mt-4">
<Badge variant="outline">
Version {framework?.framework.version}
Version {organizationFramework?.framework.version}
</Badge>
</div>
</CardContent>
Expand Down Expand Up @@ -75,17 +111,17 @@ export function FrameworkOverview({ frameworkId }: FrameworkOverviewProps) {
<CalendarIcon className="h-4 w-4 text-muted-foreground" />
<span className="text-sm">
Last assessed:{" "}
{framework?.lastAssessed
? format(framework?.lastAssessed, "MMM d, yyyy")
{organizationFramework?.lastAssessed
? format(organizationFramework?.lastAssessed, "MMM d, yyyy")
: "Never"}
</span>
</div>
<div className="flex items-center gap-2">
<CalendarIcon className="h-4 w-4 text-muted-foreground" />
<span className="text-sm">
Next assessment:{" "}
{framework?.nextAssessment
? format(framework?.nextAssessment, "MMM d, yyyy")
{organizationFramework?.nextAssessment
? format(organizationFramework?.nextAssessment, "MMM d, yyyy")
: "Not scheduled"}
</span>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<OrganizationControlType>[] {
const t = useI18n();
const { orgId } = useParams<{ orgId: string }>();
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
};
Original file line number Diff line number Diff line change
@@ -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,
},
},
},
},
},
});
Comment on lines +28 to +46

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for database query

The database query lacks error handling. If the query fails, the error will propagate up the call stack without any context about what operation failed.

export const getFrameworkCategories = async (
  frameworkId: string,
  organizationId: string
) => {
+  try {
    const organizationCategories = await db.organizationCategory.findMany({
      where: {
        organizationId,
        frameworkId,
      },
      include: {
        organizationControl: {
          include: {
            control: true,
            OrganizationControlRequirement: {
              include: {
                organizationPolicy: true,
                organizationEvidence: true,
              },
            },
          },
        },
      },
    });

    return organizationCategories;
+  } catch (error) {
+    console.error(`Failed to fetch framework categories: ${error}`);
+    throw new Error(`Failed to fetch framework categories for framework ${frameworkId} and organization ${organizationId}`);
+  }
};
📝 Committable suggestion

‼️ 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.

Suggested change
const organizationCategories = await db.organizationCategory.findMany({
where: {
organizationId,
frameworkId,
},
include: {
organizationControl: {
include: {
control: true,
OrganizationControlRequirement: {
include: {
organizationPolicy: true,
organizationEvidence: true,
},
},
},
},
},
});
export const getFrameworkCategories = async (
frameworkId: string,
organizationId: string
) => {
try {
const organizationCategories = await db.organizationCategory.findMany({
where: {
organizationId,
frameworkId,
},
include: {
organizationControl: {
include: {
control: true,
OrganizationControlRequirement: {
include: {
organizationPolicy: true,
organizationEvidence: true,
},
},
},
},
},
});
return organizationCategories;
} catch (error) {
console.error(`Failed to fetch framework categories: ${error}`);
throw new Error(
`Failed to fetch framework categories for framework ${frameworkId} and organization ${organizationId}`
);
}
};


return organizationCategories;
};
Original file line number Diff line number Diff line change
@@ -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";
}
Loading