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() {
- - + + - {t("risk.form.risk_details")} + {t("vendors.form.vendor_details")}
( - {t("risk.form.risk_title")} + {t("vendors.form.vendor_name")} @@ -143,21 +145,35 @@ export function CreateRisk() { )} /> + ( + + {t("vendors.form.vendor_website")} + + + + + + )} + /> ( - - {t("risk.form.risk_description")} - + {t("vendors.form.vendor_description")}