diff --git a/apps/app/public/onboarding/people-management.webp b/apps/app/public/onboarding/people-management.webp new file mode 100644 index 0000000000..ef7cb35e65 Binary files /dev/null and b/apps/app/public/onboarding/people-management.webp differ diff --git a/apps/app/public/onboarding/risk-management.webp b/apps/app/public/onboarding/risk-management.webp new file mode 100644 index 0000000000..e8a589e989 Binary files /dev/null and b/apps/app/public/onboarding/risk-management.webp differ diff --git a/apps/app/public/onboarding/vendor-management.webp b/apps/app/public/onboarding/vendor-management.webp new file mode 100644 index 0000000000..db9d583248 Binary files /dev/null and b/apps/app/public/onboarding/vendor-management.webp differ diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/dashboard/components/EmployeeCompletionChart.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/(overview)/components/EmployeeCompletionChart.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/dashboard/components/EmployeeCompletionChart.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/(overview)/components/EmployeeCompletionChart.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/dashboard/components/EmployeesOverview.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/(overview)/components/EmployeesOverview.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/dashboard/components/EmployeesOverview.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/(overview)/components/EmployeesOverview.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/(overview)/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/(overview)/layout.tsx new file mode 100644 index 0000000000..0ab3283a57 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/(overview)/layout.tsx @@ -0,0 +1,88 @@ +import { auth } from "@/auth"; +import { AppOnboarding } from "@/components/app-onboarding"; +import { getI18n } from "@/locales/server"; +import { db } from "@bubba/db"; +import { SecondaryMenu } from "@bubba/ui/secondary-menu"; +import { cache, Suspense } from "react"; +import { EmployeeInviteSheet } from "@/components/sheets/add-employee-sheet"; + +export default async function Layout({ + children, +}: { + children: React.ReactNode; +}) { + const t = await getI18n(); + const session = await auth(); + const user = session?.user; + const orgId = user?.organizationId; + + const overview = await getEmployeesOverview(); + + if (overview.length === 0) { + return ( +
+ Loading...
}> +
+ + +
+ + + ); + } + + + return ( +
+ + +
{children}
+
+ ); +} + +const getEmployeesOverview = cache(async () => { + const session = await auth(); + const orgId = session?.user.organizationId; + + const employees = await db.employee.findMany({ + where: { + organizationId: orgId, + }, + }); + + return employees; +}); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/dashboard/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/(overview)/page.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/dashboard/page.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/(overview)/page.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/[employeeId]/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/[employeeId]/layout.tsx new file mode 100644 index 0000000000..c0ff1c7bce --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/[employeeId]/layout.tsx @@ -0,0 +1,30 @@ +import { auth } from "@/auth"; +import { getI18n } from "@/locales/server"; +import { SecondaryMenu } from "@bubba/ui/secondary-menu"; + +export default async function Layout({ + children, +}: { + children: React.ReactNode; +}) { + const t = await getI18n(); + const session = await auth(); + const user = session?.user; + const orgId = user?.organizationId; + + return ( +
+ + +
{children}
+
+ ); +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/all/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/all/layout.tsx new file mode 100644 index 0000000000..c0ff1c7bce --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/all/layout.tsx @@ -0,0 +1,30 @@ +import { auth } from "@/auth"; +import { getI18n } from "@/locales/server"; +import { SecondaryMenu } from "@bubba/ui/secondary-menu"; + +export default async function Layout({ + children, +}: { + children: React.ReactNode; +}) { + const t = await getI18n(); + const session = await auth(); + const user = session?.user; + const orgId = user?.organizationId; + + return ( +
+ + +
{children}
+
+ ); +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/layout.tsx deleted file mode 100644 index 634731c803..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/layout.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { auth } from "@/auth"; -import { getI18n } from "@/locales/server"; -import { SecondaryMenu } from "@bubba/ui/secondary-menu"; - -export default async function Layout({ - children, -}: { - children: React.ReactNode; -}) { - const t = await getI18n(); - const session = await auth(); - const user = session?.user; - const orgId = user?.organizationId; - - return ( -
- - -
{children}
-
- ); -} 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/(overview)/layout.tsx index a4050bd3a9..e0fd1a31b6 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/layout.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/layout.tsx @@ -1,27 +1,95 @@ import { getI18n } from "@/locales/server"; import { SecondaryMenu } from "@bubba/ui/secondary-menu"; import { auth } from "@/auth"; +import { AppOnboarding } from "@/components/app-onboarding"; +import { db } from "@bubba/db"; +import { cache, Suspense } from "react"; +import { CreateRiskSheet } from "@/components/sheets/create-risk-sheet"; export default async function Layout({ - children, + children, }: { - children: React.ReactNode; + children: React.ReactNode; }) { - const t = await getI18n(); - const session = await auth(); - const user = session?.user; - const orgId = user?.organizationId; - - return ( -
- - -
{children}
-
- ); + const t = await getI18n(); + const session = await auth(); + const user = session?.user; + const orgId = user?.organizationId; + + const overview = await getRiskOverview(); + + if (overview?.risks === 0) { + return ( +
+ Loading...
}> +
+ + +
+ + + ); + } + + return ( +
+ Loading...
}> + + +
{children}
+ + + ); } + +const getRiskOverview = cache( + async () => { + const session = await auth(); + + if (!session || !session.user.organizationId) { + return { risks: 0 }; + } + + return await db.$transaction(async (tx) => { + const [risks] = await Promise.all([ + tx.risk.count({ + where: { organizationId: session.user.organizationId }, + }), + ]); + + return { + risks, + }; + }); + }, +); \ No newline at end of file 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/(overview)/loading.tsx new file mode 100644 index 0000000000..d1a694631b --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/loading.tsx @@ -0,0 +1,44 @@ +import { Button } from "@bubba/ui/button"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@bubba/ui/card"; +import { Input } from "@bubba/ui/input"; +import { getI18n } from "@/locales/server"; + +export default async function Loading() { + const t = await getI18n(); + + return ( +
+ + + {t("settings.general.org_name")} + + {t("settings.general.org_name_description")} + + + + + + +
{t("settings.general.org_name_tip")}
+ +
+
+ + + + {t("settings.general.org_website")} + + {t("settings.general.org_website_description")} + + + + + + +
{t("settings.general.org_website_tip")}
+ +
+
+
+ ); +} 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 fb6c04dc88..808ce27ce1 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 @@ -2,11 +2,9 @@ import { auth } from "@/auth"; import { RiskOverview } from "@/components/risks/charts/risk-overview"; import { RisksAssignee } from "@/components/risks/charts/risks-assignee"; import { getI18n } from "@/locales/server"; -import { db } from "@bubba/db"; import type { Metadata } from "next"; import { setStaticParamsLocale } from "next-international/server"; import { redirect } from "next/navigation"; -import { cache } from "react"; export default async function RiskManagement({ params, @@ -21,11 +19,6 @@ export default async function RiskManagement({ } setStaticParamsLocale(locale); - const overview = await getRiskOverview(); - - if (overview?.risks === 0) { - redirect(`/${session.user.organizationId}/risk/register`); - } return (
@@ -40,28 +33,6 @@ export default async function RiskManagement({ ); } -const getRiskOverview = cache( - async () => { - const session = await auth(); - - if (!session || !session.user.organizationId) { - return { risks: 0 }; - } - - return await db.$transaction(async (tx) => { - const [risks] = await Promise.all([ - tx.risk.count({ - where: { organizationId: session.user.organizationId }, - }), - ]); - - return { - risks, - }; - }); - }, -); - export async function generateMetadata({ params, }: { 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/(overview)/components/charts/vendor-overview.tsx index 4d42e62e00..8038d3e5af 100644 --- 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/(overview)/components/charts/vendor-overview.tsx @@ -14,25 +14,14 @@ export async function VendorOverview({ organizationId }: VendorOverviewProps) { return (
- - - -
- {t("vendors.dashboard.title")} -
-
-
- -
-
- -
-
- -
-
-
-
+
+
+ +
+
+ +
+
); } \ No newline at end of file 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/(overview)/layout.tsx index 4e823ea1a2..e318048c17 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/layout.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/layout.tsx @@ -1,30 +1,98 @@ import { auth } from "@/auth"; +import { AppOnboarding } from "@/components/app-onboarding"; import { getI18n } from "@/locales/server"; +import { db } from "@bubba/db"; import { SecondaryMenu } from "@bubba/ui/secondary-menu"; +import { cache, Suspense } from "react"; +import { CreateVendorSheet } from "../components/create-vendor-sheet"; export default async function Layout({ - children, + children, }: { - children: React.ReactNode; + children: React.ReactNode; }) { - const t = await getI18n(); - const session = await auth(); - const user = session?.user; - const orgId = user?.organizationId; - - return ( -
- - -
{children}
-
- ); + const t = await getI18n(); + const session = await auth(); + const user = session?.user; + const orgId = user?.organizationId; + + const overview = await getVendorOverview(); + + if (overview?.vendors === 0) { + return ( +
+ Loading...
}> +
+ + +
+ +
+ ); + } + + return ( +
+ Loading...
}> + + +
{children}
+ + + ); } + +const getVendorOverview = cache( + async () => { + const session = await auth(); + + if (!session || !session.user.organizationId) { + return { vendors: 0 }; + } + + return await db.$transaction(async (tx) => { + const [vendors] = await Promise.all([ + tx.vendor.count({ + where: { organizationId: session.user.organizationId }, + }), + ]); + + return { + vendors, + }; + }); + } +); 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/(overview)/loading.tsx new file mode 100644 index 0000000000..d1a694631b --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/loading.tsx @@ -0,0 +1,44 @@ +import { Button } from "@bubba/ui/button"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@bubba/ui/card"; +import { Input } from "@bubba/ui/input"; +import { getI18n } from "@/locales/server"; + +export default async function Loading() { + const t = await getI18n(); + + return ( +
+ + + {t("settings.general.org_name")} + + {t("settings.general.org_name_description")} + + + + + + +
{t("settings.general.org_name_tip")}
+ +
+
+ + + + {t("settings.general.org_website")} + + {t("settings.general.org_website_description")} + + + + + + +
{t("settings.general.org_website_tip")}
+ +
+
+
+ ); +} 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 fbbd844e31..21f9f68ebb 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,90 +1,43 @@ import { auth } from "@/auth"; -import { RiskOverview } from "@/components/risks/risk-overview"; -import { VendorsByStatus } from "@/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/components/charts/vendors-by-status"; import { VendorOverview } from "@/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/components/charts/vendor-overview"; import { getI18n } from "@/locales/server"; -import { db } from "@bubba/db"; -import { Button } from "@bubba/ui/button"; -import { Card } from "@bubba/ui/card"; import type { Metadata } from "next"; import { setStaticParamsLocale } from "next-international/server"; -import Link from "next/link"; import { redirect } from "next/navigation"; export default async function VendorManagement({ - params, + params, }: { - params: Promise<{ locale: string }>; + params: Promise<{ locale: string }>; }) { - const { locale } = await params; - setStaticParamsLocale(locale); - - const session = await auth(); - - if (!session?.user?.organizationId) { - redirect("/onboarding"); - } - - const overview = await getVendorOverview(session.user.organizationId); - - if (overview?.vendors === 0) { - return ( -
- -
-

- No vendors found -

-

- Get started by adding your first vendor -

-
- -
- -
-
-
- ); - } - - return ( -
- -
- ); -} - -async function getVendorOverview(organizationId: string) { - return await db.$transaction(async (tx) => { - const [vendors] = await Promise.all([ - tx.vendor.count({ - where: { organizationId }, - }), - ]); - - return { - vendors, - }; - }); + const { locale } = await params; + setStaticParamsLocale(locale); + + const session = await auth(); + + if (!session || !session.user.organizationId) { + redirect("/onboarding"); + } + + return ( +
+ +
+ ); } export async function generateMetadata({ - params, + params, }: { - params: Promise<{ locale: string }>; + params: Promise<{ locale: string }>; }): Promise { - const { locale } = await params; - setStaticParamsLocale(locale); - const t = await getI18n(); + const { locale } = await params; + setStaticParamsLocale(locale); + const t = await getI18n(); - return { - title: t("sidebar.risk"), - }; + return { + title: t("sidebar.vendors"), + }; } diff --git a/apps/app/src/components/app-onboarding.tsx b/apps/app/src/components/app-onboarding.tsx new file mode 100644 index 0000000000..a477b73596 --- /dev/null +++ b/apps/app/src/components/app-onboarding.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@bubba/ui/card"; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@bubba/ui/accordion"; +import Image from "next/image"; +import { Button } from "@bubba/ui/button"; +import { PlusIcon } from "lucide-react"; +import { useI18n } from "@/locales/client"; +import { useQueryState } from "nuqs"; + +interface FAQ { + questionKey: string; + answerKey: string; +} + +type Props = { + title: string; + description: string; + cta: string; + imageSrc: string; + imageAlt: string; + faqs?: FAQ[]; + sheetName: string; +}; + +export function AppOnboarding({ title, description, cta, imageSrc, imageAlt, faqs, sheetName }: Props) { + const t = useI18n(); + const [open, setOpen] = useQueryState(sheetName); + const isOpen = Boolean(open); + + return ( + +
+
+
+ + {title} + {description} + + +

{t("app_onboarding.risk_management.learn_more")}

+ {faqs && faqs.length > 0 && ( +
+ + {faqs.map((faq) => ( + + {faq.questionKey} + {faq.answerKey} + + ))} + +
+ )} +
+
+
+
+
+ {imageAlt} +
+
+
+ + + +
+ + ); +} diff --git a/apps/app/src/components/main-menu.tsx b/apps/app/src/components/main-menu.tsx index e0af26f6b5..7fb5d2ddc3 100644 --- a/apps/app/src/components/main-menu.tsx +++ b/apps/app/src/components/main-menu.tsx @@ -4,244 +4,244 @@ import { useI18n } from "@/locales/client"; import { cn } from "@bubba/ui/cn"; import { Icons } from "@bubba/ui/icons"; import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, } from "@bubba/ui/tooltip"; import Link from "next/link"; import { usePathname } from "next/navigation"; // Define menu item types with icon component type MenuItem = { - id: string; - path: string; - name: string; - disabled: boolean; - icon: React.FC<{ size?: number }>; - protected: boolean; + id: string; + path: string; + name: string; + disabled: boolean; + icon: React.FC<{ size?: number }>; + protected: boolean; }; interface ItemProps { - item: MenuItem; - isActive: boolean; - disabled: boolean; - organizationId: string; - isCollapsed?: boolean; + item: MenuItem; + isActive: boolean; + disabled: boolean; + organizationId: string; + isCollapsed?: boolean; } const Item = ({ - item, - isActive, - disabled, - organizationId, - isCollapsed = false, + item, + isActive, + disabled, + organizationId, + isCollapsed = false, }: ItemProps) => { - const Icon = item.icon; - const linkDisabled = disabled || item.disabled; - - // Replace the organizationId placeholder in the path - const itemPath = item.path.replace(":organizationId", organizationId); - - return ( - - {linkDisabled ? ( -
- Coming -
- ) : ( - - - -
-
- {Icon && } - {!isCollapsed && ( - - {item.name} - - )} -
-
-
- - {item.name} - -
- - )} -
- ); + const Icon = item.icon; + const linkDisabled = disabled || item.disabled; + + // Replace the organizationId placeholder in the path + const itemPath = item.path.replace(":organizationId", organizationId); + + return ( + + {linkDisabled ? ( +
+ Coming +
+ ) : ( + + + +
+
+ {Icon && } + {!isCollapsed && ( + + {item.name} + + )} +
+
+
+ + {item.name} + +
+ + )} +
+ ); }; type Props = { - organizationId: string; - userIsAdmin: boolean; - isCollapsed?: boolean; + organizationId: string; + userIsAdmin: boolean; + isCollapsed?: boolean; }; export function MainMenu({ - organizationId, - userIsAdmin, - isCollapsed = false, + organizationId, + userIsAdmin, + isCollapsed = false, }: Props) { - const t = useI18n(); - const pathname = usePathname(); - - const items: MenuItem[] = [ - { - id: "overview", - path: "/:organizationId/overview", - name: t("sidebar.overview"), - disabled: false, - icon: Icons.Overview, - protected: false, - }, - { - id: "evidence", - path: "/:organizationId/evidence/overview", - name: t("sidebar.evidence"), - disabled: false, - icon: Icons.Evidence, - protected: false, - }, - { - id: "tests", - path: "/:organizationId/tests", - name: t("sidebar.tests"), - disabled: false, - icon: Icons.CloudSync, - protected: false, - }, - { - id: "policies", - path: "/:organizationId/policies", - name: t("sidebar.policies"), - disabled: false, - icon: Icons.Policies, - protected: false, - }, - { - id: "employees", - path: "/:organizationId/employees/dashboard", - name: t("sidebar.employees"), - disabled: false, - icon: Icons.Peolple, - protected: false, - }, - { - id: "risk", - path: "/:organizationId/risk", - name: t("sidebar.risk"), - disabled: false, - icon: Icons.Risk, - protected: false, - }, - { - id: "vendors", - path: "/:organizationId/vendors", - name: t("sidebar.vendors"), - disabled: false, - icon: Icons.Vendors, - protected: false, - }, - { - id: "integrations", - path: "/:organizationId/integrations", - name: t("sidebar.integrations"), - disabled: false, - icon: Icons.Apps, - protected: true, - }, - { - id: "settings", - path: "/:organizationId/settings", - name: t("sidebar.settings"), - disabled: false, - icon: Icons.Settings, - protected: true, - }, - ]; - - // Helper function to check if a path is active - const isPathActive = (itemPath: string) => { - const normalizedItemPath = itemPath.replace( - ":organizationId", - organizationId, - ); - - // Extract the base path from the menu item (first two segments after normalization) - const itemPathParts = normalizedItemPath.split("/").filter(Boolean); - const itemBaseSegment = itemPathParts.length > 1 ? itemPathParts[1] : ""; - - // Extract the current path parts - const currentPathParts = pathname.split("/").filter(Boolean); - const currentBaseSegment = - currentPathParts.length > 1 ? currentPathParts[1] : ""; - - // Special case for root organization path - if ( - normalizedItemPath === `/${organizationId}` || - normalizedItemPath === `/${organizationId}/overview` - ) { - return ( - pathname === `/${organizationId}` || - pathname?.startsWith(`/${organizationId}/overview`) - ); - } - - // Compare the base segments (usually the feature section like "evidence", "settings", etc.) - return itemBaseSegment === currentBaseSegment; - }; - - return ( -
- -
- ); + const t = useI18n(); + const pathname = usePathname(); + + const items: MenuItem[] = [ + { + id: "overview", + path: "/:organizationId/overview", + name: t("sidebar.overview"), + disabled: false, + icon: Icons.Overview, + protected: false, + }, + { + id: "evidence", + path: "/:organizationId/evidence/overview", + name: t("sidebar.evidence"), + disabled: false, + icon: Icons.Evidence, + protected: false, + }, + { + id: "tests", + path: "/:organizationId/tests", + name: t("sidebar.tests"), + disabled: false, + icon: Icons.CloudSync, + protected: false, + }, + { + id: "policies", + path: "/:organizationId/policies", + name: t("sidebar.policies"), + disabled: false, + icon: Icons.Policies, + protected: false, + }, + { + id: "employees", + path: "/:organizationId/employees", + name: t("sidebar.employees"), + disabled: false, + icon: Icons.Peolple, + protected: false, + }, + { + id: "risk", + path: "/:organizationId/risk", + name: t("sidebar.risk"), + disabled: false, + icon: Icons.Risk, + protected: false, + }, + { + id: "vendors", + path: "/:organizationId/vendors", + name: t("sidebar.vendors"), + disabled: false, + icon: Icons.Vendors, + protected: false, + }, + { + id: "integrations", + path: "/:organizationId/integrations", + name: t("sidebar.integrations"), + disabled: false, + icon: Icons.Apps, + protected: true, + }, + { + id: "settings", + path: "/:organizationId/settings", + name: t("sidebar.settings"), + disabled: false, + icon: Icons.Settings, + protected: true, + }, + ]; + + // Helper function to check if a path is active + const isPathActive = (itemPath: string) => { + const normalizedItemPath = itemPath.replace( + ":organizationId", + organizationId, + ); + + // Extract the base path from the menu item (first two segments after normalization) + const itemPathParts = normalizedItemPath.split("/").filter(Boolean); + const itemBaseSegment = itemPathParts.length > 1 ? itemPathParts[1] : ""; + + // Extract the current path parts + const currentPathParts = pathname.split("/").filter(Boolean); + const currentBaseSegment = + currentPathParts.length > 1 ? currentPathParts[1] : ""; + + // Special case for root organization path + if ( + normalizedItemPath === `/${organizationId}` || + normalizedItemPath === `/${organizationId}/overview` + ) { + return ( + pathname === `/${organizationId}` || + pathname?.startsWith(`/${organizationId}/overview`) + ); + } + + // Compare the base segments (usually the feature section like "evidence", "settings", etc.) + return itemBaseSegment === currentBaseSegment; + }; + + return ( +
+ +
+ ); } diff --git a/apps/app/src/components/settings/team/members-list.tsx b/apps/app/src/components/settings/team/members-list.tsx index 6d928aff17..a956a4e0e6 100644 --- a/apps/app/src/components/settings/team/members-list.tsx +++ b/apps/app/src/components/settings/team/members-list.tsx @@ -3,12 +3,12 @@ import { Avatar, AvatarFallback, AvatarImage } from "@bubba/ui/avatar"; import { Badge } from "@bubba/ui/badge"; import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, } from "@bubba/ui/card"; import { Crown, UserCheck, UserCog, UserMinus } from "lucide-react"; import { MemberActions } from "./member-actions"; @@ -16,156 +16,158 @@ import type { MembershipRole } from "@prisma/client"; import { useI18n } from "@/locales/client"; interface OrganizationMember { - id: string; - organizationId: string; - department: string; - userId: string; - role: MembershipRole; - invitedEmail: string | null; - accepted: boolean; - joinedAt: Date; - lastActive: Date | null; - user: { - id: string; - name: string | null; - email: string | null; - image: string | null; - }; + id: string; + organizationId: string; + department: string; + userId: string; + role: MembershipRole; + invitedEmail: string | null; + accepted: boolean; + joinedAt: Date; + lastActive: Date | null; + user: { + id: string; + name: string | null; + email: string | null; + image: string | null; + }; } interface MembersListProps { - members: OrganizationMember[]; - currentUserRole?: string; - hasOrganization: boolean; + members: OrganizationMember[]; + currentUserRole?: string; + hasOrganization: boolean; } export function MembersList({ - members, - currentUserRole, - hasOrganization, + members, + currentUserRole, + hasOrganization, }: MembersListProps) { - const t = useI18n(); + const t = useI18n(); - if (!hasOrganization) { - return ( - - - - {t("settings.team.members.empty.no_organization.title")} - - - {t("settings.team.members.empty.no_organization.description")} - - - - ); - } + if (!hasOrganization) { + return ( + + + + {t("settings.team.members.empty.no_organization.title")} + + + {t("settings.team.members.empty.no_organization.description")} + + + + ); + } - if (members.length === 0) { - return ( - - - - {t("settings.team.members.empty.no_members.title")} - - - {t("settings.team.members.empty.no_members.description")} - - - - ); - } + if (members.length === 0) { + return ( + + + + {t("settings.team.members.empty.no_members.title")} + + + {t("settings.team.members.empty.no_members.description")} + + + + ); + } - const isOwnerOrAdmin = - currentUserRole === "owner" || currentUserRole === "admin"; + const isOwnerOrAdmin = + currentUserRole === "owner" || currentUserRole === "admin"; - return ( - - - {t("settings.team.members.title")} - - - -
- {members.map((member) => { - const isCurrentUser = - member.userId === - members.find((m) => m.role === currentUserRole)?.userId; + return ( + + + {t("settings.team.members.title")} + + {t("settings.team.members.description")} + + + +
+ {members.map((member) => { + const isCurrentUser = + member.userId === + members.find((m) => m.role === currentUserRole)?.userId; - return ( -
-
- - - - {member.user.name - ? member.user.name - .split(" ") - .map((n) => n[0]) - .join("") - .toUpperCase() - : member.user.email?.slice(0, 2).toUpperCase()} - - -
-
- - {member.user.name || member.user.email}{" "} - - ({member.user.email}) - - -
- - {getMemberRoleIcon(member.role)} - {t(`settings.team.members.role.${member.role}`)} - - {member.role !== "owner" && ( - - {member.accepted - ? t("settings.team.members.status.accepted") - : t("settings.team.members.status.pending")} - - )} -
-
-
-
-
- {isOwnerOrAdmin && !isCurrentUser && ( - - )} -
-
- ); - })} -
-
- {t("settings.team.members.description")} -
- ); + return ( +
+
+ + + + {member.user.name + ? member.user.name + .split(" ") + .map((n) => n[0]) + .join("") + .toUpperCase() + : member.user.email?.slice(0, 2).toUpperCase()} + + +
+
+ + {member.user.name || member.user.email}{" "} + + ({member.user.email}) + + +
+ + {getMemberRoleIcon(member.role)} + {t(`settings.team.members.role.${member.role}`)} + + {member.role !== "owner" && ( + + {member.accepted + ? t("settings.team.members.status.accepted") + : t("settings.team.members.status.pending")} + + )} +
+
+
+
+
+ {isOwnerOrAdmin && !isCurrentUser && ( + + )} +
+
+ ); + })} +
+
+ {t("settings.team.members.description")} +
+ ); } function getMemberRoleIcon(role: string) { - switch (role) { - case "owner": - return ; - case "admin": - return ; - case "member": - return ; - case "viewer": - return ; - default: - return ; - } + switch (role) { + case "owner": + return ; + case "admin": + return ; + case "member": + return ; + case "viewer": + return ; + default: + return ; + } } diff --git a/apps/app/src/data/tools/index.ts b/apps/app/src/data/tools/index.ts index 72bca7defe..b35ccb8f0b 100644 --- a/apps/app/src/data/tools/index.ts +++ b/apps/app/src/data/tools/index.ts @@ -1,9 +1,11 @@ import { getOrganizationTools } from "./organization"; import { getPolicyTools } from "./policies"; import { getRiskTools } from "./risks"; +import { getUserTools } from "./user"; export const tools = { ...getOrganizationTools(), ...getPolicyTools(), ...getRiskTools(), + ...getUserTools(), }; diff --git a/apps/app/src/data/tools/risks.ts b/apps/app/src/data/tools/risks.ts index 57d9b7a8d9..c9cfe86169 100644 --- a/apps/app/src/data/tools/risks.ts +++ b/apps/app/src/data/tools/risks.ts @@ -23,8 +23,9 @@ export const getRisks = tool({ category: z .enum(Object.values(RiskCategory) as [RiskCategory, ...RiskCategory[]]) .optional(), + owner: z.string().optional(), }), - execute: async ({ status, department, category }) => { + execute: async ({ status, department, category, owner }) => { const session = await auth(); if (!session?.user.organizationId) { @@ -37,6 +38,7 @@ export const getRisks = tool({ status, department, category, + ownerId: owner, }, select: { id: true, diff --git a/apps/app/src/data/tools/user.ts b/apps/app/src/data/tools/user.ts new file mode 100644 index 0000000000..bfe1076f50 --- /dev/null +++ b/apps/app/src/data/tools/user.ts @@ -0,0 +1,26 @@ +import { auth } from "@/auth"; +import { tool } from "ai"; +import { z } from "zod"; + +export function getUserTools() { + return { + getUser, + }; +} + +export const getUser = tool({ + description: "Get the user's id and organization id", + parameters: z.object({}), + execute: async () => { + const session = await auth(); + + if (!session?.user.organizationId) { + return { error: "Unauthorized" }; + } + + return { + userId: session.user.id, + organizationId: session.user.organizationId, + }; + }, +}); diff --git a/apps/app/src/locales/en.ts b/apps/app/src/locales/en.ts index e8704d6b62..c2cfc76011 100644 --- a/apps/app/src/locales/en.ts +++ b/apps/app/src/locales/en.ts @@ -24,40 +24,46 @@ import { risk } from "./features/risk"; import { tests } from "./features/tests"; import { vendors } from "./features/vendors"; +// Onboarding translations +import { app_onboarding } from "./onboarding/app-onboarding"; + // Settings translations import { settings } from "./settings/settings"; export const translations = { - // Core - common, - errors, - language, - languages, + // Core + common, + errors, + language, + languages, + + // Auth & Onboarding + auth, + onboarding, - // Auth & Onboarding - auth, - onboarding, + // Layout & Navigation + header, + not_found, + sidebar, + theme, + user_menu, - // Layout & Navigation - header, - not_found, - sidebar, - theme, - user_menu, + // Features + evidence, + evidence_tasks, + frameworks, + overview, + people, + policies, + risk, + tests, + vendors, - // Features - evidence, - evidence_tasks, - frameworks, - overview, - people, - policies, - risk, - tests, - vendors, + // Settings + settings, - // Settings - settings, + // Onboarding + app_onboarding, } as const; export type Translations = typeof translations; diff --git a/apps/app/src/locales/features/risk.ts b/apps/app/src/locales/features/risk.ts index 505e3eb9ee..468c5511cd 100644 --- a/apps/app/src/locales/features/risk.ts +++ b/apps/app/src/locales/features/risk.ts @@ -1,181 +1,185 @@ export const risk = { - risks: "risks", - overview: "Overview", - create: "Create New Risk", - comments: "Risk Comments", - risk_overview: "Risk Overview", - form: { - update_inherent_risk_success: "Inherent risk updated successfully", - update_inherent_risk_error: "Failed to update inherent risk", - update_inherent_risk: "Update Inherent Risk", - update_inherent_risk_description: "Adjust the inherent risk before any controls are applied", - update_residual_risk_success: "Residual risk updated successfully", - update_residual_risk_error: "Failed to update residual risk", - update_residual_risk: "Update Residual Risk", - update_residual_risk_description: "Adjust the residual risk after controls have been applied", - update_risk_success: "Risk updated successfully", - update_risk_error: "Failed to update risk", - update_risk: "Update Risk", - update_risk_description: "Update risk details and metadata", - create_risk_success: "Risk created successfully", - create_risk_error: "Failed to create risk", - risk_title: "Risk Title", - risk_title_description: "Enter risk title", - risk_details: "Risk Details", - risk_category: "Category", - risk_category_placeholder: "Select risk category", - risk_description: "Description", - risk_description_description: "Enter risk description", - risk_department: "Department", - risk_department_placeholder: "Select department", - risk_status: "Status", - risk_status_placeholder: "Select status" - }, - register: { - title: "Risk Register", - table: { - risk: "Risk" - }, - empty: { - create_risk: "Create Risk", - no_risks: { - title: "No risks yet", - description: "Get started by creating your first risk" - } - } - }, - dashboard: { - title: "Risk Dashboard", - by_department: "Risks by Department", - status: "Risks by Status", - risks_by_assignee: "Risks by Assignee", - overview: "Overview", - inherent_risk_description: "Initial risk level before any controls are applied", - residual_risk_description: "Remaining risk level after controls are applied" - }, - metrics: { - probability: "Probability", - impact: "Impact", - inherentRisk: "Inherent Risk", - residualRisk: "Residual Risk" - }, - tasks: { - title: "Tasks", - task_overview: "Task Overview", - attachments: "Attachments", - overview: "Overview", - sheet: { - title: "Create Task", - update: "Update Task", - update_description: "Update task details and metadata" - }, - form: { - title: "Task Details", - task_title: "Task Title", - task_title_description: "Enter task title", - description: "Description", - description_description: "Enter task description", - due_date: "Due Date", - success: "Task created successfully", - error: "Failed to create task", - update_success: "Task updated successfully", - update_error: "Failed to update task", - status: "Status", - status_placeholder: "Select status" - } - }, - vendor: { - title: "Vendor Management", - dashboard: { - title: "Vendor Dashboard", - overview: "Vendor Overview", - vendor_status: "Vendor Status", - vendor_category: "Vendor Categories", - inherent_risk_description: - "Initial risk level before any controls are applied", - residual_risk_description: - "Remaining risk level after controls are applied", - }, - register: { - title: "Vendor Register", - table: { - name: "Name", - category: "Category", - status: "Status", - owner: "Owner", - }, - }, - assessment: { - title: "Vendor Assessment", - inherent_risk: "Inherent Risk", - residual_risk: "Residual Risk", - }, - form: { - vendor_details: "Vendor Details", - vendor_name: "Name", - vendor_name_placeholder: "Enter vendor name", - vendor_website: "Website", - vendor_website_placeholder: "Enter vendor website", - vendor_description: "Description", - vendor_description_placeholder: "Enter vendor description", - vendor_category: "Category", - vendor_category_placeholder: "Select category", - vendor_status: "Status", - vendor_status_placeholder: "Select status", - create_vendor_success: "Vendor created successfully", - create_vendor_error: "Failed to create vendor", - }, - table: { - name: "Name", - category: "Category", - status: "Status", - owner: "Owner", - }, - filters: { - search_placeholder: "Search vendors...", - status_placeholder: "Filter by status", - }, - empty_states: { - no_vendors: { - title: "No vendors yet", - description: "Get started by creating your first vendor", - }, - no_results: { - title: "No results found", - description: "No vendors match your search", - description_with_filters: "Try adjusting your filters", - }, - }, - actions: { - create: "Create Vendor", - }, - status: { - not_assessed: "Not Assessed", - in_progress: "In Progress", - }, - risk_level: { - low: "Low Risk", - medium: "Medium Risk", - high: "High Risk", - }, - tasks: { - empty: { - no_results: "No results found", - no_results_description: "Try adjusting your search or filters", - clear_filters: "Clear filters", - no_tasks: "No tasks found", - }, - filters: { - search: "Search tasks...", - status: "Filter by status", - all_statuses: "All statuses", - not_started: "Not started", - in_progress: "In progress", - completed: "Completed", - assignee: "Filter by assignee", - all_assignees: "All assignees", - clear: "Clear filters" - } - }, - }, -} as const \ No newline at end of file + risks: "risks", + overview: "Overview", + create: "Create New Risk", + comments: "Risk Comments", + risk_overview: "Risk Overview", + form: { + update_inherent_risk_success: "Inherent risk updated successfully", + update_inherent_risk_error: "Failed to update inherent risk", + update_inherent_risk: "Update Inherent Risk", + update_inherent_risk_description: + "Adjust the inherent risk before any controls are applied", + update_residual_risk_success: "Residual risk updated successfully", + update_residual_risk_error: "Failed to update residual risk", + update_residual_risk: "Update Residual Risk", + update_residual_risk_description: + "Adjust the residual risk after controls have been applied", + update_risk_success: "Risk updated successfully", + update_risk_error: "Failed to update risk", + update_risk: "Update Risk", + update_risk_description: "Update risk details and metadata", + create_risk_success: "Risk created successfully", + create_risk_error: "Failed to create risk", + risk_title: "Risk Title", + risk_title_description: "Enter risk title", + risk_details: "Risk Details", + risk_category: "Category", + risk_category_placeholder: "Select risk category", + risk_description: "Description", + risk_description_description: "Enter risk description", + risk_department: "Department", + risk_department_placeholder: "Select department", + risk_status: "Status", + risk_status_placeholder: "Select status", + }, + register: { + title: "Risk Register", + table: { + risk: "Risk", + }, + empty: { + create_risk: "Create Risk", + no_risks: { + title: "No risks yet", + description: "Get started by creating your first risk", + }, + }, + }, + dashboard: { + title: "Overview", + by_department: "Risks by Department", + status: "Risks by Status", + risks_by_assignee: "Risks by Assignee", + overview: "Overview", + inherent_risk_description: + "Initial risk level before any controls are applied", + residual_risk_description: + "Remaining risk level after controls are applied", + }, + metrics: { + probability: "Probability", + impact: "Impact", + inherentRisk: "Inherent Risk", + residualRisk: "Residual Risk", + }, + tasks: { + title: "Tasks", + task_overview: "Task Overview", + attachments: "Attachments", + overview: "Overview", + sheet: { + title: "Create Task", + update: "Update Task", + update_description: "Update task details and metadata", + }, + form: { + title: "Task Details", + task_title: "Task Title", + task_title_description: "Enter task title", + description: "Description", + description_description: "Enter task description", + due_date: "Due Date", + success: "Task created successfully", + error: "Failed to create task", + update_success: "Task updated successfully", + update_error: "Failed to update task", + status: "Status", + status_placeholder: "Select status", + }, + }, + vendor: { + title: "Vendor Management", + dashboard: { + title: "Vendor Dashboard", + overview: "Vendor Overview", + vendor_status: "Vendor Status", + vendor_category: "Vendor Categories", + inherent_risk_description: + "Initial risk level before any controls are applied", + residual_risk_description: + "Remaining risk level after controls are applied", + }, + register: { + title: "Vendor Register", + table: { + name: "Name", + category: "Category", + status: "Status", + owner: "Owner", + }, + }, + assessment: { + title: "Vendor Assessment", + inherent_risk: "Inherent Risk", + residual_risk: "Residual Risk", + }, + form: { + vendor_details: "Vendor Details", + vendor_name: "Name", + vendor_name_placeholder: "Enter vendor name", + vendor_website: "Website", + vendor_website_placeholder: "Enter vendor website", + vendor_description: "Description", + vendor_description_placeholder: "Enter vendor description", + vendor_category: "Category", + vendor_category_placeholder: "Select category", + vendor_status: "Status", + vendor_status_placeholder: "Select status", + create_vendor_success: "Vendor created successfully", + create_vendor_error: "Failed to create vendor", + }, + table: { + name: "Name", + category: "Category", + status: "Status", + owner: "Owner", + }, + filters: { + search_placeholder: "Search vendors...", + status_placeholder: "Filter by status", + }, + empty_states: { + no_vendors: { + title: "No vendors yet", + description: "Get started by creating your first vendor", + }, + no_results: { + title: "No results found", + description: "No vendors match your search", + description_with_filters: "Try adjusting your filters", + }, + }, + actions: { + create: "Create Vendor", + }, + status: { + not_assessed: "Not Assessed", + in_progress: "In Progress", + }, + risk_level: { + low: "Low Risk", + medium: "Medium Risk", + high: "High Risk", + }, + tasks: { + empty: { + no_results: "No results found", + no_results_description: "Try adjusting your search or filters", + clear_filters: "Clear filters", + no_tasks: "No tasks found", + }, + filters: { + search: "Search tasks...", + status: "Filter by status", + all_statuses: "All statuses", + not_started: "Not started", + in_progress: "In progress", + completed: "Completed", + assignee: "Filter by assignee", + all_assignees: "All assignees", + clear: "Clear filters", + }, + }, + }, +} as const; diff --git a/apps/app/src/locales/onboarding/app-onboarding.ts b/apps/app/src/locales/onboarding/app-onboarding.ts new file mode 100644 index 0000000000..454f6ca1fb --- /dev/null +++ b/apps/app/src/locales/onboarding/app-onboarding.ts @@ -0,0 +1,65 @@ +export const app_onboarding = { + risk_management: { + title: "Risk Management", + description: "Manage your risks and ensure your organization is protected.", + detailed_description: + "Create and manage your risks in one place. Add details, assign owners, and track progress.", + cta: "Create your first risk", + learn_more: "Learn more", + faqs: { + question_1: "What is risk management?", + answer_1: + "Risk management is the process of identifying, assessing, and mitigating risks.", + question_2: "Why should I manage risks?", + answer_2: + "Risks can impact your organization's operations, reputation, and financial stability. By managing risks, you can reduce the likelihood of negative events and protect your organization.", + question_3: "Do I need to manage risks for SOC 2?", + answer_3: + "Yes, you need to manage risks for SOC 2 and other frameworks. Risk management is a key component of SOC 2 compliance.", + question_4: "What information should I include in a risk?", + answer_4: + "Include details like risk description, impact, likelihood, and assigned owner to effectively track and manage the risk.", + }, + }, + vendors: { + title: "Vendor Management", + description: + "Manage your vendors and ensure your organization is protected.", + cta: "Add your first vendor", + learn_more: "Learn more", + faqs: { + question_1: "What is a vendor?", + answer_1: + "A vendor is a third-party organization that provides services to your organization.", + question_2: "Why should I manage vendors?", + answer_2: + "Vendors can impact your organization's operations, reputation, and financial stability. By managing vendors, you can reduce the likelihood of negative events and protect your organization.", + question_3: "Do I need to manage vendors for SOC 2?", + answer_3: + "Yes, you need to manage vendors for SOC 2 and other frameworks. Vendor management is a key component of SOC 2 compliance.", + question_4: "What information should I include in a vendor?", + answer_4: + "Include details like vendor name, contact information, and services provided to effectively track and manage the vendor.", + }, + }, + employees: { + title: "Employee Management", + description: + "Manage your employees and ensure your organization is protected.", + cta: "Add your first employee", + learn_more: "Learn more", + faqs: { + question_1: "What is an employee?", + answer_1: "An employee is a person who works for your organization.", + question_2: "Why should I manage employees?", + answer_2: + "Employees can impact your organization's operations, reputation, and financial stability. By managing employees, you can reduce the likelihood of negative events and protect your organization.", + question_3: "Do I need to manage employees for SOC 2?", + answer_3: + "Yes, you need to manage employees for SOC 2 and other frameworks. Employee management is a key component of SOC 2 compliance.", + question_4: "What information should I include in an employee?", + answer_4: + "Include details like employee name, email, and role to effectively track and manage the employee.", + }, + }, +} as const; diff --git a/apps/app/src/middleware.ts b/apps/app/src/middleware.ts index c45adb7ac3..6aac35833d 100644 --- a/apps/app/src/middleware.ts +++ b/apps/app/src/middleware.ts @@ -2,31 +2,31 @@ import { createI18nMiddleware } from "next-international/middleware"; import type { NextRequest } from "next/server"; export const config = { - matcher: [ - // Skip auth-related routes - "/((?!api|_next/static|_next/image|favicon.ico|monitoring|ingest).*)", - ], + matcher: [ + // Skip auth-related routes + "/((?!api|_next/static|_next/image|favicon.ico|monitoring|ingest|onboarding).*)", + ], }; const I18nMiddleware = createI18nMiddleware({ - locales: ["en", "es", "fr", "no", "pt"], - defaultLocale: "en", - urlMappingStrategy: "rewrite", + locales: ["en", "es", "fr", "no", "pt"], + defaultLocale: "en", + urlMappingStrategy: "rewrite", }); export async function middleware(request: NextRequest) { - const response = I18nMiddleware(request); - const nextUrl = request.nextUrl; + const response = I18nMiddleware(request); + const nextUrl = request.nextUrl; - const pathnameLocale = nextUrl.pathname.split("/", 2)?.[1]; + const pathnameLocale = nextUrl.pathname.split("/", 2)?.[1]; - const pathnameWithoutLocale = pathnameLocale - ? nextUrl.pathname.slice(pathnameLocale.length + 1) - : nextUrl.pathname; + const pathnameWithoutLocale = pathnameLocale + ? nextUrl.pathname.slice(pathnameLocale.length + 1) + : nextUrl.pathname; - const newUrl = new URL(pathnameWithoutLocale || "/", request.url); + const newUrl = new URL(pathnameWithoutLocale || "/", request.url); - response.headers.set("x-pathname", request.nextUrl.pathname); + response.headers.set("x-pathname", request.nextUrl.pathname); - return response; + return response; }