diff --git a/apps/app/.gitignore b/apps/app/.gitignore
index 6968ac53c0..540952433e 100644
--- a/apps/app/.gitignore
+++ b/apps/app/.gitignore
@@ -40,4 +40,8 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts
-.trigger
\ No newline at end of file
+.trigger
+
+# temp files
+*/locales/translations.json
+*/locales/converter.ts
\ No newline at end of file
diff --git a/apps/app/languine.lock b/apps/app/languine.lock
index f1c929755b..b41ff2e67c 100644
--- a/apps/app/languine.lock
+++ b/apps/app/languine.lock
@@ -677,6 +677,9 @@ files:
frameworks.controls.statuses.in_progress: c9413dd7682a45da9ecc9efa8cea47d7
frameworks.controls.statuses.not_started: 10db56f8b10dc989c43fbe33202e5339
errors.unexpected: 325a357f60b95f37fec18b15cb39fbb0
+ errors.unauthorized.title: 2a5277746841d80f9489dd85fabc769d
+ errors.unauthorized.description: 118ce881f1bd6df5785ce15698255334
+ errors.unauthorized.back: 56cf8b33ab527ab81b4f6b3ceac090dd
editor.ai.thinking: aba464039b337eb2e75c2520039de58f
editor.ai.thinking_spinner: aba464039b337eb2e75c2520039de58f
editor.ai.edit_or_generate: 7d46fd8c65c35acb13973849b035cc53
@@ -718,6 +721,49 @@ files:
vendors.dashboard.title: 3b878279a04dc47d60932cb294d96259
vendors.register.title: 122b14e8fa33bc9d4f389700439dfc71
vendors.register.create_new: ef1c7d5e555d7fe288bf0bf16b88723d
+ vendors.create: ef1c7d5e555d7fe288bf0bf16b88723d
+ vendors.form.vendor_details: c594485dedf99a5bbae0ce0b1e08c57a
+ vendors.form.vendor_name: 49ee3087348e8d44e1feda1917443987
+ vendors.form.vendor_name_placeholder: 76d22a64aa21a6f1fe356624bb42487e
+ vendors.form.vendor_website: 15bbb9d0bbf25e8d2978de1168c749dc
+ vendors.form.vendor_website_placeholder: 0edbf3ae0d36cb35fdae8df56396e8f0
+ vendors.form.vendor_description: b5a7adde1af5c87d7fd797b6245c2a39
+ vendors.form.vendor_description_placeholder: edd59fcb97919e8a914f29d1b8c94a84
+ vendors.form.vendor_category: 3adbdb3ac060038aa0e6e6c138ef9873
+ vendors.form.vendor_category_placeholder: 03368e3c1eb4d2a9048775874301b19f
+ vendors.form.vendor_status: ec53a8c4f07baed5d8825072c89799be
+ vendors.form.vendor_status_placeholder: 9aadb017cc5b335be77ba090d5029259
+ vendors.form.create_vendor_success: 86783216c2055e4b01d9a6d920e418c7
+ vendors.form.create_vendor_error: ca4cedd93fdb023d6f9748dd3b66b456
+ vendors.form.update_vendor: 13002cf3244d871685ebac44761efa0f
+ vendors.form.update_vendor_success: eaedfa5b3ca431e1ca8900a8f0d5c924
+ vendors.form.update_vendor_error: 92b965a71c44a134e2eae6d71493e4d9
+ vendors.form.add_comment: a508cd6ee07b35ea8efdf98d6520ce83
+ vendors.table.name: 49ee3087348e8d44e1feda1917443987
+ vendors.table.category: 3adbdb3ac060038aa0e6e6c138ef9873
+ vendors.table.status: ec53a8c4f07baed5d8825072c89799be
+ vendors.table.owner: b6f4a2ec6356bbd56d49f2096bf9d3d3
+ vendors.filters.search_placeholder: f40df41fdaa46e2d0503adf12a9304fe
+ vendors.filters.status_placeholder: 5dd5cd6670a0fc8e47a47aa94d27b8b2
+ vendors.filters.category_placeholder: 42d00bc24fd43cfe3e2e30d01f39ee95
+ vendors.filters.owner_placeholder: 2cf13cb9c60a98dee6767692b12e9c2e
+ vendors.empty_states.no_vendors.title: 69c843f9cf71049dacacff0c48edb7b2
+ vendors.empty_states.no_vendors.description: 955707a384c1cfc46fafbdebc508f9e0
+ vendors.empty_states.no_results.title: e576c23d915755d83e2d1f47bd9f6c22
+ vendors.empty_states.no_results.description: d7fd5c52726c5016a59fb01e74069441
+ vendors.empty_states.no_results.description_with_filters: 2b70f835ed5a08e817ec9388ce25b41a
+ vendors.actions.create: ef1c7d5e555d7fe288bf0bf16b88723d
+ vendors.status.not_assessed: 1a261b378451a287de3a8fbbb6f2a376
+ vendors.status.in_progress: c9413dd7682a45da9ecc9efa8cea47d7
+ vendors.status.assessed: 2c051fa737a0d6c7274f4508448b7f8a
+ vendors.category.cloud: c8e2277ecfb1427838c488c217f99466
+ vendors.category.infrastructure: cba4f4cee5c6a3e6d8be8da68fa1234e
+ vendors.category.software_as_a_service: 68c0c53c6a8ab47f4b611dbf1c347a26
+ vendors.category.finance: c482980d384a9d0e7bc39e1140270870
+ vendors.category.marketing: 7cb15e416d62919b1b40298324fbe30b
+ vendors.category.sales: 11ff9f68afb6b8b5b8eda218d7c83a65
+ vendors.category.hr: fd4c638da5f85d025963f99fe90b1b1a
+ vendors.category.other: 6311ae17c1ee52b36e68aaf4ad066387
dashboard.risk_status: 6ce5e6fa832289748023cb773a4e89ac
dashboard.risks_by_department: 0ef508d7e840bc5f85547c3b25e5d87b
dashboard.vendor_status: 17029fbe5ad4d2af5feaf37bb4604225
diff --git a/apps/app/next.config.ts b/apps/app/next.config.ts
index f2c0136855..1b492893e2 100644
--- a/apps/app/next.config.ts
+++ b/apps/app/next.config.ts
@@ -1,39 +1,42 @@
import "./src/env.mjs";
const config = {
- poweredByHeader: false,
- reactStrictMode: true,
- images: {
- remotePatterns: [
- {
- protocol: "https",
- hostname: "**",
- },
- ],
- },
- transpilePackages: ["@bubba/ui"],
- logging: {
- fetches: {
- fullUrl: process.env.LOG_FETCHES === "true",
- },
- },
- async rewrites() {
- return [
- {
- source: "/ingest/static/:path*",
- destination: "https://us-assets.i.posthog.com/static/:path*",
- },
- {
- source: "/ingest/:path*",
- destination: "https://us.i.posthog.com/:path*",
- },
- {
- source: "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/ingest/decide",
- destination: "https://us.i.posthog.com/decide",
- },
- ];
- },
- skipTrailingSlashRedirect: true,
+ poweredByHeader: false,
+ reactStrictMode: true,
+ images: {
+ remotePatterns: [
+ {
+ protocol: "https",
+ hostname: "**",
+ },
+ ],
+ },
+ transpilePackages: ["@bubba/ui"],
+ logging: {
+ fetches: {
+ fullUrl: process.env.LOG_FETCHES === "true",
+ },
+ },
+ experimental: {
+ authInterrupts: true,
+ },
+ async rewrites() {
+ return [
+ {
+ source: "/ingest/static/:path*",
+ destination: "https://us-assets.i.posthog.com/static/:path*",
+ },
+ {
+ source: "/ingest/:path*",
+ destination: "https://us.i.posthog.com/:path*",
+ },
+ {
+ source: "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/ingest/decide",
+ destination: "https://us.i.posthog.com/decide",
+ },
+ ];
+ },
+ skipTrailingSlashRedirect: true,
};
export default config;
diff --git a/apps/app/package.json b/apps/app/package.json
index 05396d0231..4a07021f45 100644
--- a/apps/app/package.json
+++ b/apps/app/package.json
@@ -11,6 +11,7 @@
"apply-migrations": "cd ../../packages/db && bunx prisma migrate dev && cd ../../apps/app",
"clean-react": "rm -rf node_modules/react; rm -rf node_modules/react-dom",
"deploy:trigger-prod": "npx trigger.dev@latest deploy",
+ "analyze-locale-usage": "bunx tsx src/locales/analyze-locale-usage.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/people/[employeeId]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/people/[employeeId]/page.tsx
index 1b043cc3d1..283bc78a61 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/people/[employeeId]/page.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/people/[employeeId]/page.tsx
@@ -34,6 +34,6 @@ export async function generateMetadata({
const t = await getI18n();
return {
- title: t("sub_pages.people.employee_details"),
+ title: t("people.employee_details"),
};
}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/editor/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/editor/page.tsx
index dd96ab394d..22f82df83b 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/editor/page.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/editor/page.tsx
@@ -25,6 +25,6 @@ export async function generateMetadata({
const t = await getI18n();
return {
- title: t("sub_pages.policies.policy_details"),
+ title: t("policies.policy_details"),
};
}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/page.tsx
index e6907db1a3..0f0b740df0 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/page.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/[policyId]/page.tsx
@@ -40,7 +40,7 @@ export async function generateMetadata({
const t = await getI18n();
return {
- title: t("sub_pages.policies.policy_details"),
+ title: t("policies.policy_details"),
};
}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/comments/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/comments/page.tsx
index 70121fc8e4..844b73b236 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/comments/page.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/comments/page.tsx
@@ -74,6 +74,6 @@ export async function generateMetadata({
const t = await getI18n();
return {
- title: t("sub_pages.risk.risk_comments"),
+ title: t("risk.risk_comments"),
};
}
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 e28293bbd5..99ae8626ad 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
@@ -236,6 +236,6 @@ export async function generateMetadata({
const t = await getI18n();
return {
- title: t("sub_pages.risk.risk_overview"),
+ title: t("risk.risk_overview"),
};
}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/tasks/[taskId]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/tasks/[taskId]/page.tsx
index d4684da3b5..9cf1525acb 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/tasks/[taskId]/page.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/tasks/[taskId]/page.tsx
@@ -75,6 +75,6 @@ export async function generateMetadata({
const t = await getI18n();
return {
- title: t("sub_pages.risk.tasks.task_overview"),
+ title: t("risk.tasks.task_overview"),
};
}
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
index 249725e8e6..56fee7d27f 100644
--- 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
@@ -17,6 +17,6 @@ export async function generateMetadata({
const t = await getI18n();
return {
- title: t("sub_pages.risk.register"),
+ title: t("risk.register"),
};
}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/settings/members/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/settings/members/page.tsx
index 969b924054..ce5cfe33b6 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/settings/members/page.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/settings/members/page.tsx
@@ -28,6 +28,6 @@ export async function generateMetadata({
const t = await getI18n();
return {
- title: t("sub_pages.settings.members"),
+ title: t("settings.members"),
};
}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/[testId]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/[testId]/page.tsx
index b463c34b94..8e26a9bc40 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/[testId]/page.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/[testId]/page.tsx
@@ -36,9 +36,9 @@ export async function generateMetadata({
setStaticParamsLocale(locale);
const t = await getI18n();
- return {
- title: t("sub_pages.tests.test_details"),
- };
+ return {
+ title: t("tests.test_details"),
+ };
}
const getUsers = async (organizationId: string) => {
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 a90ee29792..2c323f3e8b 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,11 +1,14 @@
import { auth } from "@/auth";
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 RiskManagement({
+export default async function VendorManagement({
params,
}: {
params: Promise<{ locale: string }>;
@@ -19,10 +22,31 @@ export default async function RiskManagement({
redirect("/onboarding");
}
- const overview = await getRiskOverview(session.user.organizationId);
+ const overview = await getVendorOverview(session.user.organizationId);
- if (overview?.risks === 0) {
- redirect(`/${session.user.organizationId}/risk/register`);
+ if (overview?.vendors === 0) {
+ return (
+
+
+
+
+ No vendors found
+
+
+ Get started by adding your first vendor
+
+
+
+
+
+
+
+
+ );
}
return (
@@ -39,19 +63,19 @@ export default async function RiskManagement({
);
}
-const getRiskOverview = async (organizationId: string) => {
+async function getVendorOverview(organizationId: string) {
return await db.$transaction(async (tx) => {
- const [risks] = await Promise.all([
- tx.risk.count({
+ const [vendors] = await Promise.all([
+ tx.vendor.count({
where: { organizationId },
}),
]);
return {
- risks,
+ vendors,
};
});
-};
+}
export async function generateMetadata({
params,
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[riskId]/comments/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[riskId]/comments/page.tsx
deleted file mode 100644
index 02e7b3584f..0000000000
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[riskId]/comments/page.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { auth } from "@/auth";
-import { RiskComments } from "@/components/risks/risk-comments";
-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";
-
-interface PageProps {
- params: Promise<{ riskId: string }>;
-}
-
-export default async function RiskPage({ params }: PageProps) {
- const session = await auth();
- const { riskId } = await params;
-
- if (!session) {
- redirect("/auth");
- }
-
- if (!session.user.organizationId || !riskId) {
- redirect("/");
- }
-
- const risk = await getRisk(riskId, session.user.organizationId);
-
- if (!risk) {
- redirect("/risk");
- }
-
- const users = await getUsers(session.user.organizationId);
-
- return (
-
-
-
- );
-}
-
-const getRisk = async (riskId: string, organizationId: string) => {
- const risk = await db.risk.findUnique({
- where: {
- id: riskId,
- organizationId: organizationId,
- },
- include: {
- owner: true,
- comments: {
- orderBy: {
- createdAt: "desc",
- },
- },
- },
- });
-
- return risk;
-};
-
-const getUsers = async (organizationId: string) => {
- const users = await db.user.findMany({
- where: { organizationId: organizationId },
- });
-
- return users;
-};
-
-export async function generateMetadata({
- params,
-}: {
- params: Promise<{ locale: string }>;
-}): Promise {
- const { locale } = await params;
- setStaticParamsLocale(locale);
- const t = await getI18n();
-
- return {
- title: t("sub_pages.risk.risk_comments"),
- };
-}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[riskId]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[riskId]/page.tsx
deleted file mode 100644
index 02f99d4561..0000000000
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[riskId]/page.tsx
+++ /dev/null
@@ -1,241 +0,0 @@
-import { auth } from "@/auth";
-import { Loading } from "@/components/frameworks/loading";
-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";
-import type { RiskTaskType } from "@/components/tables/risk-tasks/columns";
-import { DataTable } from "@/components/tables/risk-tasks/data-table";
-import {
- NoResults,
- NoTasks,
-} from "@/components/tables/risk-tasks/empty-states";
-import { FilterToolbar } from "@/components/tables/risk-tasks/filter-toolbar";
-import { getServerColumnHeaders } from "@/components/tables/risk-tasks/server-columns";
-import { getI18n } from "@/locales/server";
-import { db } from "@bubba/db";
-import type { RiskTaskStatus } from "@bubba/db/types";
-import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card";
-import type { Metadata } from "next";
-import { setStaticParamsLocale } from "next-international/server";
-import { redirect } from "next/navigation";
-
-interface PageProps {
- searchParams: Promise<{
- search?: string;
- status?: string;
- sort?: string;
- page?: string;
- per_page?: string;
- }>;
- params: Promise<{ riskId: string; locale: string }>;
-}
-
-export default async function RiskPage({ searchParams, params }: PageProps) {
- const session = await auth();
- const t = await getI18n();
-
- const {
- search,
- status,
- sort,
- page = "1",
- per_page = "5",
- } = await searchParams;
-
- const { riskId } = await params;
- const columnHeaders = await getServerColumnHeaders();
- const [column, order] = sort?.split(":") ?? [];
- const hasFilters = !!(search || status);
- const { tasks: loadedTasks, total } = await getTasks({
- riskId,
- search,
- status: status as RiskTaskStatus,
- column,
- order,
- page: Number.parseInt(page),
- per_page: Number.parseInt(per_page),
- });
-
- if (!session) {
- redirect("/auth");
- }
-
- if (!session.user.organizationId || !riskId) {
- redirect("/");
- }
-
- const risk = await getRisk(riskId, session.user.organizationId);
-
- if (!risk) {
- redirect("/risk");
- }
-
- const users = await getUsers(session.user.organizationId);
-
- return (
-
-
-
-
-
-
-
- {t("risk.tasks.title")}
-
-
-
-
-
-
- {loadedTasks.length > 0 ? (
-
- ) : hasFilters ? (
-
- ) : (
- <>
-
-
- >
- )}
-
-
-
-
-
-
-
-
-
- );
-}
-
-const getRisk = async (riskId: string, organizationId: string) => {
- const risk = await db.risk.findUnique({
- where: {
- id: riskId,
- organizationId: organizationId,
- },
- include: {
- owner: true,
- },
- });
-
- return risk;
-};
-
-const getTasks = async function tasks({
- riskId,
- search,
- status,
- column,
- order,
- page = 1,
- per_page = 10,
-}: {
- riskId: string;
- search?: string;
- status?: RiskTaskStatus;
- column?: string;
- order?: string;
- page?: number;
- per_page?: number;
-}) {
- const skip = (page - 1) * per_page;
-
- const [tasks, total] = await Promise.all([
- db.riskMitigationTask
- .findMany({
- where: {
- riskId,
- AND: [
- search
- ? {
- OR: [
- { title: { contains: search, mode: "insensitive" } },
- {
- description: { contains: search, mode: "insensitive" },
- },
- ],
- }
- : {},
- status ? { status } : {},
- ],
- },
- orderBy: column
- ? {
- [column]: order === "asc" ? "asc" : "desc",
- }
- : {
- createdAt: "desc",
- },
- skip,
- take: per_page,
- include: {
- owner: {
- select: {
- name: true,
- image: true,
- },
- },
- },
- })
- .then((tasks) =>
- tasks.map(
- (task) =>
- ({
- ...task,
- dueDate: task.dueDate?.toISOString() ?? "",
- owner: {
- name: task.owner?.name ?? "",
- image: task.owner?.image ?? "",
- },
- }) as RiskTaskType,
- ),
- ),
- db.riskMitigationTask.count({
- where: {
- riskId,
- AND: [
- search
- ? {
- OR: [
- { title: { contains: search, mode: "insensitive" } },
- { description: { contains: search, mode: "insensitive" } },
- ],
- }
- : {},
- status ? { status } : {},
- ],
- },
- }),
- ]);
-
- return { tasks, total };
-};
-
-const getUsers = async (organizationId: string) => {
- const users = await db.user.findMany({
- where: { organizationId: organizationId },
- });
-
- return users;
-};
-
-export async function generateMetadata({
- params,
-}: {
- params: Promise<{ locale: string }>;
-}): Promise {
- const { locale } = await params;
- setStaticParamsLocale(locale);
- const t = await getI18n();
-
- return {
- title: t("sub_pages.risk.risk_overview"),
- };
-}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[riskId]/tasks/[taskId]/actions/getTaskAttachments.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[riskId]/tasks/[taskId]/actions/getTaskAttachments.ts
deleted file mode 100644
index 8da91dda97..0000000000
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[riskId]/tasks/[taskId]/actions/getTaskAttachments.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-"use server";
-
-import { authActionClient } from "@/actions/safe-action";
-import { db } from "@bubba/db";
-import { z } from "zod";
-
-export const getTaskAttachments = authActionClient
- .schema(
- z.object({
- id: z.string(),
- }),
- )
- .metadata({
- name: "getTaskAttachments",
- track: {
- event: "get-task-attachments",
- channel: "server",
- },
- })
- .action(async ({ ctx, parsedInput }) => {
- const { user } = ctx;
- const { id } = parsedInput;
-
- if (!user.organizationId) {
- return {
- success: false,
- error: "Not authorized - no organization found",
- };
- }
-
- try {
- const attachments = await db.taskAttachment.findMany({
- where: {
- riskMitigationTaskId: id,
- organizationId: user.organizationId,
- },
- select: {
- fileUrl: true,
- fileKey: true,
- },
- });
-
- if (!attachments) {
- return {
- success: false,
- error: "Task attachments not found",
- };
- }
-
- return {
- success: true,
- data: attachments,
- };
- } catch (error) {
- console.error("Error fetching task attachments:", error);
- return {
- success: false,
- error: "Failed to fetch task attachments",
- };
- }
- });
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[riskId]/tasks/[taskId]/hooks/useTaskAttachments.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[riskId]/tasks/[taskId]/hooks/useTaskAttachments.ts
deleted file mode 100644
index 9e773bf37d..0000000000
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[riskId]/tasks/[taskId]/hooks/useTaskAttachments.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-"use client";
-
-import useSWR from "swr";
-import { getTaskAttachments } from "../actions/getTaskAttachments";
-
-interface UseTaskAttachmentProps {
- id: string;
-}
-
-async function fetchTaskAttachments({ id }: UseTaskAttachmentProps) {
- const result = await getTaskAttachments({ id });
-
- if (!result || "error" in result) {
- throw new Error(
- typeof result?.error === "string"
- ? result.error
- : "Failed to fetch task attachments",
- );
- }
-
- return result.data;
-}
-
-export function useTaskAttachments({ id }: UseTaskAttachmentProps) {
- return useSWR(["task-attachments", id], () => fetchTaskAttachments({ id }), {
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- });
-}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[riskId]/tasks/[taskId]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[riskId]/tasks/[taskId]/page.tsx
deleted file mode 100644
index e1a97d1936..0000000000
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[riskId]/tasks/[taskId]/page.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import { auth } from "@/auth";
-import { TaskAttachments } from "@/components/risks/tasks/task-attachments";
-import { TaskComment } from "@/components/risks/tasks/task-comments";
-import { TaskOverview } from "@/components/risks/tasks/task-overview";
-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";
-
-interface PageProps {
- params: Promise<{ riskId: string; taskId: string }>;
-}
-
-export default async function RiskPage({ params }: PageProps) {
- const session = await auth();
- const { riskId, taskId } = await params;
-
- if (!session) {
- redirect("/auth");
- }
-
- if (!session.user.organizationId || !riskId) {
- redirect("/");
- }
-
- const task = await getTask(riskId, taskId);
-
- if (!task) {
- redirect("/risk");
- }
-
- const users = await getUsers(session.user.organizationId);
-
- return (
-
-
-
-
-
- );
-}
-
-const getTask = async (riskId: string, taskId: string) => {
- const task = await db.riskMitigationTask.findUnique({
- where: {
- riskId: riskId,
- id: taskId,
- },
- include: {
- owner: true,
- TaskAttachment: true,
- TaskComments: true,
- },
- });
-
- return task;
-};
-
-const getUsers = async (organizationId: string) => {
- const users = await db.user.findMany({
- where: { organizationId: organizationId },
- });
-
- return users;
-};
-
-export async function generateMetadata({
- params,
-}: {
- params: Promise<{ locale: string }>;
-}): Promise {
- const { locale } = await params;
- setStaticParamsLocale(locale);
- const t = await getI18n();
-
- return {
- title: t("sub_pages.risk.tasks.task_overview"),
- };
-}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[riskId]/tasks/search-params.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[riskId]/tasks/search-params.ts
deleted file mode 100644
index 55c0834e4e..0000000000
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[riskId]/tasks/search-params.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {
- createSearchParamsCache,
- parseAsInteger,
- parseAsString,
-} from "nuqs/server";
-
-export const searchParamsCache = createSearchParamsCache({
- q: parseAsString,
- page: parseAsInteger.withDefault(0),
- start: parseAsString,
- end: parseAsString,
- status: parseAsString,
- ownerId: parseAsString,
-});
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/comments/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/comments/page.tsx
new file mode 100644
index 0000000000..8cd21bbd94
--- /dev/null
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/comments/page.tsx
@@ -0,0 +1,67 @@
+import { auth } from "@/auth";
+
+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";
+
+interface PageProps {
+ params: Promise<{ vendorId: string }>;
+}
+
+export default async function VendorPage({ params }: PageProps) {
+ const session = await auth();
+ const { vendorId } = await params;
+
+ if (!session) {
+ redirect("/auth");
+ }
+
+ if (!session.user.organizationId || !vendorId) {
+ redirect("/");
+ }
+
+ const vendor = await db.vendor.findUnique({
+ where: {
+ id: vendorId,
+ organizationId: session.user.organizationId,
+ },
+ include: {
+ owner: true,
+ comments: {
+ orderBy: {
+ createdAt: "desc",
+ },
+ },
+ },
+ });
+
+ if (!vendor) {
+ redirect("/vendors");
+ }
+
+ const users = await db.user.findMany({
+ where: { organizationId: session.user.organizationId },
+ });
+
+ 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.vendor_comments"),
+ };
+}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[riskId]/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/layout.tsx
similarity index 67%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[riskId]/layout.tsx
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/layout.tsx
index a510061da6..9d484c7153 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[riskId]/layout.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/layout.tsx
@@ -5,8 +5,8 @@ import { SecondaryMenu } from "@bubba/ui/secondary-menu";
import { redirect } from "next/navigation";
interface LayoutProps {
- children: React.ReactNode;
- params: Promise<{ riskId: string }>;
+ children: React.ReactNode;
+ params: Promise<{ vendorId: string }>;
}
export default async function Layout({ children, params }: LayoutProps) {
@@ -19,12 +19,17 @@ export default async function Layout({ children, params }: LayoutProps) {
redirect("/");
}
- const riskId = await params;
- const risk = await getRisk(riskId.riskId, orgId);
+ const vendorId = await params;
+ const vendor = await db.vendor.findUnique({
+ where: {
+ id: vendorId.vendorId,
+ organizationId: orgId,
+ },
+ });
- if (!risk) {
- redirect("/risk");
- }
+ if (!vendor) {
+ redirect("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/vendors/register");
+ }
return (
@@ -42,14 +47,3 @@ export default async function Layout({ children, params }: LayoutProps) {
);
}
-
-const getRisk = async (riskId: string, organizationId: string) => {
- const risk = await db.risk.findUnique({
- where: {
- id: riskId,
- organizationId: organizationId,
- },
- });
-
- return risk;
-};
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
new file mode 100644
index 0000000000..53da904752
--- /dev/null
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/page.tsx
@@ -0,0 +1,187 @@
+import { auth } from "@/auth";
+import { Loading } from "@/components/frameworks/loading";
+// import { VendorOverview } from "@/components/vendors/vendor-overview";
+import type { VendorTaskType } from "@/components/tables/vendor-tasks/columns";
+import { DataTable } from "@/components/tables/vendor-tasks/data-table";
+import { NoResults, NoTasks } from "@/components/tables/vendor-tasks/empty-states";
+import { FilterToolbar } from "@/components/tables/vendor-tasks/filter-toolbar";
+import { getServerColumnHeaders } from "@/components/tables/vendor-tasks/server-columns";
+import { VendorOverview } from "@/components/vendors/vendor-overview";
+// import { DataTable } from "@/components/tables/vendor-tasks/data-table";
+// import {
+// NoResults,
+// NoTasks,
+// } from "@/components/tables/vendor-tasks/empty-states";
+// import { FilterToolbar } from "@/components/tables/vendor-tasks/filter-toolbar";
+// import { getServerColumnHeaders } from "@/components/tables/vendor-tasks/server-columns";
+import { getI18n } from "@/locales/server";
+import { db } from "@bubba/db";
+import type { VendorTaskStatus } from "@bubba/db/types";
+import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card";
+import type { Metadata } from "next";
+import { setStaticParamsLocale } from "next-international/server";
+import { redirect } from "next/navigation";
+
+interface PageProps {
+ searchParams: Promise<{
+ search?: string;
+ status?: string;
+ sort?: string;
+ page?: string;
+ per_page?: string;
+ }>;
+ params: Promise<{ vendorId: string; locale: string }>;
+}
+
+export default async function VendorPage({ searchParams, params }: PageProps) {
+ const session = await auth();
+ const t = await getI18n();
+ const { vendorId } = await params;
+ if (!session) {
+ redirect("/auth");
+ }
+
+ if (!session.user.organizationId || !vendorId) {
+ redirect("/");
+ }
+
+ const {
+ search,
+ status,
+ sort,
+ page = "1",
+ per_page = "5",
+ } = await searchParams;
+
+ const columnHeaders = await getServerColumnHeaders();
+ const [column, order] = sort?.split(":") ?? [];
+ const hasFilters = !!(search || status);
+
+ const vendor = await db.vendor.findUnique({
+ where: {
+ id: vendorId,
+ organizationId: session.user.organizationId,
+ },
+ include: {
+ owner: true,
+ },
+ });
+
+ if (!vendor) {
+ redirect("/vendors");
+ }
+
+ const users = await db.user.findMany({
+ where: { organizationId: session.user.organizationId },
+ });
+
+ const tasks = await db.vendorTask.findMany({
+ where: {
+ vendorId: vendorId,
+ AND: [
+ search
+ ? {
+ OR: [
+ { title: { contains: search, mode: "insensitive" } },
+ { description: { contains: search, mode: "insensitive" } },
+ ],
+ }
+ : {},
+ status ? { status: status as VendorTaskStatus } : {},
+ ],
+ },
+ orderBy: column
+ ? {
+ [column]: order === "asc" ? "asc" : "desc",
+ }
+ : {
+ createdAt: "desc",
+ },
+ skip: (Number(page) - 1) * Number(per_page),
+ take: Number(per_page),
+ include: {
+ owner: {
+ select: {
+ name: true,
+ image: true,
+ },
+ },
+ },
+ });
+
+ const total = await db.vendorTask.count({
+ where: {
+ vendorId: vendorId,
+ AND: [
+ search
+ ? {
+ OR: [
+ { title: { contains: search, mode: "insensitive" } },
+ { description: { contains: search, mode: "insensitive" } },
+ ],
+ }
+ : {},
+ status ? { status: status as VendorTaskStatus } : {},
+ ],
+ },
+ });
+
+ const loadedTasks = tasks.map((task) => ({
+ ...task,
+ dueDate: task.dueDate?.toISOString() ?? "",
+ owner: {
+ name: task.owner?.name ?? "",
+ image: task.owner?.image ?? "",
+ },
+ })) as VendorTaskType[];
+
+ return (
+
+
+
+
+
+
+
+ {t("vendor.tasks.title")}
+
+
+
+
+
+
+ {loadedTasks.length > 0 ? (
+
+ ) : hasFilters ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
+
+
+ );
+}
+
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{ locale: string }>;
+}): Promise {
+ const { locale } = await params;
+ setStaticParamsLocale(locale);
+ const t = await getI18n();
+
+ return {
+ title: t("vendors.vendor_overview"),
+ };
+}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/actions/create-vendor-action.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/actions/create-vendor-action.ts
new file mode 100644
index 0000000000..58394ca105
--- /dev/null
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/actions/create-vendor-action.ts
@@ -0,0 +1,50 @@
+'use server'
+
+import { createSafeActionClient } from "next-safe-action";
+import { z } from "zod";
+import type { ActionResponse } from '@/types/actions';
+import { VendorStatus, VendorCategory } from "@bubba/db/types";
+import { revalidatePath } from "next/cache";
+import { auth } from "@/auth";
+import { db } from "@bubba/db";
+
+const schema = z.object({
+ name: z.string().min(1, "Name is required"),
+ website: z.string().url("Must be a valid URL").optional(),
+ description: z.string().optional(),
+ category: z.nativeEnum(VendorCategory),
+ status: z.nativeEnum(VendorStatus).default(VendorStatus.not_assessed),
+ ownerId: z.string().optional(),
+});
+
+export const createVendorAction = createSafeActionClient()
+ .schema(schema)
+ .action(async (input): Promise => {
+ try {
+ const session = await auth();
+
+ if (!session?.user?.organizationId) {
+ throw new Error("Unauthorized");
+ }
+
+ const vendor = await db.vendor.create({
+ data: {
+ name: input.parsedInput.name,
+ description: input.parsedInput.description || "",
+ category: input.parsedInput.category,
+ status: input.parsedInput.status,
+ ownerId: input.parsedInput.ownerId,
+ organizationId: session.user.organizationId,
+ }
+ });
+
+ revalidatePath(`/${session.user.organizationId}/vendors`);
+
+ return { success: true, data: vendor };
+ } catch (error) {
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "Failed to create vendor"
+ };
+ }
+ });
\ No newline at end of file
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/components/create-risk-form.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/components/create-vendor-form.tsx
similarity index 61%
rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/components/create-risk-form.tsx
rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/components/create-vendor-form.tsx
index d79fce263d..de7aa7b4b3 100644
--- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/components/create-risk-form.tsx
+++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/components/create-vendor-form.tsx
@@ -1,13 +1,9 @@
"use client";
-import { createRiskAction } from "@/actions/risk/create-risk-action";
-import { createRiskSchema } from "@/actions/schema";
import { useOrganizationAdmins } from "@/app/[locale]/(app)/(dashboard)/[orgId]/hooks/useOrganizationAdmins";
-import { useRisks } from "@/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/hooks/useRisks";
import { SelectUser } from "@/components/select-user";
import { useI18n } from "@/locales/client";
-import type { RiskStatus } from "@bubba/db/types";
-import { Departments, RiskCategory } from "@bubba/db/types";
+import { VendorCategory, VendorStatus } from "@bubba/db/types";
import {
Accordion,
AccordionContent,
@@ -34,11 +30,13 @@ import {
import { Textarea } from "@bubba/ui/textarea";
import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowRightIcon } from "lucide-react";
+import { useSession } from "next-auth/react";
import { useAction } from "next-safe-action/hooks";
import { useQueryState } from "nuqs";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
-import type { z } from "zod";
+import { z } from "zod";
+import { createVendorAction } from "../actions/create-vendor-action";
interface User {
id: string;
@@ -46,8 +44,18 @@ interface User {
name: string | null;
}
-export function CreateRisk() {
+const createVendorSchema = z.object({
+ name: z.string().min(1, "Name is required"),
+ website: z.string().url("Must be a valid URL").optional(),
+ description: z.string().optional(),
+ category: z.nativeEnum(VendorCategory),
+ status: z.nativeEnum(VendorStatus).default(VendorStatus.not_assessed),
+ ownerId: z.string().optional(),
+});
+
+export function CreateVendor() {
const t = useI18n();
+ const session = useSession();
// Get the same query parameters as the table
const [search] = useQueryState("search");
@@ -59,55 +67,51 @@ export function CreateRisk() {
defaultValue: 10,
parse: Number,
});
- const [status] = useQueryState("status", {
+ const [status] = useQueryState("status", {
defaultValue: null,
- parse: (value) => value as RiskStatus | null,
+ parse: (value) => value as VendorStatus | null,
});
- const [department] = useQueryState("department", {
+ const [category] = useQueryState("category", {
defaultValue: null,
- parse: (value) => value as Departments | null,
+ parse: (value) => value as VendorCategory | null,
});
const [assigneeId] = useQueryState("assigneeId", {
defaultValue: null,
parse: (value) => value,
});
- const { mutate: mutateRisks } = useRisks({
- search: search || "",
- page: Number(page),
- pageSize: Number(pageSize),
- status,
- department,
- assigneeId,
- });
-
const { data: admins, isLoading: isLoadingAdmins } = useOrganizationAdmins();
- const [_, setCreateRiskSheet] = useQueryState("create-risk-sheet");
+ const [_, setCreateVendorSheet] = useQueryState("create-vendor-sheet");
- const createRisk = useAction(createRiskAction, {
+ const createVendor = useAction(createVendorAction, {
onSuccess: async () => {
- toast.success(t("risk.form.create_risk_success"));
- setCreateRiskSheet(null);
- // Force invalidate and revalidate the risks list
- await mutateRisks();
+ const organizationId = session.data?.user?.organizationId;
+
+ if (!organizationId) {
+ toast.error(t("vendors.form.create_vendor_error"));
+ return;
+ }
+
+ toast.success(t("vendors.form.create_vendor_success"));
+ setCreateVendorSheet(null);
},
onError: () => {
- toast.error(t("risk.form.create_risk_error"));
+ toast.error(t("vendors.form.create_vendor_error"));
},
});
- const form = useForm>({
- resolver: zodResolver(createRiskSchema),
+ const form = useForm>({
+ resolver: zodResolver(createVendorSchema),
defaultValues: {
- title: "",
+ name: "",
description: "",
- category: RiskCategory.operations,
- department: Departments.admin,
+ category: VendorCategory.cloud,
+ status: VendorStatus.not_assessed,
},
});
- const onSubmit = (data: z.infer) => {
- createRisk.execute(data);
+ const onSubmit = (data: z.infer) => {
+ createVendor.execute(data);
};
return (
@@ -115,27 +119,25 @@ export function CreateRisk() {