diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/components/FrameworkProgress.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/components/FrameworkProgress.tsx
deleted file mode 100644
index c9c3b3cee6..0000000000
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/components/FrameworkProgress.tsx
+++ /dev/null
@@ -1,184 +0,0 @@
-"use client";
-
-import { useComplianceScores } from "@/hooks/use-compliance-scores";
-import { useI18n } from "@/locales/client";
-import type {
- Framework,
- OrganizationControl,
- OrganizationFramework,
-} from "@bubba/db/types";
-import { Button } from "@bubba/ui/button";
-import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card";
-import { FileStack } from "lucide-react";
-import Link from "next/link";
-import { useMediaQuery } from "@bubba/ui/hooks";
-import type { ReactNode } from "react";
-import { useParams } from "next/navigation";
-import { auth } from "@/auth";
-interface Props {
- frameworks: (OrganizationFramework & {
- organizationControl: OrganizationControl[];
- framework: Framework;
- })[];
-}
-
-export function FrameworkProgress({ frameworks }: Props) {
- const t = useI18n();
- const {
- policiesCompliance,
- evidenceTasksCompliance,
- cloudTestsCompliance,
- overallCompliance,
- isLoading,
- } = useComplianceScores({ frameworks });
-
- const { orgId } = useParams<{ orgId: string }>();
-
- const isMobile = useMediaQuery("(max-width: 640px)");
-
- const CircleProgress = ({
- percentage,
- label,
- href,
- }: {
- percentage: number;
- label: ReactNode;
- href: string;
- }) => (
-
-
-
-
-
-
- {label}
-
-
- );
-
- return (
-
-
- {t("frameworks.overview.progress.title")}
-
-
- {isLoading ? (
-
-
-
- Loading compliance data...
-
-
- ) : frameworks.length === 0 ? (
-
-
-
- {t("frameworks.overview.progress.empty.title")}
-
-
- {t("frameworks.overview.progress.empty.description")}
-
-
-
- ) : (
-
- {/* Main compliance circle */}
-
-
-
-
-
- {overallCompliance}%
-
-
Compliant
-
-
-
-
- {/* Three smaller circles */}
-
-
-
- Evidence
- Evidence Tasks
- >
- }
- href={`/${orgId}/evidence/list`}
- />
-
- Tests
- Cloud Tests
- >
- }
- href={`/${orgId}/tests`}
- />
-
-
- )}
-
-
- );
-}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/components/RequirementStatusChart.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/components/RequirementStatusChart.tsx
deleted file mode 100644
index 13345995f1..0000000000
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/components/RequirementStatusChart.tsx
+++ /dev/null
@@ -1,177 +0,0 @@
-"use client";
-
-import { useI18n } from "@/locales/client";
-import type { Framework, OrganizationFramework } from "@bubba/db/types";
-import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card";
-import { Progress } from "@bubba/ui/progress";
-import Link from "next/link";
-import { useMemo } from "react";
-import type { OrganizationControlType } from "../overview/frameworks/[frameworkId]/components/table/FrameworkControlsTableColumns";
-import { useOrganizationCategories } from "../overview/frameworks/[frameworkId]/hooks/useOrganizationCategories";
-import { useParams } from "next/navigation";
-
-interface Props {
- frameworks: (OrganizationFramework & {
- framework: Framework;
- })[];
-}
-
-// Helper function to check if a control is compliant based on its requirements
-const isControlCompliant = (control: OrganizationControlType) => {
- // First, check if the control has the direct status of "compliant"
- if (control.status === "compliant") {
- return true;
- }
-
- // Then check the requirements if they exist
- const requirements = control.requirements;
-
- if (!requirements || requirements.length === 0) {
- return false;
- }
-
- const totalRequirements = requirements.length;
- const completedRequirements = requirements.filter((req) => {
- let isCompleted = false;
-
- switch (req.type) {
- case "policy":
- isCompleted = req.organizationPolicy?.status === "published";
- break;
- case "file":
- isCompleted = !!req.fileUrl;
- break;
- case "evidence":
- isCompleted = req.organizationEvidence?.published === true;
- break;
- default:
- isCompleted = req.published || false;
- }
-
- return isCompleted;
- }).length;
-
- return completedRequirements === totalRequirements;
-};
-
-// Individual FrameworkCard component
-function FrameworkCard({
- framework,
-}: { framework: OrganizationFramework & { framework: Framework } }) {
- const { data: organizationCategories, isLoading } = useOrganizationCategories(
- framework.framework.id,
- );
- const { orgId } = useParams<{ orgId: string }>();
-
- // Transform the organizationCategories into controls
- const controls = 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,
- frameworkId: framework.framework.id,
- category: category.name,
- requirements: control.OrganizationControlRequirement,
- })),
- );
- }, [
- organizationCategories,
- framework.framework.id,
- ]) as OrganizationControlType[];
-
- // Calculate framework compliance based on controls
- const compliance = useMemo(() => {
- if (isLoading || controls.length === 0) return 0;
-
- const totalControls = controls.length;
- const compliantControls = controls.filter(isControlCompliant).length;
-
- return totalControls > 0
- ? Math.round((compliantControls / totalControls) * 100)
- : 0;
- }, [controls, isLoading]);
-
- if (isLoading) {
- return (
-
- );
- }
-
- return (
-
-
-
- {framework.framework.name.substring(0, 2).toUpperCase()}
-
-
-
-
-
{framework.framework.name}
-
- {compliance}% Compliant
-
-
-
-
-
- );
-}
-
-// Main component
-export function RequirementStatus({ frameworks }: Props) {
- const t = useI18n();
- const isLoading = !frameworks;
-
- return (
-
-
- {t("frameworks.title")}
-
-
- {isLoading ? (
-
-
-
- Loading compliance data...
-
-
- ) : frameworks.length === 0 ? (
-
-
- {t("frameworks.overview.empty.description")}
-
-
- ) : (
-
- {/* Framework List */}
-
- {frameworks.map((framework) => (
-
- ))}
-
-
- )}
-
-
- );
-}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/layout.tsx
deleted file mode 100644
index a1d11fa0f9..0000000000
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/layout.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { auth } from "@/auth";
-import { getI18n } from "@/locales/server";
-import { redirect } from "next/navigation";
-import { SecondaryMenu } from "@bubba/ui/secondary-menu";
-
-export default async function Layout({
- children,
-}: {
- children: React.ReactNode;
-}) {
- const t = await getI18n();
- const session = await auth();
-
- if (!session?.user?.organizationId) {
- redirect("/auth");
- }
-
- return (
-
-
-
- {children}
-
- );
-}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/hooks/useOrganizationCategories.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/hooks/useOrganizationCategories.ts
deleted file mode 100644
index bd71206a4a..0000000000
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/hooks/useOrganizationCategories.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-"use client";
-
-import useSWR from "swr";
-import { getOrganizationCategories } from "../actions/getOrganizationCategories";
-
-async function fetchOrganizationCategories(frameworkId: string) {
- const result = await getOrganizationCategories({ frameworkId });
-
- if (!result) {
- throw new Error("Failed to fetch frameworks");
- }
-
- const data = result.data?.data;
- if (!data) {
- throw new Error("Invalid response from server");
- }
-
- return data;
-}
-
-export function useOrganizationCategories(frameworkId: string) {
- const { data, error, isLoading, mutate } = useSWR(
- ["organization-categories", frameworkId],
- () => fetchOrganizationCategories(frameworkId),
- {
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- },
- );
-
- return {
- data,
- isLoading,
- error,
- mutate,
- };
-}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/hooks/useOrganizationFramework.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/hooks/useOrganizationFramework.ts
deleted file mode 100644
index bc84d39d9f..0000000000
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/hooks/useOrganizationFramework.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-"use client";
-
-import useSWR from "swr";
-import { getOrganizationFramework } from "../actions/getOrganizationFramework";
-
-async function fetchOrganizationFramework(frameworkId: string) {
- const result = await getOrganizationFramework({ frameworkId });
-
- if (!result) {
- throw new Error("Failed to fetch frameworks");
- }
-
- const data = result.data?.data;
- if (!data) {
- throw new Error("Invalid response from server");
- }
-
- return data;
-}
-
-export function useOrganizationFramework(frameworkId: string) {
- const { data, error, isLoading, mutate } = useSWR(
- ["organization-framework", frameworkId],
- () => fetchOrganizationFramework(frameworkId),
- {
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- }
- );
-
- return {
- data,
- isLoading,
- error,
- mutate,
- };
-}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/layout.tsx
index 9a0e9b7e89..423d3d5928 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/layout.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/layout.tsx
@@ -5,40 +5,40 @@ import dynamic from "next/dynamic";
import { redirect } from "next/navigation";
const HotKeys = dynamic(
- () => import("@/components/hot-keys").then((mod) => mod.HotKeys),
- {
- ssr: true,
- },
+ () => import("@/components/hot-keys").then((mod) => mod.HotKeys),
+ {
+ ssr: true,
+ },
);
export default async function Layout({
- children,
- params,
+ children,
+ params,
}: {
- children: React.ReactNode;
- params: Promise<{ orgId: string }>;
+ children: React.ReactNode;
+ params: Promise<{ orgId: string }>;
}) {
- const session = await auth();
- const orgId = (await params).orgId;
+ const session = await auth();
+ const orgId = (await params).orgId;
- if (!session) {
- redirect("/auth");
- }
+ if (!session) {
+ redirect("/auth");
+ }
- if (!orgId) {
- redirect("/");
- }
+ if (!orgId) {
+ redirect("/overview");
+ }
- return (
-
-
+ return (
+
+
-
-
- {children}
-
+
+
+ {children}
+
-
-
- );
+
+
+ );
}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/FrameworkProgress.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/FrameworkProgress.tsx
new file mode 100644
index 0000000000..3fc10f4723
--- /dev/null
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/FrameworkProgress.tsx
@@ -0,0 +1,165 @@
+"use client";
+
+import { useI18n } from "@/locales/client";
+import { Button } from "@bubba/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card";
+import { useMediaQuery } from "@bubba/ui/hooks";
+import { FileStack } from "lucide-react";
+import Link from "next/link";
+import { useParams } from "next/navigation";
+import type { ReactNode } from "react";
+import type { ComplianceScoresProps } from "./types";
+import type {
+ Framework,
+ OrganizationControl,
+ OrganizationFramework,
+} from "@bubba/db/types";
+
+interface FrameworkProgressProps {
+ frameworks: (OrganizationFramework & {
+ organizationControl: OrganizationControl[];
+ framework: Framework;
+ })[];
+ complianceScores: ComplianceScoresProps;
+}
+
+export function FrameworkProgress({
+ frameworks,
+ complianceScores,
+}: FrameworkProgressProps) {
+ const t = useI18n();
+ const {
+ policiesCompliance,
+ evidenceTasksCompliance,
+ cloudTestsCompliance,
+ overallCompliance,
+ } = complianceScores;
+
+ const { orgId } = useParams<{ orgId: string }>();
+
+ const isMobile = useMediaQuery("(max-width: 640px)");
+
+ const CircleProgress = ({
+ percentage,
+ label,
+ href,
+ }: {
+ percentage: number;
+ label: ReactNode;
+ href: string;
+ }) => (
+
+
+
+
+
+
+ {label}
+
+
+ );
+
+ if (!frameworks.length) return null;
+
+ return (
+
+
+ {t("frameworks.overview.progress.title")}
+
+
+
+ {/* Main compliance circle */}
+
+
+
+
+
+ {overallCompliance}%
+
+
Compliant
+
+
+
+
+ {/* Three smaller circles */}
+
+
+
+ Evidence
+ Evidence Tasks
+ >
+ }
+ href={`/${orgId}/evidence/list`}
+ />
+
+ Tests
+ Cloud Tests
+ >
+ }
+ href={`/${orgId}/tests`}
+ />
+
+
+
+
+ );
+}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/components/FrameworksGrid.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/FrameworksGrid.tsx
similarity index 100%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/components/FrameworksGrid.tsx
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/FrameworksGrid.tsx
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/components/FrameworksOverview.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/FrameworksOverview.tsx
similarity index 50%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/components/FrameworksOverview.tsx
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/FrameworksOverview.tsx
index 1831d88465..d42d1290fb 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/components/FrameworksOverview.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/FrameworksOverview.tsx
@@ -5,22 +5,35 @@ import type {
OrganizationControl,
OrganizationFramework,
} from "@bubba/db/types";
+import type { ComplianceScoresProps, FrameworkWithCompliance } from "./types";
import { FrameworkProgress } from "./FrameworkProgress";
import { RequirementStatus } from "./RequirementStatusChart";
-export const FrameworksOverview = ({
- frameworks,
-}: {
+export interface FrameworksOverviewProps {
frameworks: (OrganizationFramework & {
organizationControl: OrganizationControl[];
framework: Framework;
})[];
-}) => {
+ complianceScores: ComplianceScoresProps;
+ frameworksWithCompliance: FrameworkWithCompliance[];
+}
+
+export const FrameworksOverview = ({
+ frameworks,
+ complianceScores,
+ frameworksWithCompliance,
+}: FrameworksOverviewProps) => {
return (
);
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/RequirementStatusChart.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/RequirementStatusChart.tsx
new file mode 100644
index 0000000000..f3eb93f710
--- /dev/null
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/RequirementStatusChart.tsx
@@ -0,0 +1,90 @@
+"use client";
+
+import { useI18n } from "@/locales/client";
+import type { Framework, OrganizationFramework } from "@bubba/db/types";
+import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card";
+import { Progress } from "@bubba/ui/progress";
+import Link from "next/link";
+import { useParams } from "next/navigation";
+
+interface FrameworkWithCompliance {
+ framework: OrganizationFramework & {
+ framework: Framework;
+ };
+ compliance: number;
+}
+
+interface Props {
+ frameworks: (OrganizationFramework & {
+ framework: Framework;
+ })[];
+ frameworksWithCompliance: FrameworkWithCompliance[];
+}
+
+// Individual FrameworkCard component
+function FrameworkCard({
+ framework,
+ compliance,
+}: {
+ framework: OrganizationFramework & { framework: Framework };
+ compliance: number;
+}) {
+ const { orgId } = useParams<{ orgId: string }>();
+
+ return (
+
+
+
+ {framework.framework.name.substring(0, 2).toUpperCase()}
+
+
+
+
+
{framework.framework.name}
+
+ {compliance}% Compliant
+
+
+
+
+
+ );
+}
+
+// Main component
+export function RequirementStatus({
+ frameworks,
+ frameworksWithCompliance,
+}: Props) {
+ const t = useI18n();
+
+ if (!frameworks.length || !frameworksWithCompliance.length) return null;
+
+ return (
+
+
+ {t("frameworks.title")}
+
+
+
+ {/* Framework List */}
+
+ {frameworksWithCompliance.map(({ framework, compliance }) => (
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/types.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/types.ts
new file mode 100644
index 0000000000..7617132cfe
--- /dev/null
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/types.ts
@@ -0,0 +1,29 @@
+import type {
+ Framework,
+ OrganizationEvidence,
+ OrganizationFramework,
+ OrganizationIntegrationResults,
+ OrganizationPolicy,
+} from "@bubba/db/types";
+
+export interface ComplianceScoresProps {
+ policiesCompliance: number;
+ evidenceTasksCompliance: number;
+ cloudTestsCompliance: number;
+ overallCompliance: number;
+ frameworkCompliance: {
+ id: string;
+ name: string;
+ compliance: number;
+ }[];
+ policies: OrganizationPolicy[];
+ evidenceTasks: OrganizationEvidence[];
+ tests: OrganizationIntegrationResults[];
+}
+
+export interface FrameworkWithCompliance {
+ framework: OrganizationFramework & {
+ framework: Framework;
+ };
+ compliance: number;
+}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/data/getComplianceScores.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/data/getComplianceScores.ts
new file mode 100644
index 0000000000..b9a0dbcfa2
--- /dev/null
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/data/getComplianceScores.ts
@@ -0,0 +1,267 @@
+"use server";
+
+import { db } from "@bubba/db";
+import type {
+ Framework,
+ OrganizationControl,
+ OrganizationEvidence,
+ OrganizationIntegrationResults,
+ OrganizationFramework,
+ OrganizationPolicy,
+} from "@bubba/db/types";
+
+interface ComplianceScoresResult {
+ policiesCompliance: number;
+ evidenceTasksCompliance: number;
+ cloudTestsCompliance: number;
+ overallCompliance: number;
+ frameworkCompliance: {
+ id: string;
+ name: string;
+ compliance: number;
+ }[];
+ policies: OrganizationPolicy[];
+ evidenceTasks: OrganizationEvidence[];
+ tests: any[];
+}
+
+export async function getComplianceScores(
+ organizationId: string,
+ frameworks: (OrganizationFramework & {
+ framework: Framework;
+ organizationControl: OrganizationControl[];
+ })[]
+): Promise {
+ // Get all policies for the organization
+ const policies = await db.organizationPolicy.findMany({
+ where: { organizationId },
+ include: {
+ policy: {
+ select: {
+ id: true,
+ name: true,
+ description: true,
+ slug: true,
+ },
+ },
+ },
+ });
+
+ // Get all policies by framework
+ const policiesByFramework = await db.policyFramework.findMany({
+ where: {
+ policy: {
+ OrganizationPolicy: {
+ some: {
+ organizationId,
+ },
+ },
+ },
+ },
+ include: {
+ policy: {
+ select: {
+ id: true,
+ OrganizationPolicy: {
+ where: { organizationId },
+ select: { id: true, status: true },
+ },
+ },
+ },
+ },
+ });
+
+ // Map policy framework data to a simpler structure
+ const mappedPoliciesByFramework = policiesByFramework.map((pf) => ({
+ frameworkId: pf.frameworkId,
+ policyId: pf.policyId,
+ organizationPolicyId: pf.policy.OrganizationPolicy[0]?.id,
+ status: pf.policy.OrganizationPolicy[0]?.status,
+ }));
+
+ // Get all evidence tasks for the organization
+ const evidenceTasks = await db.organizationEvidence.findMany({
+ where: { organizationId },
+ include: {
+ evidence: true,
+ },
+ });
+
+ // Map evidence tasks to framework
+ const evidenceByFramework = evidenceTasks
+ .filter((task) => task.frameworkId) // Only include tasks with a frameworkId
+ .map((task) => ({
+ id: task.id,
+ frameworkId: task.frameworkId,
+ evidenceId: task.id,
+ published: task.published,
+ }));
+
+ // Get all tests for the organization (using organizationIntegrationResults as per the getTests action)
+ const integrationResults = await db.organizationIntegrationResults.findMany({
+ where: { organizationId },
+ include: {
+ organizationIntegration: {
+ select: {
+ id: true,
+ name: true,
+ integration_id: true,
+ },
+ },
+ },
+ });
+
+ // Transform the data to match the expected format
+ const tests = integrationResults.map((result) => ({
+ id: result.id,
+ severity: result.label,
+ result: result.status,
+ title: result.title || result.organizationIntegration.name,
+ provider: result.organizationIntegration.integration_id,
+ createdAt: result.completedAt || new Date(),
+ }));
+
+ // Calculate policies compliance
+ const policiesCompliance =
+ policies.length > 0
+ ? Math.round(
+ (policies.filter((p) => p.status === "published").length /
+ policies.length) *
+ 100
+ )
+ : 0;
+
+ // Calculate evidence tasks compliance
+ const evidenceTasksCompliance =
+ evidenceTasks.length > 0
+ ? Math.round(
+ (evidenceTasks.filter((task) => task.published === true).length /
+ evidenceTasks.length) *
+ 100
+ )
+ : 0;
+
+ // Calculate cloud tests compliance (checking for "PASSED" status as per the type definition)
+ const cloudTestsCompliance =
+ tests.length > 0
+ ? Math.round(
+ (tests.filter((test) => test.result?.toUpperCase() === "PASSED")
+ .length /
+ tests.length) *
+ 100
+ )
+ : 0;
+
+ // Calculate framework-specific compliance
+ const frameworkCompliance = frameworks.map((framework) => {
+ // Calculate framework controls compliance
+ const totalControls = framework.organizationControl.length;
+
+ // Count controls with any progress (in_progress or compliant)
+ const inProgressControls = framework.organizationControl.filter(
+ (control) => control.status === "in_progress"
+ ).length;
+
+ const compliantControls = framework.organizationControl.filter(
+ (control) => control.status === "compliant"
+ ).length;
+
+ // For compliance percentage, count both in_progress (partial) and compliant (full)
+ // Give in_progress controls half weight compared to compliant ones
+ const progressWeight = 0.5; // Weight for in_progress controls
+ const weightedProgress =
+ compliantControls + inProgressControls * progressWeight;
+
+ // Calculate compliance percentage based on controls
+ const controlsCompliance =
+ totalControls > 0
+ ? Math.round((weightedProgress / totalControls) * 100)
+ : 0;
+
+ // Find policies for this specific framework
+ const frameworkPolicies = mappedPoliciesByFramework.filter(
+ (policy) => policy.frameworkId === framework.framework.id
+ );
+
+ // Calculate framework-specific policies compliance
+ const frameworkPoliciesCompliance =
+ frameworkPolicies.length > 0
+ ? Math.round(
+ (frameworkPolicies.filter(
+ (frameworkPolicy) => frameworkPolicy.status === "published"
+ ).length /
+ frameworkPolicies.length) *
+ 100
+ )
+ : null;
+
+ // Find evidence tasks related to this framework
+ const frameworkEvidenceTasks = evidenceByFramework.filter(
+ (evidence) => evidence.frameworkId === framework.framework.id
+ );
+
+ // Calculate framework-specific evidence tasks compliance
+ const frameworkEvidenceTasksCompliance =
+ frameworkEvidenceTasks.length > 0
+ ? Math.round(
+ (frameworkEvidenceTasks.filter(
+ (evidence) => evidence.published === true
+ ).length /
+ frameworkEvidenceTasks.length) *
+ 100
+ )
+ : null;
+
+ // Calculate overall framework compliance as average of all components with data
+ const complianceScores = [controlsCompliance];
+ if (frameworkPoliciesCompliance !== null)
+ complianceScores.push(frameworkPoliciesCompliance);
+ if (frameworkEvidenceTasksCompliance !== null)
+ complianceScores.push(frameworkEvidenceTasksCompliance);
+
+ const totalCompliance =
+ complianceScores.length > 0
+ ? Math.round(
+ complianceScores.reduce((sum, score) => sum + score, 0) /
+ complianceScores.length
+ )
+ : controlsCompliance;
+
+ return {
+ id: framework.framework.id,
+ name: framework.framework.name,
+ compliance: totalCompliance,
+ };
+ });
+
+ // Calculate overall compliance as the average of all available scores
+ const calculateOverallCompliance = () => {
+ // Count how many categories have data
+ const categoriesWithData = [
+ policies.length > 0,
+ evidenceTasks.length > 0,
+ tests.length > 0,
+ ].filter(Boolean).length;
+
+ // If no categories have data, return 0
+ if (categoriesWithData === 0) return 0;
+
+ // Calculate the sum of all compliance scores
+ const totalScore =
+ policiesCompliance + evidenceTasksCompliance + cloudTestsCompliance;
+
+ // Return the average, rounded to the nearest integer
+ return Math.round(totalScore / categoriesWithData);
+ };
+
+ return {
+ policiesCompliance,
+ evidenceTasksCompliance,
+ cloudTestsCompliance,
+ overallCompliance: calculateOverallCompliance(),
+ frameworkCompliance,
+ policies,
+ evidenceTasks,
+ tests,
+ };
+}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/data/getFrameworkCategories.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/data/getFrameworkCategories.ts
new file mode 100644
index 0000000000..4efdfee793
--- /dev/null
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/data/getFrameworkCategories.ts
@@ -0,0 +1,127 @@
+"use server";
+
+import { db } from "@bubba/db";
+import type {
+ ComplianceStatus,
+ Framework,
+ OrganizationControlRequirement,
+ OrganizationEvidence,
+ OrganizationFramework,
+ OrganizationPolicy,
+} from "@bubba/db/types";
+
+// Define a type for the control with its requirements as returned from the database
+type ControlWithRequirements = {
+ id: string;
+ status: ComplianceStatus;
+ control: {
+ code: string;
+ description: string | null;
+ name: string;
+ };
+ OrganizationControlRequirement: Array<
+ OrganizationControlRequirement & {
+ organizationPolicy: Pick | null;
+ organizationEvidence: Pick | null;
+ }
+ >;
+};
+
+// Helper function to check if a control is compliant based on its requirements
+const isControlCompliant = (control: ControlWithRequirements) => {
+ // First, check if the control has the direct status of "compliant"
+ if (control.status === "compliant") {
+ return true;
+ }
+
+ // Then check the requirements if they exist
+ const requirements = control.OrganizationControlRequirement;
+
+ if (!requirements || requirements.length === 0) {
+ return false;
+ }
+
+ const totalRequirements = requirements.length;
+ const completedRequirements = requirements.filter((req) => {
+ let isCompleted = false;
+
+ switch (req.type) {
+ case "policy":
+ isCompleted = req.organizationPolicy?.status === "published";
+ break;
+ case "file":
+ isCompleted = !!req.fileUrl;
+ break;
+ case "evidence":
+ isCompleted = req.organizationEvidence?.published === true;
+ break;
+ default:
+ isCompleted = req.published;
+ }
+
+ return isCompleted;
+ }).length;
+
+ return completedRequirements === totalRequirements;
+};
+
+export async function getFrameworkCategories(
+ organizationId: string,
+ frameworks: (OrganizationFramework & { framework: Framework })[]
+) {
+ // For each framework, get the categories and controls
+ const frameworksWithCompliance = await Promise.all(
+ frameworks.map(async (framework) => {
+ const organizationCategories = await db.organizationCategory.findMany({
+ where: {
+ organizationId,
+ frameworkId: framework.framework.id,
+ },
+ include: {
+ organizationControl: {
+ include: {
+ control: true,
+ OrganizationControlRequirement: {
+ include: {
+ organizationPolicy: {
+ select: {
+ status: true,
+ },
+ },
+ organizationEvidence: {
+ select: {
+ published: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ });
+
+ // Transform the categories into a list of controls
+ const controls = organizationCategories.flatMap((category) =>
+ category.organizationControl.map((control) => control)
+ );
+
+ // Calculate compliance percentage
+ const totalControls = controls.length;
+ const compliantControls = controls.filter((control) =>
+ isControlCompliant(control as ControlWithRequirements)
+ ).length;
+
+ const compliance =
+ totalControls > 0
+ ? Math.round((compliantControls / totalControls) * 100)
+ : 0;
+
+ return {
+ framework,
+ compliance,
+ };
+ })
+ );
+
+ return frameworksWithCompliance;
+}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/data/getFrameworks.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/data/getFrameworks.ts
new file mode 100644
index 0000000000..a96e8f8b97
--- /dev/null
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/data/getFrameworks.ts
@@ -0,0 +1,15 @@
+"use server";
+
+import { db } from "@bubba/db";
+
+export const getFrameworks = async (organizationId: string) => {
+ const frameworks = await db.organizationFramework.findMany({
+ where: { organizationId: organizationId },
+ include: {
+ organizationControl: true,
+ framework: true,
+ },
+ });
+
+ return frameworks;
+};
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/actions/getOrganizationCategories.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/actions/getOrganizationCategories.ts
similarity index 100%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/actions/getOrganizationCategories.ts
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/actions/getOrganizationCategories.ts
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/actions/getOrganizationControlsProgress.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/actions/getOrganizationControlsProgress.ts
similarity index 100%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/actions/getOrganizationControlsProgress.ts
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/actions/getOrganizationControlsProgress.ts
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/actions/getOrganizationFramework.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/actions/getOrganizationFramework.ts
similarity index 100%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/actions/getOrganizationFramework.ts
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/actions/getOrganizationFramework.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]/overview/frameworks/[frameworkId]/components/FrameworkControls.tsx
similarity index 100%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/FrameworkControls.tsx
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/components/FrameworkControls.tsx
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]/overview/frameworks/[frameworkId]/components/FrameworkOverview.tsx
similarity index 100%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/FrameworkOverview.tsx
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/components/FrameworkOverview.tsx
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTable.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTable.tsx
similarity index 100%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTable.tsx
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTable.tsx
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]/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTableColumns.tsx
similarity index 100%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTableColumns.tsx
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTableColumns.tsx
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTableHeader.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTableHeader.tsx
similarity index 100%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTableHeader.tsx
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTableHeader.tsx
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]/overview/frameworks/[frameworkId]/data/getFramework.ts
similarity index 100%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/data/getFramework.ts
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/data/getFramework.ts
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]/overview/frameworks/[frameworkId]/data/getFrameworkCategories.ts
similarity index 100%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/data/getFrameworkCategories.ts
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/data/getFrameworkCategories.ts
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]/overview/frameworks/[frameworkId]/lib/utils.ts
similarity index 100%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/lib/utils.ts
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/lib/utils.ts
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/loading.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/loading.tsx
new file mode 100644
index 0000000000..2cee56ed75
--- /dev/null
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/loading.tsx
@@ -0,0 +1,119 @@
+import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card";
+import { Skeleton } from "@bubba/ui/skeleton";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@bubba/ui/table";
+
+export default function Loading() {
+ return (
+
+ {/* Framework Overview Skeleton */}
+
+ {/* Framework Info Card */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Compliance Progress Card */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Assessment Status Card */}
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Framework Controls Table Skeleton */}
+
+
+
+
+ {["code", "name", "status"].map((column) => (
+
+
+
+ ))}
+
+
+
+ {[
+ "control1",
+ "control2",
+ "control3",
+ "control4",
+ "control5",
+ "control6",
+ "control7",
+ "control8",
+ "control9",
+ "control10",
+ "control11",
+ "control12",
+ "control13",
+ "control14",
+ "control15",
+ "control16",
+ "control17",
+ "control18",
+ ].map((control) => (
+
+ {["code", "name", "status"].map((column) => (
+
+
+
+ ))}
+
+ ))}
+
+
+
+
+ );
+}
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]/overview/frameworks/[frameworkId]/page.tsx
similarity index 100%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/page.tsx
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/page.tsx
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]/overview/frameworks/controls/[id]/components/SingleControl.tsx
similarity index 100%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/SingleControl.tsx
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/components/SingleControl.tsx
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/SingleControlSkeleton.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/components/SingleControlSkeleton.tsx
similarity index 100%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/SingleControlSkeleton.tsx
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/components/SingleControlSkeleton.tsx
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]/overview/frameworks/controls/[id]/components/table/ControlRequirementsTable.tsx
similarity index 100%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/table/ControlRequirementsTable.tsx
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/components/table/ControlRequirementsTable.tsx
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/table/ControlRequirementsTableColumns.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/components/table/ControlRequirementsTableColumns.tsx
similarity index 100%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/table/ControlRequirementsTableColumns.tsx
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/components/table/ControlRequirementsTableColumns.tsx
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/table/ControlRequirementsTableHeader.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/components/table/ControlRequirementsTableHeader.tsx
similarity index 100%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/table/ControlRequirementsTableHeader.tsx
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/components/table/ControlRequirementsTableHeader.tsx
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/table/types.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/components/table/types.ts
similarity index 100%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/table/types.ts
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/components/table/types.ts
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]/overview/frameworks/controls/[id]/data/getControl.ts
similarity index 100%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/data/getControl.ts
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/data/getControl.ts
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]/overview/frameworks/controls/[id]/data/getOrganizationControlProgress.ts
similarity index 100%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/data/getOrganizationControlProgress.ts
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/data/getOrganizationControlProgress.ts
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/loading.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/loading.tsx
new file mode 100644
index 0000000000..1d1decdb0e
--- /dev/null
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/loading.tsx
@@ -0,0 +1,97 @@
+import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card";
+import { Skeleton } from "@bubba/ui/skeleton";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@bubba/ui/table";
+
+export default function Loading() {
+ return (
+
+
+ {/* Control Info Cards */}
+
+ {/* Control Details Card */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Domain Card */}
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Requirements Table */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {["type", "description", "status"].map((requirementType) => (
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+ );
+}
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]/overview/frameworks/controls/[id]/page.tsx
similarity index 100%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/page.tsx
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/page.tsx
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/layout.tsx
new file mode 100644
index 0000000000..27b4f2bb99
--- /dev/null
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/layout.tsx
@@ -0,0 +1,32 @@
+import { auth } from "@/auth";
+import { getI18n } from "@/locales/server";
+import { redirect } from "next/navigation";
+import { SecondaryMenu } from "@bubba/ui/secondary-menu";
+
+export default async function Layout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const t = await getI18n();
+ const session = await auth();
+
+ if (!session?.user?.organizationId) {
+ redirect("/auth");
+ }
+
+ return (
+
+
+
+ {children}
+
+ );
+}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/loading.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/loading.tsx
new file mode 100644
index 0000000000..d307ac1ee6
--- /dev/null
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/loading.tsx
@@ -0,0 +1,70 @@
+import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card";
+import { Skeleton } from "@bubba/ui/skeleton";
+
+export default function Loading() {
+ return (
+
+
+ {/* Framework Progress Card Skeleton */}
+
+
+
+
+
+
+
+
+ {/* Main compliance circle */}
+
+
+
+
+ {/* Three smaller circles */}
+
+ {["policies", "evidence", "tests"].map((item) => (
+
+
+
+
+ ))}
+
+
+
+
+
+ {/* Requirements Status Card Skeleton */}
+
+
+
+
+
+
+
+
+
+ {["framework1", "framework2", "framework3"].map((item) => (
+
+ ))}
+
+
+
+
+
+
+ );
+}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/page.tsx
similarity index 59%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/page.tsx
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/page.tsx
index 8191e2b662..a9555f048f 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/page.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/page.tsx
@@ -1,15 +1,17 @@
+import { auth } from "@/auth";
import { getI18n } from "@/locales/server";
import type { Metadata } from "next";
import { setStaticParamsLocale } from "next-international/server";
-import { FrameworksOverview } from "./components/FrameworksOverview";
-import { db } from "@bubba/db";
-import { auth } from "@/auth";
import { redirect } from "next/navigation";
+import { FrameworksOverview } from "./components/FrameworksOverview";
+import { getFrameworks } from "./data/getFrameworks";
+import { getComplianceScores } from "./data/getComplianceScores";
+import { getFrameworkCategories } from "./data/getFrameworkCategories";
export default async function DashboardPage({
params,
}: {
- params: Promise<{ locale: string }>;
+ params: Promise<{ locale: string; orgId: string }>;
}) {
const { locale } = await params;
setStaticParamsLocale(locale);
@@ -22,8 +24,22 @@ export default async function DashboardPage({
}
const frameworks = await getFrameworks(organizationId);
-
- return ;
+ const complianceScores = await getComplianceScores(
+ organizationId,
+ frameworks,
+ );
+ const frameworksWithCompliance = await getFrameworkCategories(
+ organizationId,
+ frameworks,
+ );
+
+ return (
+
+ );
}
export async function generateMetadata({
@@ -39,15 +55,3 @@ export async function generateMetadata({
title: t("sidebar.overview"),
};
}
-
-const getFrameworks = async (organizationId: string) => {
- const frameworks = await db.organizationFramework.findMany({
- where: { organizationId: organizationId },
- include: {
- organizationControl: true,
- framework: true,
- },
- });
-
- return frameworks;
-};
diff --git a/apps/app/src/app/[locale]/page.tsx b/apps/app/src/app/[locale]/page.tsx
index e0ead87c27..b6f710ceee 100644
--- a/apps/app/src/app/[locale]/page.tsx
+++ b/apps/app/src/app/[locale]/page.tsx
@@ -9,7 +9,7 @@ export default async function RootPage() {
}
if (session.user?.organizationId) {
- redirect(`/${session.user.organizationId}`);
+ redirect(`/${session.user.organizationId}/overview`);
}
redirect("/setup");
diff --git a/apps/app/src/components/main-menu.tsx b/apps/app/src/components/main-menu.tsx
index f6d9e1254f..a4ca333e5b 100644
--- a/apps/app/src/components/main-menu.tsx
+++ b/apps/app/src/components/main-menu.tsx
@@ -190,7 +190,7 @@ export function MainMenu({
const defaultItems: MenuItem[] = [
{
id: "overview",
- path: "/:organizationId",
+ path: "/:organizationId/overview",
name: t("sidebar.overview"),
disabled: false,
icon: Icons.Overview,
diff --git a/apps/app/src/hooks/use-compliance-scores.ts b/apps/app/src/hooks/use-compliance-scores.ts
deleted file mode 100644
index 94e9d80726..0000000000
--- a/apps/app/src/hooks/use-compliance-scores.ts
+++ /dev/null
@@ -1,222 +0,0 @@
-"use client";
-
-import { useMemo } from "react";
-import { usePolicies } from "@/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/hooks/usePolicies";
-import { useTests } from "@/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/hooks/useTests";
-import type {
- Framework,
- OrganizationControl,
- OrganizationFramework,
-} from "@bubba/db/types";
-import { useOrganizationEvidenceTasks } from "@/app/[locale]/(app)/(dashboard)/[orgId]/evidence/hooks/useEvidenceTasks";
-import { usePoliciesByFramework } from "./use-policies-by-framework";
-
-interface UseComplianceScoresProps {
- frameworks?: (OrganizationFramework & {
- framework: Framework;
- organizationControl: OrganizationControl[];
- })[];
-}
-
-export function useComplianceScores({
- frameworks = [],
-}: UseComplianceScoresProps = {}) {
- // Getting ALL policies.
- const { policies = [], isLoading: policiesLoading } = usePolicies({
- page: 1,
- pageSize: 1000,
- });
-
- // Get policies by framework.
- const { policiesByFramework = [], isLoading: frameworksLoading } =
- usePoliciesByFramework();
-
- // Getting ALL evidence tasks.
- const { data: evidenceTasks = [], isLoading: evidenceTasksLoading } =
- useOrganizationEvidenceTasks({
- page: 1,
- pageSize: 1000,
- });
-
- // Create evidenceByFramework array for clearer separation of concerns
- const evidenceByFramework = useMemo(() => {
- // Map evidence tasks to their frameworks
- return evidenceTasks
- .filter((task) => task.frameworkId) // Only include tasks with a frameworkId
- .map((task) => ({
- id: task.id,
- frameworkId: task.frameworkId,
- evidenceId: task.id,
- published: task.published,
- }));
- }, [evidenceTasks]);
-
- // Getting ALL tests.
- const { tests = [], isLoading: testsLoading } = useTests('');
-
- // Combined loading state
- const isLoading = policiesLoading || evidenceTasksLoading || testsLoading;
-
- // Calculate all compliance scores
- const scores = useMemo(() => {
- // Calculate policies compliance (checking for published status)
- const policiesCompliance =
- policies.length > 0
- ? Math.round(
- (policies.filter((p) => p.status === "published").length /
- policies.length) *
- 100
- )
- : 0;
-
- // Calculate evidence tasks compliance (checking for published status)
- const evidenceTasksCompliance =
- evidenceTasks.length > 0
- ? Math.round(
- (evidenceTasks.filter((task) => task.published === true).length /
- evidenceTasks.length) *
- 100
- )
- : 0;
-
- // Calculate cloud tests compliance (tests with "passed" result)
- const cloudTestsCompliance =
- tests.length > 0
- ? Math.round(
- (tests.filter((t) => t.result === "passed").length / tests.length) *
- 100
- )
- : 0;
-
- // Calculate framework-specific compliance
- const frameworkCompliance = frameworks.map((framework) => {
- // Calculate framework controls compliance
- const totalControls = framework.organizationControl.length;
-
- // Count controls with any progress (in_progress or compliant)
- const inProgressControls = framework.organizationControl.filter(
- (control) => control.status === "in_progress"
- ).length;
-
- const compliantControls = framework.organizationControl.filter(
- (control) => control.status === "compliant"
- ).length;
-
- // For compliance percentage, count both in_progress (partial) and compliant (full)
- // Give in_progress controls half weight compared to compliant ones
- const progressWeight = 0.5; // Weight for in_progress controls
- const weightedProgress =
- compliantControls + inProgressControls * progressWeight;
-
- // Calculate compliance percentage based on controls
- const controlsCompliance =
- totalControls > 0
- ? Math.round((weightedProgress / totalControls) * 100)
- : 0;
-
- // Find policies for this specific framework using policiesByFramework
- const frameworkPolicies = policiesByFramework.filter(
- (policy) => policy.frameworkId === framework.framework.id
- );
-
- // Calculate framework-specific policies compliance
- // We need to look up the policy status in the main policies array since policiesByFramework might not have it directly
- const frameworkPoliciesCompliance =
- frameworkPolicies.length > 0
- ? Math.round(
- (frameworkPolicies.filter((frameworkPolicy) => {
- // Find the corresponding policy in the main policies array
- const policy = policies.find(
- (p) => p.id === frameworkPolicy.policyId
- );
- // Check if it's published
- return policy?.status === "published";
- }).length /
- frameworkPolicies.length) *
- 100
- )
- : null;
-
- // Find evidence tasks related to this framework using evidenceByFramework
- const frameworkEvidenceTasks = evidenceByFramework.filter(
- (evidence) => evidence.frameworkId === framework.framework.id
- );
-
- // Calculate framework-specific evidence tasks compliance
- const frameworkEvidenceTasksCompliance =
- frameworkEvidenceTasks.length > 0
- ? Math.round(
- (frameworkEvidenceTasks.filter(
- (evidence) => evidence.published === true
- ).length /
- frameworkEvidenceTasks.length) *
- 100
- )
- : null;
-
- // Calculate overall framework compliance as average of all components with data
- const complianceScores = [controlsCompliance];
- if (frameworkPoliciesCompliance !== null)
- complianceScores.push(frameworkPoliciesCompliance);
- if (frameworkEvidenceTasksCompliance !== null)
- complianceScores.push(frameworkEvidenceTasksCompliance);
-
- const totalCompliance =
- complianceScores.length > 0
- ? Math.round(
- complianceScores.reduce((sum, score) => sum + score, 0) /
- complianceScores.length
- )
- : controlsCompliance;
-
- return {
- id: framework.framework.id,
- name: framework.framework.name,
- compliance: totalCompliance,
- };
- });
-
- // Calculate overall compliance as the average of all available scores
- const calculateOverallCompliance = () => {
- // Count how many categories have data
- const categoriesWithData = [
- policies.length > 0,
- evidenceTasks.length > 0,
- tests.length > 0,
- ].filter(Boolean).length;
-
- // If no categories have data, return 0
- if (categoriesWithData === 0) return 0;
-
- // Calculate the sum of all compliance scores
- const totalScore =
- policiesCompliance + evidenceTasksCompliance + cloudTestsCompliance;
-
- // Return the average, rounded to the nearest integer
- return Math.round(totalScore / categoriesWithData);
- };
-
- return {
- policiesCompliance,
- evidenceTasksCompliance,
- cloudTestsCompliance,
- overallCompliance: calculateOverallCompliance(),
- frameworkCompliance,
- };
- }, [
- policies,
- evidenceTasks,
- tests,
- frameworks,
- policiesByFramework,
- evidenceByFramework,
- ]);
-
- return {
- ...scores,
- isLoading,
- policies,
- evidenceTasks,
- tests,
- };
-}
diff --git a/packages/db/prisma/seed.js b/packages/db/prisma/seed.js
index 919a3ce9ee..1bfa92e08b 100644
--- a/packages/db/prisma/seed.js
+++ b/packages/db/prisma/seed.js
@@ -1,9 +1,7 @@
"use strict";
-var __importDefault =
- (this && this.__importDefault) ||
- function (mod) {
- return mod && mod.__esModule ? mod : { default: mod };
- };
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
Object.defineProperty(exports, "__esModule", { value: true });
const client_1 = require("@prisma/client");
const client_2 = require("@prisma/client");
@@ -13,576 +11,487 @@ const node_fs_2 = __importDefault(require("node:fs"));
const data_1 = require("@bubba/data");
const prisma = new client_1.PrismaClient();
async function main() {
- if (process.env.NODE_ENV === "development") {
- console.log("\nšļø Cleaning up existing data...");
- await prisma.organizationFramework.deleteMany();
- await prisma.organizationCategory.deleteMany();
- await prisma.organizationControl.deleteMany();
- await prisma.organizationPolicy.deleteMany();
- await prisma.organizationControlRequirement.deleteMany();
- await prisma.organizationEvidence.deleteMany();
- await prisma.policy.deleteMany();
- await prisma.policyControl.deleteMany();
- await prisma.policyFramework.deleteMany();
- await prisma.control.deleteMany();
- await prisma.controlRequirement.deleteMany();
- await prisma.framework.deleteMany();
- await prisma.frameworkCategory.deleteMany();
- await prisma.evidence.deleteMany();
- console.log("ā
Database cleaned");
- }
- console.log("\nš Seeding policies...");
- await seedPolicies();
- console.log("ā
Policies seeded");
- console.log("\nš Seeding evidence records (phase 1)");
- await seedEvidenceRecords();
- console.log("ā
Evidence records seeded");
- console.log("\nšļø Seeding frameworks...");
- await seedFrameworks();
- console.log("ā
Frameworks seeded");
- console.log("\nš Seeding policy frameworks...");
- await seedPolicyFramework();
- console.log("ā
Policy frameworks seeded");
- console.log("\nš Updating policy links (phase 1)");
- await updatePolicyLinks();
- console.log("ā
Policy links updated");
- console.log("\nš Updating evidence links (phase 2)");
- await updateEvidenceLinks();
- console.log("ā
Evidence links updated");
- console.log("\nš„ Seeding training videos...");
- await seedTrainingVideos();
- console.log("ā
Training videos seeded");
- console.log("\nš All data seeded successfully!");
+ if (process.env.NODE_ENV === "development") {
+ console.log("\nšļø Cleaning up existing data...");
+ await prisma.organizationFramework.deleteMany();
+ await prisma.organizationCategory.deleteMany();
+ await prisma.organizationControl.deleteMany();
+ await prisma.organizationPolicy.deleteMany();
+ await prisma.organizationControlRequirement.deleteMany();
+ await prisma.organizationEvidence.deleteMany();
+ await prisma.policy.deleteMany();
+ await prisma.policyControl.deleteMany();
+ await prisma.policyFramework.deleteMany();
+ await prisma.control.deleteMany();
+ await prisma.controlRequirement.deleteMany();
+ await prisma.framework.deleteMany();
+ await prisma.frameworkCategory.deleteMany();
+ await prisma.evidence.deleteMany();
+ console.log("ā
Database cleaned");
+ }
+ console.log("\nš Seeding policies...");
+ await seedPolicies();
+ console.log("ā
Policies seeded");
+ console.log("\nš Seeding evidence records (phase 1)");
+ await seedEvidenceRecords();
+ console.log("ā
Evidence records seeded");
+ console.log("\nšļø Seeding frameworks...");
+ await seedFrameworks();
+ console.log("ā
Frameworks seeded");
+ console.log("\nš Seeding policy frameworks...");
+ await seedPolicyFramework();
+ console.log("ā
Policy frameworks seeded");
+ console.log("\nš Updating policy links (phase 1)");
+ await updatePolicyLinks();
+ console.log("ā
Policy links updated");
+ console.log("\nš Updating evidence links (phase 2)");
+ await updateEvidenceLinks();
+ console.log("ā
Evidence links updated");
+ console.log("\nš„ Seeding training videos...");
+ await seedTrainingVideos();
+ console.log("ā
Training videos seeded");
+ console.log("\nš All data seeded successfully!");
}
main()
- .catch((e) => {
+ .catch((e) => {
console.error("\nā Error during seeding:", e);
process.exit(1);
- })
- .finally(async () => {
+})
+ .finally(async () => {
await prisma.$disconnect();
- });
+});
async function seedPolicies() {
- const policiesDir = (0, node_path_1.join)(__dirname, "../../data/policies");
- const policyFiles = (0, node_fs_1.readdirSync)(policiesDir).filter((file) =>
- file.endsWith(".json")
- );
- console.log(`š Found ${policyFiles.length} policy files to process`);
- for (const file of policyFiles) {
- console.log(` ā³ Processing ${file}...`);
- try {
- const fileContent = (0, node_fs_1.readFileSync)(
- (0, node_path_1.join)(policiesDir, file),
- "utf8"
- );
- const policyData = JSON.parse(fileContent);
- // Check for any existing policies with the same slug
- const existingPolicyWithSlug = await prisma.policy.findFirst({
- where: {
- slug: policyData.metadata.slug,
- NOT: { id: policyData.metadata.id },
- },
- });
- // If there's a conflict, delete the existing policy
- if (existingPolicyWithSlug) {
- console.log(
- ` ā ļø Found existing policy with slug "${policyData.metadata.slug}", replacing it...`
- );
- await prisma.policy.delete({
- where: { id: existingPolicyWithSlug.id },
- });
- }
- // Now we can safely upsert the new policy
- await prisma.policy.upsert({
- where: {
- id: policyData.metadata.id,
- },
- update: {
- name: policyData.metadata.name,
- slug: policyData.metadata.slug,
- description: policyData.metadata.description,
- content: policyData.content,
- usedBy: policyData.metadata.usedBy,
- frequency: policyData.metadata?.frequency ?? null,
- department:
- policyData.metadata?.department ?? client_1.Departments.none,
- },
- create: {
- id: policyData.metadata.id,
- slug: policyData.metadata.slug,
- name: policyData.metadata.name,
- description: policyData.metadata.description,
- content: policyData.content,
- usedBy: policyData.metadata.usedBy,
- frequency: policyData.metadata?.frequency ?? null,
- department:
- policyData.metadata?.department ?? client_1.Departments.none,
- },
- });
- console.log(` ā
${file} processed`);
- } catch (error) {
- console.error(` ā Error processing ${file}:`, error);
- if (error instanceof Error) {
- console.error(` Error details: ${error.message}`);
- }
+ const policiesDir = (0, node_path_1.join)(__dirname, "../../data/policies");
+ const policyFiles = (0, node_fs_1.readdirSync)(policiesDir).filter((file) => file.endsWith(".json"));
+ console.log(`š Found ${policyFiles.length} policy files to process`);
+ for (const file of policyFiles) {
+ console.log(` ā³ Processing ${file}...`);
+ try {
+ const fileContent = (0, node_fs_1.readFileSync)((0, node_path_1.join)(policiesDir, file), "utf8");
+ const policyData = JSON.parse(fileContent);
+ // Check for any existing policies with the same slug
+ const existingPolicyWithSlug = await prisma.policy.findFirst({
+ where: {
+ slug: policyData.metadata.slug,
+ NOT: { id: policyData.metadata.id },
+ },
+ });
+ // If there's a conflict, delete the existing policy
+ if (existingPolicyWithSlug) {
+ console.log(` ā ļø Found existing policy with slug "${policyData.metadata.slug}", replacing it...`);
+ await prisma.policy.delete({
+ where: { id: existingPolicyWithSlug.id },
+ });
+ }
+ // Now we can safely upsert the new policy
+ await prisma.policy.upsert({
+ where: {
+ id: policyData.metadata.id,
+ },
+ update: {
+ name: policyData.metadata.name,
+ slug: policyData.metadata.slug,
+ description: policyData.metadata.description,
+ content: policyData.content,
+ usedBy: policyData.metadata.usedBy,
+ frequency: policyData.metadata?.frequency ?? null,
+ department: policyData.metadata?.department ?? client_1.Departments.none,
+ },
+ create: {
+ id: policyData.metadata.id,
+ slug: policyData.metadata.slug,
+ name: policyData.metadata.name,
+ description: policyData.metadata.description,
+ content: policyData.content,
+ usedBy: policyData.metadata.usedBy,
+ frequency: policyData.metadata?.frequency ?? null,
+ department: policyData.metadata?.department ?? client_1.Departments.none,
+ },
+ });
+ console.log(` ā
${file} processed`);
+ }
+ catch (error) {
+ console.error(` ā Error processing ${file}:`, error);
+ if (error instanceof Error) {
+ console.error(` Error details: ${error.message}`);
+ }
+ }
}
- }
}
async function seedFrameworks() {
- const frameworksFile = (0, node_path_1.join)(
- __dirname,
- "../../data/frameworks.json"
- );
- const frameworksJson = JSON.parse(
- (0, node_fs_1.readFileSync)(frameworksFile, "utf8")
- );
- console.log(
- `š Found ${Object.keys(frameworksJson).length} frameworks to process`
- );
- // Populate the app level frameworks that every org has access to.
- for (const [frameworkId, frameworkData] of Object.entries(frameworksJson)) {
- console.log(` ā³ Processing framework: ${frameworkData.name}...`);
- // First, upsert the framework itself.
- const insertedFramework = await prisma.framework.upsert({
- where: { id: frameworkId },
- update: {
- description: frameworkData.description,
- version: frameworkData.version,
- },
- create: {
- id: frameworkId,
- name: frameworkData.name,
- description: frameworkData.description,
- version: frameworkData.version,
- },
- });
- // Then, upsert the framework categories.
- await seedFrameworkCategories(insertedFramework.id);
- console.log(` ā
Framework ${frameworkData.name} processed`);
- }
+ const frameworksFile = (0, node_path_1.join)(__dirname, "../../data/frameworks.json");
+ const frameworksJson = JSON.parse((0, node_fs_1.readFileSync)(frameworksFile, "utf8"));
+ console.log(`š Found ${Object.keys(frameworksJson).length} frameworks to process`);
+ // Populate the app level frameworks that every org has access to.
+ for (const [frameworkId, frameworkData] of Object.entries(frameworksJson)) {
+ console.log(` ā³ Processing framework: ${frameworkData.name}...`);
+ // First, upsert the framework itself.
+ const insertedFramework = await prisma.framework.upsert({
+ where: { id: frameworkId },
+ update: {
+ description: frameworkData.description,
+ version: frameworkData.version,
+ },
+ create: {
+ id: frameworkId,
+ name: frameworkData.name,
+ description: frameworkData.description,
+ version: frameworkData.version,
+ },
+ });
+ // Then, upsert the framework categories.
+ await seedFrameworkCategories(insertedFramework.id);
+ console.log(` ā
Framework ${frameworkData.name} processed`);
+ }
}
async function seedFrameworkCategories(frameworkId) {
- let categories;
- try {
- categories = node_fs_2.default.readFileSync(
- (0, node_path_1.join)(
- __dirname,
- `../../data/categories/${frameworkId}.json`
- ),
- "utf8"
- );
- } catch (error) {
- console.log(
- ` ā ļø No categories found for framework ${frameworkId}, skipping`
- );
- return;
- }
- const categoriesData = JSON.parse(categories);
- console.log(
- ` š Found ${Object.keys(categoriesData).length} categories for ${frameworkId}`
- );
- // Upsert the framework categories for the given framework.
- for (const [categoryCode, categoryData] of Object.entries(categoriesData)) {
- console.log(` ā³ Processing category: ${categoryData.name}...`);
- // First, upsert the framework category itself for the given framework.
- await prisma.frameworkCategory.upsert({
- where: { id: categoryCode },
- update: {
- name: categoryData.name,
- code: categoryData.code,
- description: categoryData.description,
- frameworkId: frameworkId,
- },
- create: {
- id: categoryCode,
- name: categoryData.name,
- description: categoryData.description,
- code: categoryData.code,
- frameworkId: frameworkId,
- },
- });
- // Then, upsert the controls for the given framework category.
- await seedFrameworkCategoryControls(frameworkId, categoryCode);
- console.log(` ā
Category ${categoryData.name} processed`);
- }
+ let categories;
+ try {
+ categories = node_fs_2.default.readFileSync((0, node_path_1.join)(__dirname, `../../data/categories/${frameworkId}.json`), "utf8");
+ }
+ catch (error) {
+ console.log(` ā ļø No categories found for framework ${frameworkId}, skipping`);
+ return;
+ }
+ const categoriesData = JSON.parse(categories);
+ console.log(` š Found ${Object.keys(categoriesData).length} categories for ${frameworkId}`);
+ // Upsert the framework categories for the given framework.
+ for (const [categoryCode, categoryData] of Object.entries(categoriesData)) {
+ console.log(` ā³ Processing category: ${categoryData.name}...`);
+ // First, upsert the framework category itself for the given framework.
+ await prisma.frameworkCategory.upsert({
+ where: { id: categoryCode },
+ update: {
+ name: categoryData.name,
+ code: categoryData.code,
+ description: categoryData.description,
+ frameworkId: frameworkId,
+ },
+ create: {
+ id: categoryCode,
+ name: categoryData.name,
+ description: categoryData.description,
+ code: categoryData.code,
+ frameworkId: frameworkId,
+ },
+ });
+ // Then, upsert the controls for the given framework category.
+ await seedFrameworkCategoryControls(frameworkId, categoryCode);
+ console.log(` ā
Category ${categoryData.name} processed`);
+ }
}
async function seedFrameworkCategoryControls(frameworkId, categoryCode) {
- const controls = node_fs_2.default.readFileSync(
- (0, node_path_1.join)(__dirname, `../../data/controls/${frameworkId}.json`),
- "utf8"
- );
- const controlsData = JSON.parse(controls);
- const filteredControlsData = Object.fromEntries(
- Object.entries(controlsData).filter(
- ([_, data]) => data.categoryId === categoryCode
- )
- );
- console.log(
- ` š® Processing ${Object.keys(filteredControlsData).length} controls`
- );
- for (const [controlCode, controlData] of Object.entries(
- filteredControlsData
- )) {
- // First, upsert the controls itself for the given category.
- await prisma.control.upsert({
- where: { code: controlCode },
- update: {
- name: controlData.name,
- description: controlData.description,
- domain: controlData.domain,
- frameworkCategoryId: categoryCode,
- },
- create: {
- // Use the control code (e.g. CC1.1) as both the id and code
- id: controlCode,
- code: controlCode,
- name: controlData.name,
- description: controlData.description,
- domain: controlData.domain,
- frameworkCategoryId: categoryCode,
- },
- });
- // Then, upsert the requirements for the given control.
- console.log(
- ` š Processing ${controlData.requirements.length} requirements for ${controlCode}`
- );
- for (const requirement of controlData.requirements) {
- // For both policy and evidence requirements, initially set policyId and evidenceId to null
- // They will be updated later in their respective update functions
- await prisma.controlRequirement.upsert({
- where: {
- id: requirement.id,
- },
- create: {
- id: requirement.id,
- controlId: controlCode,
- name: requirement.name || "",
- type: requirement.type,
- description: requirement.description || "",
- // Set both policyId and evidenceId to null initially
- policyId: null,
- evidenceId: null,
- frequency: requirement?.frequency ?? null,
- department: requirement?.department ?? client_1.Departments.none,
- },
- update: {
- name: requirement.name || "",
- description: requirement.description || "",
- // Don't update policyId or evidenceId here
- frequency: requirement?.frequency ?? null,
- department: requirement?.department ?? client_1.Departments.none,
- },
- });
+ const controls = node_fs_2.default.readFileSync((0, node_path_1.join)(__dirname, `../../data/controls/${frameworkId}.json`), "utf8");
+ const controlsData = JSON.parse(controls);
+ const filteredControlsData = Object.fromEntries(Object.entries(controlsData).filter(([_, data]) => data.categoryId === categoryCode));
+ console.log(` š® Processing ${Object.keys(filteredControlsData).length} controls`);
+ for (const [controlCode, controlData] of Object.entries(filteredControlsData)) {
+ // First, upsert the controls itself for the given category.
+ await prisma.control.upsert({
+ where: { code: controlCode },
+ update: {
+ name: controlData.name,
+ description: controlData.description,
+ domain: controlData.domain,
+ frameworkCategoryId: categoryCode,
+ },
+ create: {
+ // Use the control code (e.g. CC1.1) as both the id and code
+ id: controlCode,
+ code: controlCode,
+ name: controlData.name,
+ description: controlData.description,
+ domain: controlData.domain,
+ frameworkCategoryId: categoryCode,
+ },
+ });
+ // Then, upsert the requirements for the given control.
+ console.log(` š Processing ${controlData.requirements.length} requirements for ${controlCode}`);
+ for (const requirement of controlData.requirements) {
+ // For both policy and evidence requirements, initially set policyId and evidenceId to null
+ // They will be updated later in their respective update functions
+ await prisma.controlRequirement.upsert({
+ where: {
+ id: requirement.id,
+ },
+ create: {
+ id: requirement.id,
+ controlId: controlCode,
+ name: requirement.name || "",
+ type: requirement.type,
+ description: requirement.description || "",
+ // Set both policyId and evidenceId to null initially
+ policyId: null,
+ evidenceId: null,
+ frequency: requirement?.frequency ?? null,
+ department: requirement?.department ?? client_1.Departments.none,
+ },
+ update: {
+ name: requirement.name || "",
+ description: requirement.description || "",
+ // Don't update policyId or evidenceId here
+ frequency: requirement?.frequency ?? null,
+ department: requirement?.department ?? client_1.Departments.none,
+ },
+ });
+ }
}
- }
}
async function seedPolicyFramework() {
- const policies = await prisma.policy.findMany();
- console.log(
- `š Processing ${policies.length} policies for framework mapping`
- );
- for (const policy of policies) {
- console.log(` ā³ Mapping policy: ${policy.name}...`);
- if (!policy.usedBy) {
- console.log(` ā ļø Policy ${policy.name} has no usedBy, skipping`);
- continue;
+ const policies = await prisma.policy.findMany();
+ console.log(`š Processing ${policies.length} policies for framework mapping`);
+ for (const policy of policies) {
+ console.log(` ā³ Mapping policy: ${policy.name}...`);
+ if (!policy.usedBy) {
+ console.log(` ā ļø Policy ${policy.name} has no usedBy, skipping`);
+ continue;
+ }
+ for (const [frameworkId, controlCodes] of Object.entries(policy.usedBy)) {
+ // First verify the framework exists
+ const framework = await prisma.framework.findUnique({
+ where: { id: frameworkId },
+ });
+ if (!framework) {
+ console.log(` ā ļø Framework ${frameworkId} not found, skipping`);
+ continue;
+ }
+ // Upsert the policy framework mapping
+ await prisma.policyFramework.upsert({
+ where: { id: `${frameworkId}_${policy.id}` },
+ update: {
+ policyId: policy.id,
+ frameworkId: frameworkId,
+ },
+ create: {
+ id: `${frameworkId}_${policy.id}`,
+ policyId: policy.id,
+ frameworkId: frameworkId,
+ },
+ });
+ // For each control code, create the policy control mapping directly
+ for (const controlCode of controlCodes) {
+ console.log(` ā³ Mapping control ${controlCode} to policy ${policy.name}`);
+ // Now create the policy control mapping using the control code directly
+ await prisma.policyControl.upsert({
+ where: {
+ id: `${frameworkId}_${policy.id}_${controlCode}`,
+ },
+ update: {
+ policyId: policy.id,
+ controlId: controlCode, // Use the control code directly
+ },
+ create: {
+ id: `${frameworkId}_${policy.id}_${controlCode}`,
+ policyId: policy.id,
+ controlId: controlCode, // Use the control code directly
+ },
+ });
+ }
+ }
+ console.log(` ā
Policy ${policy.name} mapped`);
}
- for (const [frameworkId, controlCodes] of Object.entries(policy.usedBy)) {
- // First verify the framework exists
- const framework = await prisma.framework.findUnique({
- where: { id: frameworkId },
- });
- if (!framework) {
- console.log(` ā ļø Framework ${frameworkId} not found, skipping`);
- continue;
- }
- // Upsert the policy framework mapping
- await prisma.policyFramework.upsert({
- where: { id: `${frameworkId}_${policy.id}` },
- update: {
- policyId: policy.id,
- frameworkId: frameworkId,
- },
- create: {
- id: `${frameworkId}_${policy.id}`,
- policyId: policy.id,
- frameworkId: frameworkId,
- },
- });
- // For each control code, create the policy control mapping directly
- for (const controlCode of controlCodes) {
- console.log(
- ` ā³ Mapping control ${controlCode} to policy ${policy.name}`
- );
- // Now create the policy control mapping using the control code directly
- await prisma.policyControl.upsert({
- where: {
- id: `${frameworkId}_${policy.id}_${controlCode}`,
- },
- update: {
- policyId: policy.id,
- controlId: controlCode, // Use the control code directly
- },
- create: {
- id: `${frameworkId}_${policy.id}_${controlCode}`,
- policyId: policy.id,
- controlId: controlCode, // Use the control code directly
- },
- });
- }
- }
- console.log(` ā
Policy ${policy.name} mapped`);
- }
}
// Phase 1: Create evidence records from files (without linking to requirements)
async function seedEvidenceRecords() {
- const evidenceDir = (0, node_path_1.join)(__dirname, "../../data/evidence");
- const evidenceFiles = (0, node_fs_1.readdirSync)(evidenceDir).filter((file) =>
- file.endsWith(".json")
- );
- console.log(`š Found ${evidenceFiles.length} evidence files to process`);
- for (const file of evidenceFiles) {
- const evidenceId = file.replace(".json", "");
- console.log(` ā³ Processing evidence file: ${file}...`);
- try {
- const fileContent = (0, node_fs_1.readFileSync)(
- (0, node_path_1.join)(evidenceDir, file),
- "utf8"
- );
- const evidenceData = JSON.parse(fileContent);
- // Upsert the evidence record
- await prisma.evidence.upsert({
- where: {
- id: evidenceData.id,
- },
- update: {
- name: evidenceData.name,
- description: evidenceData.description,
- frequency: evidenceData.frequency ?? null,
- department: evidenceData.department ?? client_1.Departments.none,
- },
- create: {
- id: evidenceData.id,
- name: evidenceData.name,
- description: evidenceData.description,
- frequency: evidenceData.frequency ?? null,
- department: evidenceData.department ?? client_1.Departments.none,
- },
- });
- console.log(` ā
Evidence ${evidenceId} processed`);
- } catch (error) {
- console.error(` ā Error processing ${file}:`, error);
- if (error instanceof Error) {
- console.error(` Error details: ${error.message}`);
- }
+ const evidenceDir = (0, node_path_1.join)(__dirname, "../../data/evidence");
+ const evidenceFiles = (0, node_fs_1.readdirSync)(evidenceDir).filter((file) => file.endsWith(".json"));
+ console.log(`š Found ${evidenceFiles.length} evidence files to process`);
+ for (const file of evidenceFiles) {
+ const evidenceId = file.replace(".json", "");
+ console.log(` ā³ Processing evidence file: ${file}...`);
+ try {
+ const fileContent = (0, node_fs_1.readFileSync)((0, node_path_1.join)(evidenceDir, file), "utf8");
+ const evidenceData = JSON.parse(fileContent);
+ // Upsert the evidence record
+ await prisma.evidence.upsert({
+ where: {
+ id: evidenceData.id,
+ },
+ update: {
+ name: evidenceData.name,
+ description: evidenceData.description,
+ frequency: evidenceData.frequency ?? null,
+ department: evidenceData.department ?? client_1.Departments.none,
+ },
+ create: {
+ id: evidenceData.id,
+ name: evidenceData.name,
+ description: evidenceData.description,
+ frequency: evidenceData.frequency ?? null,
+ department: evidenceData.department ?? client_1.Departments.none,
+ },
+ });
+ console.log(` ā
Evidence ${evidenceId} processed`);
+ }
+ catch (error) {
+ console.error(` ā Error processing ${file}:`, error);
+ if (error instanceof Error) {
+ console.error(` Error details: ${error.message}`);
+ }
+ }
}
- }
}
// Phase 2: Update control requirements to link to evidence
async function updateEvidenceLinks() {
- // Get all control requirements that are evidence type
- const evidenceRequirements = await prisma.controlRequirement.findMany({
- where: {
- type: client_2.RequirementType.evidence,
- },
- });
- console.log(
- `š Processing ${evidenceRequirements.length} evidence requirements`
- );
- for (const requirement of evidenceRequirements) {
- // Get the controls file for this requirement to extract the evidenceId
- const control = await prisma.control.findUnique({
- where: { id: requirement.controlId },
- include: { frameworkCategory: true },
- });
- if (!control) {
- console.log(
- ` ā ļø Control not found for requirement ${requirement.id}, skipping`
- );
- continue;
- }
- if (!control.frameworkCategory) {
- console.log(
- ` ā ļø Framework category not found for control ${control.id}, skipping`
- );
- continue;
- }
- // Get the framework ID from the category
- const frameworkId = control.frameworkCategory.frameworkId;
- // Get the controls data from the file
- const controlsFile = (0, node_path_1.join)(
- __dirname,
- `../../data/controls/${frameworkId}.json`
- );
- const controlsData = JSON.parse(
- node_fs_2.default.readFileSync(controlsFile, "utf8")
- );
- // Find the requirement in the control data
- const controlData = controlsData[control.code];
- if (!controlData) {
- console.log(
- ` ā ļø Control data not found for ${control.code} in framework ${frameworkId}, skipping`
- );
- continue;
- }
- const reqData = controlData.requirements.find(
- (req) => req.id === requirement.id
- );
- if (!reqData) {
- console.log(
- ` ā ļø Requirement data not found for ${requirement.id} in control ${control.code}, skipping`
- );
- continue;
- }
- // Get the evidenceId from the requirement data
- const evidenceId = reqData.evidenceId;
- if (!evidenceId) {
- console.log(
- ` ā ļø No evidenceId found for requirement ${requirement.id}, skipping`
- );
- continue;
- }
- // Verify the evidence exists
- const evidence = await prisma.evidence.findUnique({
- where: { id: evidenceId },
+ // Get all control requirements that are evidence type
+ const evidenceRequirements = await prisma.controlRequirement.findMany({
+ where: {
+ type: client_2.RequirementType.evidence,
+ },
});
- if (!evidence) {
- console.log(
- ` ā ļø Evidence ${evidenceId} not found for requirement ${requirement.id}, skipping`
- );
- continue;
+ console.log(`š Processing ${evidenceRequirements.length} evidence requirements`);
+ for (const requirement of evidenceRequirements) {
+ // Get the controls file for this requirement to extract the evidenceId
+ const control = await prisma.control.findUnique({
+ where: { id: requirement.controlId },
+ include: { frameworkCategory: true },
+ });
+ if (!control) {
+ console.log(` ā ļø Control not found for requirement ${requirement.id}, skipping`);
+ continue;
+ }
+ if (!control.frameworkCategory) {
+ console.log(` ā ļø Framework category not found for control ${control.id}, skipping`);
+ continue;
+ }
+ // Get the framework ID from the category
+ const frameworkId = control.frameworkCategory.frameworkId;
+ // Get the controls data from the file
+ const controlsFile = (0, node_path_1.join)(__dirname, `../../data/controls/${frameworkId}.json`);
+ const controlsData = JSON.parse(node_fs_2.default.readFileSync(controlsFile, "utf8"));
+ // Find the requirement in the control data
+ const controlData = controlsData[control.code];
+ if (!controlData) {
+ console.log(` ā ļø Control data not found for ${control.code} in framework ${frameworkId}, skipping`);
+ continue;
+ }
+ const reqData = controlData.requirements.find((req) => req.id === requirement.id);
+ if (!reqData) {
+ console.log(` ā ļø Requirement data not found for ${requirement.id} in control ${control.code}, skipping`);
+ continue;
+ }
+ // Get the evidenceId from the requirement data
+ const evidenceId = reqData.evidenceId;
+ if (!evidenceId) {
+ console.log(` ā ļø No evidenceId found for requirement ${requirement.id}, skipping`);
+ continue;
+ }
+ // Verify the evidence exists
+ const evidence = await prisma.evidence.findUnique({
+ where: { id: evidenceId },
+ });
+ if (!evidence) {
+ console.log(` ā ļø Evidence ${evidenceId} not found for requirement ${requirement.id}, skipping`);
+ continue;
+ }
+ console.log(` ā³ Linking requirement ${requirement.id} to evidence ${evidenceId}...`);
+ // Update the control requirement to link to the evidence
+ await prisma.controlRequirement.update({
+ where: {
+ id: requirement.id,
+ },
+ data: {
+ evidenceId: evidenceId,
+ name: evidence.name,
+ description: evidence.description,
+ frequency: evidence.frequency,
+ department: evidence.department,
+ },
+ });
+ console.log(` ā
Requirement ${requirement.id} linked to evidence ${evidenceId}`);
}
- console.log(
- ` ā³ Linking requirement ${requirement.id} to evidence ${evidenceId}...`
- );
- // Update the control requirement to link to the evidence
- await prisma.controlRequirement.update({
- where: {
- id: requirement.id,
- },
- data: {
- evidenceId: evidenceId,
- name: evidence.name,
- description: evidence.description,
- frequency: evidence.frequency,
- department: evidence.department,
- },
- });
- console.log(
- ` ā
Requirement ${requirement.id} linked to evidence ${evidenceId}`
- );
- }
}
// Phase 1: Update control requirements to link to policies
async function updatePolicyLinks() {
- // Get all control requirements that are policy type
- const policyRequirements = await prisma.controlRequirement.findMany({
- where: {
- type: client_2.RequirementType.policy,
- },
- });
- console.log(`š Processing ${policyRequirements.length} policy requirements`);
- for (const requirement of policyRequirements) {
- // Get the controls file for this requirement to extract the policyId
- const control = await prisma.control.findUnique({
- where: { id: requirement.controlId },
- include: { frameworkCategory: true },
- });
- if (!control) {
- console.log(
- ` ā ļø Control not found for requirement ${requirement.id}, skipping`
- );
- continue;
- }
- if (!control.frameworkCategory) {
- console.log(
- ` ā ļø Framework category not found for control ${control.id}, skipping`
- );
- continue;
- }
- // Get the framework ID from the category
- const frameworkId = control.frameworkCategory.frameworkId;
- // Get the controls data from the file
- const controlsFile = (0, node_path_1.join)(
- __dirname,
- `../../data/controls/${frameworkId}.json`
- );
- const controlsData = JSON.parse(
- node_fs_2.default.readFileSync(controlsFile, "utf8")
- );
- // Find the requirement in the control data
- const controlData = controlsData[control.code];
- if (!controlData) {
- console.log(
- ` ā ļø Control data not found for ${control.code} in framework ${frameworkId}, skipping`
- );
- continue;
- }
- const reqData = controlData.requirements.find(
- (req) => req.id === requirement.id
- );
- if (!reqData) {
- console.log(
- ` ā ļø Requirement data not found for ${requirement.id} in control ${control.code}, skipping`
- );
- continue;
- }
- // Get the policyId from the requirement data
- const policyId = reqData.policyId;
- if (!policyId) {
- console.log(
- ` ā ļø No policyId found for requirement ${requirement.id}, skipping`
- );
- continue;
- }
- // Verify the policy exists
- const policy = await prisma.policy.findUnique({
- where: { id: policyId },
+ // Get all control requirements that are policy type
+ const policyRequirements = await prisma.controlRequirement.findMany({
+ where: {
+ type: client_2.RequirementType.policy,
+ },
});
- if (!policy) {
- console.log(
- ` ā ļø Policy ${policyId} not found for requirement ${requirement.id}, skipping`
- );
- continue;
+ console.log(`š Processing ${policyRequirements.length} policy requirements`);
+ for (const requirement of policyRequirements) {
+ // Get the controls file for this requirement to extract the policyId
+ const control = await prisma.control.findUnique({
+ where: { id: requirement.controlId },
+ include: { frameworkCategory: true },
+ });
+ if (!control) {
+ console.log(` ā ļø Control not found for requirement ${requirement.id}, skipping`);
+ continue;
+ }
+ if (!control.frameworkCategory) {
+ console.log(` ā ļø Framework category not found for control ${control.id}, skipping`);
+ continue;
+ }
+ // Get the framework ID from the category
+ const frameworkId = control.frameworkCategory.frameworkId;
+ // Get the controls data from the file
+ const controlsFile = (0, node_path_1.join)(__dirname, `../../data/controls/${frameworkId}.json`);
+ const controlsData = JSON.parse(node_fs_2.default.readFileSync(controlsFile, "utf8"));
+ // Find the requirement in the control data
+ const controlData = controlsData[control.code];
+ if (!controlData) {
+ console.log(` ā ļø Control data not found for ${control.code} in framework ${frameworkId}, skipping`);
+ continue;
+ }
+ const reqData = controlData.requirements.find((req) => req.id === requirement.id);
+ if (!reqData) {
+ console.log(` ā ļø Requirement data not found for ${requirement.id} in control ${control.code}, skipping`);
+ continue;
+ }
+ // Get the policyId from the requirement data
+ const policyId = reqData.policyId;
+ if (!policyId) {
+ console.log(` ā ļø No policyId found for requirement ${requirement.id}, skipping`);
+ continue;
+ }
+ // Verify the policy exists
+ const policy = await prisma.policy.findUnique({
+ where: { id: policyId },
+ });
+ if (!policy) {
+ console.log(` ā ļø Policy ${policyId} not found for requirement ${requirement.id}, skipping`);
+ continue;
+ }
+ console.log(` ā³ Linking requirement ${requirement.id} to policy ${policyId}...`);
+ // Update the control requirement to link to the policy
+ await prisma.controlRequirement.update({
+ where: {
+ id: requirement.id,
+ },
+ data: {
+ policyId: policyId,
+ name: policy.name,
+ description: policy.description || "",
+ frequency: policy.frequency,
+ department: policy.department ?? client_1.Departments.none,
+ },
+ });
+ console.log(` ā
Requirement ${requirement.id} linked to policy ${policyId}`);
}
- console.log(
- ` ā³ Linking requirement ${requirement.id} to policy ${policyId}...`
- );
- // Update the control requirement to link to the policy
- await prisma.controlRequirement.update({
- where: {
- id: requirement.id,
- },
- data: {
- policyId: policyId,
- name: policy.name,
- description: policy.description || "",
- frequency: policy.frequency,
- department: policy.department ?? client_1.Departments.none,
- },
- });
- console.log(
- ` ā
Requirement ${requirement.id} linked to policy ${policyId}`
- );
- }
}
async function seedTrainingVideos() {
- for (const video of data_1.trainingVideos) {
- await prisma.portalTrainingVideos.upsert({
- where: { id: video.id },
- update: {
- title: video.title,
- description: video.description,
- videoUrl: video.url,
- youtubeId: video.youtubeId,
- },
- create: {
- id: video.id,
- title: video.title,
- description: video.description,
- videoUrl: video.url,
- youtubeId: video.youtubeId,
- },
- });
- }
+ console.log(`š Seeding ${data_1.trainingVideos.length} training videos...`);
+ for (const video of data_1.trainingVideos) {
+ console.log(` ā³ Processing video: ${video.title}...`);
+ await prisma.portalTrainingVideos.upsert({
+ where: { id: video.id },
+ update: {
+ title: video.title,
+ description: video.description,
+ videoUrl: video.url,
+ youtubeId: video.youtubeId,
+ },
+ create: {
+ id: video.id,
+ title: video.title,
+ description: video.description,
+ videoUrl: video.url,
+ youtubeId: video.youtubeId,
+ },
+ });
+ console.log(` ā
Video ${video.title} processed`);
+ }
+ console.log("ā
Training videos seeded");
}