diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirements.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirements.tsx index 6fb83a741c..17fcab1680 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirements.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/components/FrameworkRequirements.tsx @@ -1,21 +1,27 @@ "use client"; +import { DataTable } from "@/components/data-table/data-table"; +import { DataTableColumnHeader } from "@/components/data-table/data-table-column-header"; +import { DataTableSortList } from "@/components/data-table/data-table-sort-list"; +import { useDataTable } from "@/hooks/use-data-table"; import { useI18n } from "@/locales/client"; import { FrameworkId } from "@comp/db/types"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@comp/ui/table"; -import Link from "next/link"; +import { Card, CardTitle, CardHeader, CardContent } from "@comp/ui/card"; +import { Input } from "@comp/ui/input"; +import { ColumnDef } from "@tanstack/react-table"; import { useParams } from "next/navigation"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import { getFrameworkRequirements } from "../../lib/getFrameworkRequirements"; import type { FrameworkInstanceWithControls } from "../../types"; +type RequirementItem = { + id: string; + compositeId: string; + name: string; + description: string; + mappedControlsCount: number; +}; + export function FrameworkRequirements({ frameworkId, frameworkInstanceWithControls, @@ -28,6 +34,7 @@ export function FrameworkRequirements({ orgId: string; frameworkInstanceId: string; }>(); + const [searchTerm, setSearchTerm] = useState(""); const requirements = useMemo(() => { const reqs = getFrameworkRequirements(frameworkId); @@ -49,67 +56,118 @@ export function FrameworkRequirements({ }); }, [frameworkId, frameworkInstanceWithControls.controls]); + // Define columns for requirements table + const columns = useMemo[]>( + () => [ + { + accessorKey: "id", + header: ({ column }) => ( + + ), + cell: ({ row }) => {row.original.name}, + enableSorting: true, + size: 200, + minSize: 150, + maxSize: 250, + enableResizing: true, + }, + { + accessorKey: "description", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + {row.original.description} + + ), + enableSorting: true, + size: 500, + minSize: 300, + maxSize: 700, + enableResizing: true, + }, + { + accessorKey: "mappedControlsCount", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + {row.original.mappedControlsCount} + + ), + enableSorting: true, + size: 150, + minSize: 100, + maxSize: 200, + enableResizing: true, + }, + ], + [t], + ); + + // Filter requirements data based on search term + const filteredRequirements = useMemo(() => { + if (!requirements?.length) return []; + if (!searchTerm.trim()) return requirements; + + const searchLower = searchTerm.toLowerCase(); + return requirements.filter( + (requirement) => + requirement.id.toLowerCase().includes(searchLower) || + requirement.name.toLowerCase().includes(searchLower) || + requirement.description.toLowerCase().includes(searchLower), + ); + }, [requirements, searchTerm]); + + // Set up the requirements table + const table = useDataTable({ + data: filteredRequirements, + columns, + pageCount: 1, + shallow: false, + getRowId: (row) => row.id, + initialState: { + sorting: [{ id: "id", desc: false }], + }, + }); + if (!requirements?.length) { return null; } return ( -
- - - - ID - Name - - Description - - - {t("frameworks.controls.title")} - - - - - {requirements.map((requirement) => ( - - - - {requirement.id} - - - - - {requirement.name} - - - - - {requirement.description} - - - - - {requirement.mappedControlsCount} - - - - ))} - -
-
+ + + + {t("frameworks.requirements.requirements")} ( + {filteredRequirements.length}) + + + +
+ setSearchTerm(e.target.value)} + className="max-w-sm" + /> +
+ +
+
+ row.id} + /> +
+
); } diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/table/RequirementControlsTable.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/table/RequirementControlsTable.tsx index 56d4483873..7b2215353a 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/table/RequirementControlsTable.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/frameworks/[frameworkInstanceId]/requirements/[requirementKey]/components/table/RequirementControlsTable.tsx @@ -1,19 +1,24 @@ "use client"; +import { useI18n } from "@/locales/client"; +import { DataTable } from "@/components/data-table/data-table"; +import { DataTableColumnHeader } from "@/components/data-table/data-table-column-header"; +import { useDataTable } from "@/hooks/use-data-table"; +import { useParams } from "next/navigation"; +import { useMemo, useState } from "react"; +import Link from "next/link"; +import { ColumnDef } from "@tanstack/react-table"; +import { DisplayFrameworkStatus } from "@/components/frameworks/framework-status"; import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@comp/ui/table"; -import { - useReactTable, - getCoreRowModel, - flexRender, -} from "@tanstack/react-table"; -import { RequirementControlsTableColumns } from "./RequirementControlsTableColumns"; + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@comp/ui/tooltip"; +import { Input } from "@comp/ui/input"; +import { DataTableSortList } from "@/components/data-table/data-table-sort-list"; +import { getControlStatus } from "../../../../../lib/utils"; +import { isArtifactCompleted } from "@/app/[locale]/(app)/(dashboard)/[orgId]/lib/utils/control-compliance"; import type { Control } from "@comp/db/types"; import type { Artifact, Evidence, Policy } from "@comp/db/types"; @@ -29,62 +34,141 @@ interface RequirementControlsTableProps { export function RequirementControlsTable({ controls, }: RequirementControlsTableProps) { - const table = useReactTable({ - data: controls, - columns: RequirementControlsTableColumns(), - getCoreRowModel: getCoreRowModel(), + const t = useI18n(); + const { orgId } = useParams<{ orgId: string }>(); + const [searchTerm, setSearchTerm] = useState(""); + + // Define columns for the controls table + const columns = useMemo< + ColumnDef< + Control & { + artifacts: (Artifact & { + policy: Policy | null; + evidence: Evidence | null; + })[]; + } + >[] + >( + () => [ + { + id: "name", + accessorKey: "name", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ + {row.original.name} + +
+ ), + enableSorting: true, + size: 300, + minSize: 200, + maxSize: 400, + enableResizing: true, + }, + { + id: "status", + accessorKey: "artifacts", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const artifacts = row.original.artifacts; + const status = getControlStatus(artifacts); + + const totalArtifacts = artifacts.length; + const completedArtifacts = + artifacts.filter(isArtifactCompleted).length; + + return ( + + + +
+ +
+
+ +
+

+ Progress:{" "} + {Math.round( + (completedArtifacts / totalArtifacts) * 100, + ) || 0} + % +

+

+ Completed: {completedArtifacts}/{totalArtifacts} artifacts +

+
+
+
+
+ ); + }, + enableSorting: true, + size: 200, + minSize: 150, + maxSize: 250, + enableResizing: true, + }, + ], + [t, orgId], + ); + + // Filter controls data based on search term + const filteredControls = useMemo(() => { + if (!controls?.length) return []; + if (!searchTerm.trim()) return controls; + + const searchLower = searchTerm.toLowerCase(); + return controls.filter((control) => + control.name.toLowerCase().includes(searchLower), + ); + }, [controls, searchTerm]); + + // Set up the controls table + const table = useDataTable({ + data: filteredControls, + columns, + pageCount: 1, + shallow: false, + getRowId: (row) => row.id, + initialState: { + sorting: [{ id: "name", desc: false }], + }, }); return ( -
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ))} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ))} - - )) - ) : ( - - - No controls found. - - - )} - -
+
+
+ setSearchTerm(e.target.value)} + className="max-w-sm" + /> +
+ +
+ row.id} + />
); } 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 cfa7780f97..715b31938d 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/layout.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/layout.tsx @@ -1,13 +1,12 @@ -import { auth } from "@comp/auth"; +import { AnimatedLayout } from "@/components/animated-layout"; import { Header } from "@/components/header"; +import { AssistantSheet } from "@/components/sheets/assistant-sheet"; import { Sidebar } from "@/components/sidebar"; -import { AnimatedLayout } from "@/components/animated-layout"; import { SidebarProvider } from "@/context/sidebar-context"; -import { cookies, headers } from "next/headers"; +import { auth } from "@comp/auth"; import dynamic from "next/dynamic"; +import { cookies, headers } from "next/headers"; import { redirect } from "next/navigation"; -import { UserProvider } from "@/store/user/provider"; -import { AssistantSheet } from "@/components/sheets/assistant-sheet"; const HotKeys = dynamic( () => import("@/components/hot-keys").then((mod) => mod.HotKeys), diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/RiskRegisterTable.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/RiskRegisterTable.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/RiskRegisterTable.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/RiskRegisterTable.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/components/table/RiskRegisterColumns.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/components/table/RiskRegisterColumns.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/components/table/RiskRegisterColumns.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/components/table/RiskRegisterColumns.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/components/table/RiskRegisterFilters.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/components/table/RiskRegisterFilters.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/components/table/RiskRegisterFilters.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/components/table/RiskRegisterFilters.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/data/getRisks.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/data/getRisks.ts similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/data/getRisks.ts rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/data/getRisks.ts diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/page.tsx index 553afbefdf..7682b85f6d 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/page.tsx @@ -1,38 +1,50 @@ -import { auth } from "@comp/auth"; -import { RiskOverview } from "@/components/risks/charts/risk-overview"; -import { RisksAssignee } from "@/components/risks/charts/RisksAssignee"; -import { getI18n } from "@/locales/server"; +import { RiskRegisterTable } from "./RiskRegisterTable"; import type { Metadata } from "next"; +import { getI18n } from "@/locales/server"; import { setStaticParamsLocale } from "next-international/server"; +import type { RiskStatus, Departments } from "@comp/db/types"; +import { getRisks } from "./data/getRisks"; +import { auth } from "@comp/auth"; +import { cache } from "react"; +import { db } from "@comp/db"; import { headers } from "next/headers"; -import { redirect } from "next/navigation"; +import PageWithBreadcrumb from "@/components/pages/PageWithBreadcrumb"; -export default async function RiskManagement({ +export default async function RiskRegisterPage({ params, }: { - params: Promise<{ locale: string }>; + params: Promise<{ + locale: string; + search: string; + page: number; + pageSize: number; + status: RiskStatus | null; + department: Departments | null; + assigneeId: string | null; + }>; }) { - const { locale } = await params; - const session = await auth.api.getSession({ - headers: await headers(), - }); + const { search, page, pageSize, status, department, assigneeId } = + await params; - if (!session || !session.session.activeOrganizationId) { - redirect("/"); - } + const risks = await getRisks({ + search: search || "", + page: page || 1, + pageSize: pageSize || 10, + status: status || null, + department: department || null, + assigneeId: assigneeId || null, + }); - setStaticParamsLocale(locale); + const assignees = await getAssignees(); return ( -
-
- -
- -
- -
-
+ + + ); } @@ -46,6 +58,29 @@ export async function generateMetadata({ const t = await getI18n(); return { - title: t("sidebar.risk"), + title: t("risk.register.title"), }; } + +const getAssignees = cache(async () => { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session || !session.session.activeOrganizationId) { + return []; + } + + return await db.member.findMany({ + where: { + organizationId: session.session.activeOrganizationId, + isActive: true, + role: { + notIn: ["employee"], + }, + }, + include: { + user: true, + }, + }); +}); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/search-params.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/search-params.ts similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/search-params.ts rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/search-params.ts diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/layout.tsx deleted file mode 100644 index 6818a92b50..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/layout.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { auth } from "@comp/auth"; -import { getI18n } from "@/locales/server"; -import { SecondaryMenu } from "@comp/ui/secondary-menu"; -import { headers } from "next/headers"; -import { redirect } from "next/navigation"; -interface LayoutProps { - children: React.ReactNode; - params: Promise<{ riskId: string }>; -} - -export default async function Layout({ children, params }: LayoutProps) { - const t = await getI18n(); - - const session = await auth.api.getSession({ - headers: await headers(), - }); - - if (!session || !session.session.activeOrganizationId) { - redirect("/"); - } - - const { riskId } = await params; - - if (!riskId) { - redirect(`/${session.session.activeOrganizationId}/risk`); - } - - const orgId = session.session.activeOrganizationId; - - return ( -
- -
{children}
-
- ); -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/page.tsx index 49a17b855e..ce6cb70053 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/page.tsx @@ -1,3 +1,4 @@ +import PageWithBreadcrumb from "@/components/pages/PageWithBreadcrumb"; import { InherentRiskChart } from "@/components/risks/charts/inherent-risk-chart"; import { ResidualRiskChart } from "@/components/risks/charts/residual-risk-chart"; import { RiskOverview } from "@/components/risks/risk-overview"; @@ -20,11 +21,11 @@ interface PageProps { page?: string; per_page?: string; }>; - params: Promise<{ riskId: string; locale: string }>; + params: Promise<{ riskId: string; locale: string; orgId: string }>; } export default async function RiskPage({ searchParams, params }: PageProps) { - const { riskId } = await params; + const { riskId, orgId } = await params; const risk = await getRisk(riskId); const assignees = await getAssignees(); @@ -56,14 +57,20 @@ export default async function RiskPage({ searchParams, params }: PageProps) { } return ( -
- - -
- - + +
+ +
+ + +
-
+ ); } diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/backup-overview/layout.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/layout.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/backup-overview/layout.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/loading.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/backup-overview/loading.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/loading.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/backup-overview/loading.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/backup-overview/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/backup-overview/page.tsx new file mode 100644 index 0000000000..553afbefdf --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/backup-overview/page.tsx @@ -0,0 +1,51 @@ +import { auth } from "@comp/auth"; +import { RiskOverview } from "@/components/risks/charts/risk-overview"; +import { RisksAssignee } from "@/components/risks/charts/RisksAssignee"; +import { getI18n } from "@/locales/server"; +import type { Metadata } from "next"; +import { setStaticParamsLocale } from "next-international/server"; +import { headers } from "next/headers"; +import { redirect } from "next/navigation"; + +export default async function RiskManagement({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session || !session.session.activeOrganizationId) { + redirect("/"); + } + + setStaticParamsLocale(locale); + + return ( +
+
+ +
+ +
+ +
+
+ ); +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + setStaticParamsLocale(locale); + const t = await getI18n(); + + return { + title: t("sidebar.risk"), + }; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/layout.tsx new file mode 100644 index 0000000000..f7906940e3 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/layout.tsx @@ -0,0 +1,11 @@ +export default async function Layout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+
{children}
+
+ ); +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/layout.tsx deleted file mode 100644 index a8684a5d89..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/layout.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { auth } from "@comp/auth"; -import { getI18n } from "@/locales/server"; -import { SecondaryMenu } from "@comp/ui/secondary-menu"; -import { headers } from "next/headers"; - -export default async function Layout({ - children, -}: { - children: React.ReactNode; -}) { - const t = await getI18n(); - - const session = await auth.api.getSession({ - headers: await headers(), - }); - - const user = session?.user; - const orgId = session?.session.activeOrganizationId; - - return ( -
- - -
{children}
-
- ); -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/page.tsx deleted file mode 100644 index 1776c97d1e..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/page.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { RiskRegisterTable } from "./RiskRegisterTable"; -import type { Metadata } from "next"; -import { getI18n } from "@/locales/server"; -import { setStaticParamsLocale } from "next-international/server"; -import type { RiskStatus, Departments } from "@comp/db/types"; -import { getRisks } from "./data/getRisks"; -import { auth } from "@comp/auth"; -import { cache } from "react"; -import { db } from "@comp/db"; -import { headers } from "next/headers"; - -export default async function RiskRegisterPage({ - params, -}: { - params: Promise<{ - locale: string; - search: string; - page: number; - pageSize: number; - status: RiskStatus | null; - department: Departments | null; - assigneeId: string | null; - }>; -}) { - const { search, page, pageSize, status, department, assigneeId } = - await params; - - const risks = await getRisks({ - search: search || "", - page: page || 1, - pageSize: pageSize || 10, - status: status || null, - department: department || null, - assigneeId: assigneeId || null, - }); - - const assignees = await getAssignees(); - - return ( - - ); -} - -export async function generateMetadata({ - params, -}: { - params: Promise<{ locale: string }>; -}): Promise { - const { locale } = await params; - setStaticParamsLocale(locale); - const t = await getI18n(); - - return { - title: t("risk.register.title"), - }; -} - -const getAssignees = cache(async () => { - const session = await auth.api.getSession({ - headers: await headers(), - }); - - if (!session || !session.session.activeOrganizationId) { - return []; - } - - return await db.member.findMany({ - where: { - organizationId: session.session.activeOrganizationId, - isActive: true, - role: { - notIn: ["employee"], - }, - }, - include: { - user: true, - }, - }); -}); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/register/components/VendorRegisterColumns.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/components/VendorRegisterColumns.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/register/components/VendorRegisterColumns.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/components/VendorRegisterColumns.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/register/components/VendorRegisterFilters.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/components/VendorRegisterFilters.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/register/components/VendorRegisterFilters.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/components/VendorRegisterFilters.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/register/components/VendorRegisterTable.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/components/VendorRegisterTable.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/register/components/VendorRegisterTable.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/components/VendorRegisterTable.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/page.tsx index a6485e72f2..9bc81c18a8 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/page.tsx @@ -1,33 +1,87 @@ -import { VendorOverview } from "@/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/components/charts/vendor-overview"; -import { getServersideSession } from "@/lib/get-session"; +import { auth } from "@comp/auth"; import { getI18n } from "@/locales/server"; +import { db } from "@comp/db"; import type { Metadata } from "next"; import { setStaticParamsLocale } from "next-international/server"; -import { headers } from "next/headers"; import { redirect } from "next/navigation"; +import { VendorRegisterTable } from "./components/VendorRegisterTable"; +import { Departments, VendorStatus } from "@comp/db/types"; +import { z } from "zod"; +import { headers } from "next/headers"; +import { getServersideSession } from "@/lib/get-session"; +import { cache } from "react"; +import PageWithBreadcrumb from "@/components/pages/PageWithBreadcrumb"; -export default async function VendorManagement({ +export default async function Page({ + searchParams, params, }: { - params: Promise<{ locale: string }>; + searchParams: Promise<{ + createVendorSheet?: string; + page?: string; + pageSize?: string; + status?: string; + department?: string; + assigneeId?: string; + }>; + params: Promise<{ orgId: string }>; }) { - const { locale } = await params; - setStaticParamsLocale(locale); + const searchParamsSchema = z.object({ + createVendorSheet: z.string().optional(), + page: z.string().regex(/^\d+$/).transform(Number).optional(), + pageSize: z.string().regex(/^\d+$/).transform(Number).optional(), + status: z.nativeEnum(VendorStatus).optional(), + department: z.nativeEnum(Departments).optional(), + assigneeId: z.string().uuid().optional(), + }); - const { - session: { activeOrganizationId }, - } = await getServersideSession({ + const result = searchParamsSchema.safeParse(await searchParams); + const { orgId } = await params; + + if (!result.success) { + console.error("Invalid search params:", result.error); + redirect("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/vendors/register"); + } + + const { createVendorSheet, page, pageSize, status, department, assigneeId } = + result.data; + + const session = await auth.api.getSession({ headers: await headers(), }); - if (!activeOrganizationId) { - redirect("/"); + if (!session?.session.activeOrganizationId) { + redirect("/onboarding"); } + const vendors = await db.vendor.findMany({ + where: { + organizationId: session.session.activeOrganizationId, + ...(status && { status: status }), + ...(department && { department: department }), + ...(assigneeId && { assigneeId: assigneeId }), + }, + include: { + assignee: { + select: { + user: true, + }, + }, + }, + skip: page ? (Number(page) - 1) * Number(pageSize || 10) : 0, + take: Number(pageSize || 10), + }); + + const assignees = await getAssignees(); + return ( -
- -
+ + + ); } @@ -41,6 +95,32 @@ export async function generateMetadata({ const t = await getI18n(); return { - title: t("sidebar.vendors"), + title: t("vendors.register.title"), }; } + +const getAssignees = cache(async () => { + const { + session: { activeOrganizationId }, + } = await getServersideSession({ + headers: await headers(), + }); + + if (!activeOrganizationId) { + return []; + } + + const assignees = await db.member.findMany({ + where: { + organizationId: activeOrganizationId, + role: { + notIn: ["employee"], + }, + }, + include: { + user: true, + }, + }); + + return assignees; +}); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/layout.tsx deleted file mode 100644 index a7ca1c753c..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/layout.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { auth } from "@comp/auth"; -import { getI18n } from "@/locales/server"; -import { db } from "@comp/db"; -import { SecondaryMenu } from "@comp/ui/secondary-menu"; -import { redirect } from "next/navigation"; -import { headers } from "next/headers"; - -interface LayoutProps { - children: React.ReactNode; - params: Promise<{ vendorId: string }>; -} - -export default async function Layout({ children, params }: LayoutProps) { - const t = await getI18n(); - - const session = await auth.api.getSession({ - headers: await headers(), - }); - - if (!session || !session.session.activeOrganizationId) { - redirect("/"); - } - - const { vendorId } = await params; - - if (!vendorId) { - redirect(`/${session.session.activeOrganizationId}/vendors`); - } - - const orgId = session.session.activeOrganizationId; - - const vendor = await db.vendor.findUnique({ - where: { - id: vendorId, - organizationId: orgId, - }, - }); - - if (!vendor) { - redirect(`/${orgId}/vendors/register`); - } - - return ( -
- -
{children}
-
- ); -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/page.tsx index 03a3ec3edb..fed2fd998a 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/page.tsx @@ -12,6 +12,7 @@ import { InherentRiskVendorChart } from "./components/inherent-risk-vendor-chart import { ResidualRiskVendorChart } from "./components/residual-risk-vendor-chart"; import { SecondaryFields } from "./components/secondary-fields/secondary-fields"; import { TitleAndDescription } from "./components/title-and-description/title-and-description"; +import PageWithBreadcrumb from "@/components/pages/PageWithBreadcrumb"; interface PageProps { searchParams: Promise<{ @@ -21,11 +22,11 @@ interface PageProps { page?: string; per_page?: string; }>; - params: Promise<{ vendorId: string; locale: string }>; + params: Promise<{ vendorId: string; locale: string; orgId: string }>; } export default async function VendorPage({ searchParams, params }: PageProps) { - const { vendorId } = await params; + const { vendorId, orgId } = await params; const vendor = await getVendor(vendorId); const assignees = await getAssignees(); const t = await getI18n(); @@ -56,47 +57,21 @@ export default async function VendorPage({ searchParams, params }: PageProps) { } return ( -
- - - {/* - - -
- {t("vendors.tasks.title")} -
-
-
- -
- {loadedTasks.length > 0 ? ( - <> - - - - ) : hasFilters ? ( - - ) : ( - <> - - - - )} -
-
-
*/} -
- - + +
+ + +
+ + +
-
+ ); } diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/components/charts/category-chart.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/backup-overview/components/charts/category-chart.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/components/charts/category-chart.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/backup-overview/components/charts/category-chart.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/components/charts/status-chart.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/backup-overview/components/charts/status-chart.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/components/charts/status-chart.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/backup-overview/components/charts/status-chart.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/components/charts/vendor-overview.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/backup-overview/components/charts/vendor-overview.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/components/charts/vendor-overview.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/backup-overview/components/charts/vendor-overview.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/components/charts/vendors-by-category.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/backup-overview/components/charts/vendors-by-category.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/components/charts/vendors-by-category.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/backup-overview/components/charts/vendors-by-category.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/components/charts/vendors-by-status.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/backup-overview/components/charts/vendors-by-status.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/components/charts/vendors-by-status.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/backup-overview/components/charts/vendors-by-status.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/backup-overview/layout.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/layout.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/backup-overview/layout.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/loading.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/backup-overview/loading.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/loading.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/backup-overview/loading.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/backup-overview/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/backup-overview/page.tsx new file mode 100644 index 0000000000..b8d3c8f023 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/backup-overview/page.tsx @@ -0,0 +1,46 @@ +import { VendorOverview } from "@/app/[locale]/(app)/(dashboard)/[orgId]/vendors/backup-overview/components/charts/vendor-overview"; +import { getServersideSession } from "@/lib/get-session"; +import { getI18n } from "@/locales/server"; +import type { Metadata } from "next"; +import { setStaticParamsLocale } from "next-international/server"; +import { headers } from "next/headers"; +import { redirect } from "next/navigation"; + +export default async function VendorManagement({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + setStaticParamsLocale(locale); + + const { + session: { activeOrganizationId }, + } = await getServersideSession({ + headers: await headers(), + }); + + if (!activeOrganizationId) { + redirect("/"); + } + + return ( +
+ +
+ ); +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + setStaticParamsLocale(locale); + const t = await getI18n(); + + return { + title: t("sidebar.vendors"), + }; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/layout.tsx new file mode 100644 index 0000000000..f7906940e3 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/layout.tsx @@ -0,0 +1,11 @@ +export default async function Layout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+
{children}
+
+ ); +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/register/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/register/layout.tsx deleted file mode 100644 index 84ab7ed093..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/register/layout.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { auth } from "@comp/auth"; -import { getI18n } from "@/locales/server"; -import { SecondaryMenu } from "@comp/ui/secondary-menu"; -import { headers } from "next/headers"; - -export default async function Layout({ - children, -}: { - children: React.ReactNode; -}) { - const t = await getI18n(); - - const session = await auth.api.getSession({ - headers: await headers(), - }); - - const orgId = session?.session.activeOrganizationId; - - return ( -
- - -
{children}
-
- ); -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/register/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/register/page.tsx deleted file mode 100644 index 4af3020c36..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/register/page.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { auth } from "@comp/auth"; -import { getI18n } from "@/locales/server"; -import { db } from "@comp/db"; -import type { Metadata } from "next"; -import { setStaticParamsLocale } from "next-international/server"; -import { redirect } from "next/navigation"; -import { VendorRegisterTable } from "./components/VendorRegisterTable"; -import { Departments, VendorStatus } from "@comp/db/types"; -import { z } from "zod"; -import { headers } from "next/headers"; -import { getServersideSession } from "@/lib/get-session"; -import { cache } from "react"; - -export default async function Page({ - searchParams, -}: { - searchParams: Promise<{ - createVendorSheet?: string; - page?: string; - pageSize?: string; - status?: string; - department?: string; - assigneeId?: string; - }>; -}) { - const searchParamsSchema = z.object({ - createVendorSheet: z.string().optional(), - page: z.string().regex(/^\d+$/).transform(Number).optional(), - pageSize: z.string().regex(/^\d+$/).transform(Number).optional(), - status: z.nativeEnum(VendorStatus).optional(), - department: z.nativeEnum(Departments).optional(), - assigneeId: z.string().uuid().optional(), - }); - - const result = searchParamsSchema.safeParse(await searchParams); - - if (!result.success) { - console.error("Invalid search params:", result.error); - redirect("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/vendors/register"); - } - - const { createVendorSheet, page, pageSize, status, department, assigneeId } = - result.data; - - const session = await auth.api.getSession({ - headers: await headers(), - }); - - if (!session?.session.activeOrganizationId) { - redirect("/onboarding"); - } - - const vendors = await db.vendor.findMany({ - where: { - organizationId: session.session.activeOrganizationId, - ...(status && { status: status }), - ...(department && { department: department }), - ...(assigneeId && { assigneeId: assigneeId }), - }, - include: { - assignee: { - select: { - user: true, - }, - }, - }, - skip: page ? (Number(page) - 1) * Number(pageSize || 10) : 0, - take: Number(pageSize || 10), - }); - - const assignees = await getAssignees(); - - return ; -} - -export async function generateMetadata({ - params, -}: { - params: Promise<{ locale: string }>; -}): Promise { - const { locale } = await params; - setStaticParamsLocale(locale); - const t = await getI18n(); - - return { - title: t("vendors.register.title"), - }; -} - -const getAssignees = cache(async () => { - const { - session: { activeOrganizationId }, - } = await getServersideSession({ - headers: await headers(), - }); - - if (!activeOrganizationId) { - return []; - } - - const assignees = await db.member.findMany({ - where: { - organizationId: activeOrganizationId, - role: { - notIn: ["employee"], - }, - }, - include: { - user: true, - }, - }); - - return assignees; -}); diff --git a/apps/app/src/hooks/use-data-table.ts b/apps/app/src/hooks/use-data-table.ts index 7c5e29d990..d7169eddad 100644 --- a/apps/app/src/hooks/use-data-table.ts +++ b/apps/app/src/hooks/use-data-table.ts @@ -282,6 +282,7 @@ export function useDataTable(props: UseDataTableProps) { enableColumnFilter: false, }, enableRowSelection: true, + enableColumnResizing: true, onRowSelectionChange: setRowSelection, onPaginationChange, onSortingChange, diff --git a/apps/app/src/locales/features/frameworks.ts b/apps/app/src/locales/features/frameworks.ts index 02a66fe0d1..3b68144fef 100644 --- a/apps/app/src/locales/features/frameworks.ts +++ b/apps/app/src/locales/features/frameworks.ts @@ -46,9 +46,13 @@ export const frameworks = { in_progress: "In Progress", not_started: "Not Started", }, + search: { + placeholder: "Search controls...", + }, }, requirements: { requirement: "Requirement", + requirements: "Requirements", title: "Linked Requirements", description: "Review and manage compliance requirements", table: { diff --git a/apps/app/src/locales/features/people.ts b/apps/app/src/locales/features/people.ts index a00cf9ff8a..ff5bbd7490 100644 --- a/apps/app/src/locales/features/people.ts +++ b/apps/app/src/locales/features/people.ts @@ -1,77 +1,77 @@ export const people = { - title: "People", - all: "All Employees", - details: { - title: "Employee Details", - tasks: "Tasks", - }, - status: { - active: "Active", - inactive: "Inactive", - }, - description: "Manage your team members and their roles.", - filters: { - search: "Search people...", - role: "Filter by role", - }, - actions: { - invite: "Add Employee", - clear: "Clear filters", - }, - table: { - name: "Name", - email: "Email", - department: "Department", - status: "Status", - }, - empty: { - no_employees: { - title: "No employees yet", - description: "Get started by inviting your first team member.", - }, - no_results: { - title: "No results found", - description: "No employees match your search", - description_with_filters: "Try adjusting your filters", - }, - }, - invite: { - title: "Add Employee", - description: "Add an employee to your organization.", - email: { - label: "Email address", - placeholder: "Enter email address", - }, - role: { - label: "Role", - placeholder: "Select a role", - }, - name: { - label: "Name", - placeholder: "Enter name", - }, - department: { - label: "Department", - placeholder: "Select a department", - }, - submit: "Add Employee", - success: - "Employee added successfully, they will receive an email to join the portal.", - error: "Failed to add employee", - }, - dashboard: { - title: "Dashboard", - employee_task_completion: "Employee Task Completion", - policies_completed: "Policies Completed", - policies_pending: "Policies Pending", - trainings_completed: "Trainings Completed", - trainings_pending: "Trainings Pending", - policies: "Policies", - trainings: "Trainings", - completed: "Completed", - not_completed: "Not Completed", - no_data: "No employee data available", - no_tasks_completed: "No tasks have been completed yet", - no_tasks_available: "No tasks available to complete", - }, + title: "People", + all: "Employees", + details: { + title: "Employee Details", + tasks: "Tasks", + }, + status: { + active: "Active", + inactive: "Inactive", + }, + description: "Manage your team members and their roles.", + filters: { + search: "Search people...", + role: "Filter by role", + }, + actions: { + invite: "Add Employee", + clear: "Clear filters", + }, + table: { + name: "Name", + email: "Email", + department: "Department", + status: "Status", + }, + empty: { + no_employees: { + title: "No employees yet", + description: "Get started by inviting your first team member.", + }, + no_results: { + title: "No results found", + description: "No employees match your search", + description_with_filters: "Try adjusting your filters", + }, + }, + invite: { + title: "Add Employee", + description: "Add an employee to your organization.", + email: { + label: "Email address", + placeholder: "Enter email address", + }, + role: { + label: "Role", + placeholder: "Select a role", + }, + name: { + label: "Name", + placeholder: "Enter name", + }, + department: { + label: "Department", + placeholder: "Select a department", + }, + submit: "Add Employee", + success: + "Employee added successfully, they will receive an email to join the portal.", + error: "Failed to add employee", + }, + dashboard: { + title: "Dashboard", + employee_task_completion: "Employee Task Completion", + policies_completed: "Policies Completed", + policies_pending: "Policies Pending", + trainings_completed: "Trainings Completed", + trainings_pending: "Trainings Pending", + policies: "Policies", + trainings: "Trainings", + completed: "Completed", + not_completed: "Not Completed", + no_data: "No employee data available", + no_tasks_completed: "No tasks have been completed yet", + no_tasks_available: "No tasks available to complete", + }, } as const;