From a9760153e5dcc80f00e9b8e4b2f0f59632a6cbd4 Mon Sep 17 00:00:00 2001 From: Lewis Carhart Date: Thu, 6 Mar 2025 19:07:51 +0000 Subject: [PATCH 1/2] feat: Implement Vendor Management Dashboard and Components Add comprehensive vendor management features: - Create vendor overview page with status and category charts - Implement vendor-specific layouts and routing - Add new vendor-related Prisma models and migrations - Update main menu to enable vendor navigation - Localize vendor dashboard and overview translations - Enhance UI with responsive design and chart components --- .../(app)/(dashboard)/settings/layout.tsx | 2 +- .../(dashboard)/vendors/(overview)/layout.tsx | 23 ++ .../(dashboard)/vendors/(overview)/page.tsx | 64 ++++++ apps/app/src/components/main-menu.tsx | 20 +- .../risks/charts/risks-by-department.tsx | 91 ++++---- .../risks/charts/risks-by-status.tsx | 53 +++-- .../tables/api-keys/create-api-key-dialog.tsx | 2 +- .../src/components/tables/api-keys/index.tsx | 2 +- .../vendors/charts/category-chart.tsx | 209 +++++++++++++++++ .../vendors/charts/status-chart.tsx | 213 ++++++++++++++++++ .../vendors/charts/vendors-by-category.tsx | 73 ++++++ .../vendors/charts/vendors-by-status.tsx | 42 ++++ .../vendors/charts/vendors-overview.tsx | 15 ++ apps/app/src/locales/en.ts | 89 ++------ .../migration.sql | 168 ++++++++++++++ .../20250306181056_vendors/migration.sql | 20 ++ packages/db/prisma/schema/schema.prisma | 40 +--- packages/db/prisma/schema/vendor.prisma | 137 +++++++++++ packages/integrations/src/aws/config.ts | 73 +++--- 19 files changed, 1114 insertions(+), 222 deletions(-) create mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/vendors/(overview)/layout.tsx create mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/vendors/(overview)/page.tsx create mode 100644 apps/app/src/components/vendors/charts/category-chart.tsx create mode 100644 apps/app/src/components/vendors/charts/status-chart.tsx create mode 100644 apps/app/src/components/vendors/charts/vendors-by-category.tsx create mode 100644 apps/app/src/components/vendors/charts/vendors-by-status.tsx create mode 100644 apps/app/src/components/vendors/charts/vendors-overview.tsx create mode 100644 packages/db/prisma/migrations/20250306175455_vendor_management/migration.sql create mode 100644 packages/db/prisma/migrations/20250306181056_vendors/migration.sql diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/settings/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/settings/layout.tsx index 5f1625c5ac..8e70f1159c 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/settings/layout.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/settings/layout.tsx @@ -10,7 +10,7 @@ export default async function Layout({ const t = await getI18n(); return ( -
+
Loading...
}> + + +
{children}
+
+ ); +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/vendors/(overview)/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/vendors/(overview)/page.tsx new file mode 100644 index 0000000000..eb880f6f6c --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/vendors/(overview)/page.tsx @@ -0,0 +1,64 @@ +import { auth } from "@/auth"; +import { VendorOverview } from "@/components/vendors/charts/vendors-overview"; +import { getI18n } from "@/locales/server"; +import { db } from "@bubba/db"; +import type { Metadata } from "next"; +import { setStaticParamsLocale } from "next-international/server"; +import { unstable_cache } from "next/cache"; +import { redirect } from "next/navigation"; + +export default async function VendorOverviewPage({ + params, +}: { + 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); + + return ( +
+
+ +
+
+ ); +} + +const getVendorOverview = unstable_cache( + async (organizationId: string) => { + return await db.$transaction(async (tx) => { + const [vendors] = await Promise.all([ + tx.vendor.count({ + where: { organizationId }, + }), + ]); + + return { + vendors, + }; + }); + }, + ["vendor-overview-cache"], +); + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + setStaticParamsLocale(locale); + const t = await getI18n(); + + return { + title: t("sidebar.vendors"), + }; +} diff --git a/apps/app/src/components/main-menu.tsx b/apps/app/src/components/main-menu.tsx index 99b4b04de6..e39e5bf5f6 100644 --- a/apps/app/src/components/main-menu.tsx +++ b/apps/app/src/components/main-menu.tsx @@ -174,13 +174,18 @@ export function MainMenu({ initialItems, onSelect }: Props) { disabled: false, }, { - path: "/policies", - name: t("sidebar.policies"), + path: "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/evidence/overview", + name: t("sidebar.evidence"), disabled: false, }, { - path: "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/evidence/overview", - name: t("sidebar.evidence"), + path: "/tests", + name: t("sidebar.tests"), + disabled: false, + }, + { + path: "/policies", + name: t("sidebar.policies"), disabled: false, }, { @@ -193,15 +198,10 @@ export function MainMenu({ initialItems, onSelect }: Props) { name: t("sidebar.risk"), disabled: false, }, - { - path: "/tests", - name: t("sidebar.tests"), - disabled: false, - }, { path: "/vendors", name: t("sidebar.vendors"), - disabled: true, + disabled: false, }, { path: "/integrations", diff --git a/apps/app/src/components/risks/charts/risks-by-department.tsx b/apps/app/src/components/risks/charts/risks-by-department.tsx index 8263668970..bc111cfb47 100644 --- a/apps/app/src/components/risks/charts/risks-by-department.tsx +++ b/apps/app/src/components/risks/charts/risks-by-department.tsx @@ -2,65 +2,68 @@ import { db } from "@bubba/db"; import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card"; import { DepartmentChart } from "./department-chart"; import { unstable_cache } from "next/cache"; +import { getI18n } from "@/locales/server"; const ALL_DEPARTMENTS = ["none", "admin", "gov", "hr", "it", "itsm", "qms"]; interface Props { - organizationId: string; + organizationId: string; } export async function RisksByDepartment({ organizationId }: Props) { - const risks = await getRisksByDepartment(organizationId); + const t = await getI18n(); - const data = ALL_DEPARTMENTS.map((dept) => { - const found = risks.find( - (risk) => - (risk.department || "none").toLowerCase() === dept.toLowerCase(), - ); + const risks = await getRisksByDepartment(organizationId); - return { - name: dept === "none" ? "None" : dept.toUpperCase(), - value: found ? found._count : 0, - }; - }).sort((a, b) => b.value - a.value); + const data = ALL_DEPARTMENTS.map((dept) => { + const found = risks.find( + (risk) => + (risk.department || "none").toLowerCase() === dept.toLowerCase(), + ); - // Separate departments with values > 0 and departments with values = 0 - const departmentsWithValues = data.filter((dept) => dept.value > 0); - const departmentsWithoutValues = data.filter((dept) => dept.value === 0); + return { + name: dept === "none" ? "None" : dept.toUpperCase(), + value: found ? found._count : 0, + }; + }).sort((a, b) => b.value - a.value); - // Determine which departments to show - let departmentsToShow = [...departmentsWithValues]; + // Separate departments with values > 0 and departments with values = 0 + const departmentsWithValues = data.filter((dept) => dept.value > 0); + const departmentsWithoutValues = data.filter((dept) => dept.value === 0); - // If we have fewer than 4 departments with values, show up to 2 departments with no values - if (departmentsWithValues.length < 4 && departmentsWithoutValues.length > 0) { - departmentsToShow = [ - ...departmentsWithValues, - ...departmentsWithoutValues.slice(0, 2), - ]; - } + // Determine which departments to show + let departmentsToShow = [...departmentsWithValues]; - return ( - - - Risks by Department - - - - - - ); + // If we have fewer than 4 departments with values, show up to 2 departments with no values + if (departmentsWithValues.length < 4 && departmentsWithoutValues.length > 0) { + departmentsToShow = [ + ...departmentsWithValues, + ...departmentsWithoutValues.slice(0, 2), + ]; + } + + return ( + + + {t("dashboard.risks_by_department")} + + + + + + ); } const getRisksByDepartment = unstable_cache( - async (organizationId: string) => { - const risksByDepartment = await db.risk.groupBy({ - by: ["department"], - where: { organizationId }, - _count: true, - }); + async (organizationId: string) => { + const risksByDepartment = await db.risk.groupBy({ + by: ["department"], + where: { organizationId }, + _count: true, + }); - return risksByDepartment; - }, - ["risks-by-department"], - { tags: ["risks", "departments"] }, + return risksByDepartment; + }, + ["risks-by-department"], + { tags: ["risks", "departments"] }, ); diff --git a/apps/app/src/components/risks/charts/risks-by-status.tsx b/apps/app/src/components/risks/charts/risks-by-status.tsx index bdb91242b0..011fd64ed7 100644 --- a/apps/app/src/components/risks/charts/risks-by-status.tsx +++ b/apps/app/src/components/risks/charts/risks-by-status.tsx @@ -2,39 +2,42 @@ import { db } from "@bubba/db"; import { StatusChart } from "./status-chart"; import { unstable_cache } from "next/cache"; import { Card, CardHeader, CardTitle, CardContent } from "@bubba/ui/card"; +import { getI18n } from "@/locales/server"; interface Props { - organizationId: string; + organizationId: string; } export async function RisksByStatus({ organizationId }: Props) { - const risks = await getRisksByStatus(organizationId); + const t = await getI18n(); - const data = risks.map((risk) => ({ - name: risk.status, - value: risk._count, - })); + const risks = await getRisksByStatus(organizationId); - return ( - - - Risks by Status - - - - - - ); + const data = risks.map((risk) => ({ + name: risk.status, + value: risk._count, + })); + + return ( + + + {t("dashboard.risk_status")} + + + + + + ); } const getRisksByStatus = unstable_cache( - async (organizationId: string) => { - return await db.risk.groupBy({ - by: ["status"], - where: { organizationId }, - _count: true, - }); - }, - ["risks-by-status"], - { tags: ["risks", "status"] }, + async (organizationId: string) => { + return await db.risk.groupBy({ + by: ["status"], + where: { organizationId }, + _count: true, + }); + }, + ["risks-by-status"], + { tags: ["risks", "status"] }, ); diff --git a/apps/app/src/components/tables/api-keys/create-api-key-dialog.tsx b/apps/app/src/components/tables/api-keys/create-api-key-dialog.tsx index 0de66e024c..b6fd0f1e8c 100644 --- a/apps/app/src/components/tables/api-keys/create-api-key-dialog.tsx +++ b/apps/app/src/components/tables/api-keys/create-api-key-dialog.tsx @@ -211,7 +211,7 @@ export function CreateApiKeyDialog({ - + diff --git a/apps/app/src/components/tables/api-keys/index.tsx b/apps/app/src/components/tables/api-keys/index.tsx index cd29f78b9e..898624e5f5 100644 --- a/apps/app/src/components/tables/api-keys/index.tsx +++ b/apps/app/src/components/tables/api-keys/index.tsx @@ -99,7 +99,7 @@ export function ApiKeysTable() { return ( <> - +
{t("settings.api_keys.list_title")} diff --git a/apps/app/src/components/vendors/charts/category-chart.tsx b/apps/app/src/components/vendors/charts/category-chart.tsx new file mode 100644 index 0000000000..f1288b341a --- /dev/null +++ b/apps/app/src/components/vendors/charts/category-chart.tsx @@ -0,0 +1,209 @@ +"use client"; + +import React, { type CSSProperties } from "react"; +import { scaleBand, scaleLinear, max, format } from "d3"; +import { ClientTooltip } from "@bubba/ui/chart-tooltip"; + +const VENDOR_CATEGORY_COLORS = { + cloud: "bg-[var(--chart-open)]", + infrastructure: "bg-[var(--chart-pending)]", + software_as_a_service: "bg-[var(--chart-closed)]", + finance: "bg-[var(--chart-open)]", + marketing: "bg-[var(--chart-pending)]", + sales: "bg-[var(--chart-closed)]", + hr: "bg-[var(--chart-open)]", + other: "bg-[var(--chart-pending)]", +}; + +interface VendorCategoryData { + name: string; + value: number; +} + +interface VendorCategoryChartProps { + data: VendorCategoryData[]; + showEmptyDepartments?: boolean; +} + +export function VendorCategoryChart({ + data, + showEmptyDepartments = true, +}: VendorCategoryChartProps) { + const filteredData = showEmptyDepartments + ? data + : data.filter((dept) => dept.value > 0); + + const sortedData = [...filteredData].sort((a, b) => b.value - a.value); + + // Return early with a message if no departments have risks + if (sortedData.length === 0) { + return ( +
+ No departments with risks found +
+ ); + } + + // Define fixed settings for consistent bar sizing + const barHeight = 40; // Fixed height for each bar in pixels + const barGap = 16; // Gap between bars in pixels + const minChartHeight = 300; // Minimum chart height + + // Calculate dynamic chart height based on number of departments + const chartHeight = Math.max( + minChartHeight, + (barHeight + barGap) * sortedData.length + 40, // Extra padding at top/bottom + ); + + // Get the maximum value for scaling + const maxValue = max(sortedData.map((d) => d.value)) ?? 1; + + // Scales + const yScale = scaleBand() + .domain(sortedData.map((d) => d.name)) + .range([0, 100]) + .padding(0.25); + + const xScale = scaleLinear().domain([0, maxValue]).range([0, 100]); + + const marginLeft = 70; + const marginRight = 20; + const marginBottom = 20; + + const getBarKey = (item: VendorCategoryData) => + `bar-${item.name}-${item.value}`; + const getTickKey = (value: number) => `tick-${value}`; + const getGridKey = (value: number, position = 0) => + `grid-${value.toString().replace(".", "-")}-${position}`; + const getLabelKey = (item: VendorCategoryData) => `label-${item.name}`; + + const getCategoryColor = (categoryName: string) => { + const normalizedName = categoryName.toLowerCase(); + return ( + VENDOR_CATEGORY_COLORS[ + normalizedName as keyof typeof VENDOR_CATEGORY_COLORS + ] || "bg-gray-400" + ); + }; + + // Generate appropriate tick values based on max value + const generateTickValues = () => { + // For small values, generate specific tick values + if (maxValue <= 1) return [0, 1]; + if (maxValue <= 2) return [0, 1, 2]; + if (maxValue <= 5) return [0, 1, 2, 3, 4, 5]; + if (maxValue <= 10) return [0, 2, 4, 6, 8, 10]; + + // For larger values, let D3 handle it with a reasonable number of ticks + const tickCount = Math.min(maxValue, 6); // Limit number of ticks + return xScale.ticks(tickCount).map(Number); + }; + + const tickValues = generateTickValues(); + + return ( + +
+
+ {sortedData.map((d, index) => { + const barWidth = d.value === 0 ? 3 : xScale(d.value); + const fixedBarHeightPercentage = (barHeight / chartHeight) * 100; + + // Calculate exact position to align with labels + // Get center point of the band for this item + const bandCenter = yScale(d.name)! + yScale.bandwidth() / 2; + // Position bar so its center aligns with the band center + const barTopPosition = bandCenter - fixedBarHeightPercentage / 2; + + return ( +
+ ); + })} + + {tickValues.map((value, position) => { + const uniqueKey = getGridKey(value, position); + return ( + + + + ); + })} + + + {tickValues.map((value) => ( +
+ {Number.isInteger(value) ? format(",")(value) : value.toFixed(2)} +
+ ))} +
+ +
+ {sortedData.map((entry) => ( + + {entry.name} + + ))} +
+
+ + ); +} diff --git a/apps/app/src/components/vendors/charts/status-chart.tsx b/apps/app/src/components/vendors/charts/status-chart.tsx new file mode 100644 index 0000000000..984aa5a2e6 --- /dev/null +++ b/apps/app/src/components/vendors/charts/status-chart.tsx @@ -0,0 +1,213 @@ +"use client"; + +import React, { type CSSProperties } from "react"; +import { scaleBand, scaleLinear, max, format } from "d3"; +import { ClientTooltip } from "@bubba/ui/chart-tooltip"; + +const STATUS_COLORS = { + not_assessed: "bg-[var(--chart-open)]", + in_progress: "bg-[var(--chart-pending)]", + assessed: "bg-[var(--chart-closed)]", +}; + +interface StatusData { + name: string; + value: number; +} + +interface StatusChartProps { + data: StatusData[]; +} + +export function StatusChart({ data }: StatusChartProps) { + const ensureAllStatuses = (inputData: StatusData[]): StatusData[] => { + const result = inputData.map((item) => ({ + ...item, + name: + item.name.charAt(0).toUpperCase() + item.name.slice(1).toLowerCase(), + })); + + const statusNames = Object.keys(STATUS_COLORS); + + for (const status of statusNames) { + const capitalized = status.charAt(0).toUpperCase() + status.slice(1); + if (!result.some((item) => item.name.toLowerCase() === status)) { + result.push({ name: capitalized, value: 0 }); + } + } + + return result; + }; + + const sortedData = [...ensureAllStatuses(data)].sort( + (a, b) => b.value - a.value, + ); + + if (sortedData.length === 0) { + return ( +
+ No statuses with risks found +
+ ); + } + + const barHeight = 40; // Fixed height for each bar in pixels + const barGap = 16; // Gap between bars in pixels + const minChartHeight = 300; // Minimum chart height + + // Calculate dynamic chart height based on number of departments + const chartHeight = Math.max( + minChartHeight, + (barHeight + barGap) * sortedData.length + 40, // Extra padding at top/bottom + ); + + // Get the maximum value for scaling + const maxValue = max(sortedData.map((d) => d.value)) ?? 1; + + // Scales + const yScale = scaleBand() + .domain(sortedData.map((d) => d.name)) + .range([0, 100]) + .padding(0.25); + + const xScale = scaleLinear().domain([0, maxValue]).range([0, 100]); + + const marginLeft = 70; + const marginRight = 20; + const marginBottom = 20; + + const getBarKey = (item: StatusData) => `bar-${item.name}-${item.value}`; + const getTickKey = (value: number) => `tick-${value}`; + const getGridKey = (value: number, position = 0) => + `grid-${value.toString().replace(".", "-")}-${position}`; + const getLabelKey = (item: StatusData) => `label-${item.name}`; + + const getStatusColor = (statusName: string) => { + const normalizedName = statusName.toLowerCase(); + return ( + STATUS_COLORS[normalizedName as keyof typeof STATUS_COLORS] || + "bg-gray-400" + ); + }; + + // Generate appropriate tick values based on max value + const generateTickValues = () => { + // For small values, generate specific tick values + if (maxValue <= 1) return [0, 1]; + if (maxValue <= 2) return [0, 1, 2]; + if (maxValue <= 5) return [0, 1, 2, 3, 4, 5]; + if (maxValue <= 10) return [0, 2, 4, 6, 8, 10]; + + // For larger values, let D3 handle it with a reasonable number of ticks + const tickCount = Math.min(maxValue, 6); // Limit number of ticks + return xScale.ticks(tickCount).map(Number); + }; + + const tickValues = generateTickValues(); + + return ( + +
+
+ {sortedData.map((d, index) => { + const barWidth = d.value === 0 ? 3 : xScale(d.value); + const fixedBarHeightPercentage = (barHeight / chartHeight) * 100; + + // Calculate exact position to align with labels + // Get center point of the band for this item + const bandCenter = yScale(d.name)! + yScale.bandwidth() / 2; + // Position bar so its center aligns with the band center + const barTopPosition = bandCenter - fixedBarHeightPercentage / 2; + + return ( +
+ ); + })} + + {tickValues.map((value, position) => { + const uniqueKey = getGridKey(value, position); + return ( + + + + ); + })} + + + {tickValues.map((value) => ( +
+ {Number.isInteger(value) ? format(",")(value) : value.toFixed(2)} +
+ ))} +
+ +
+ {sortedData.map((entry) => ( + + {entry.name} + + ))} +
+
+ + ); +} diff --git a/apps/app/src/components/vendors/charts/vendors-by-category.tsx b/apps/app/src/components/vendors/charts/vendors-by-category.tsx new file mode 100644 index 0000000000..55ee5d30de --- /dev/null +++ b/apps/app/src/components/vendors/charts/vendors-by-category.tsx @@ -0,0 +1,73 @@ +import { db } from "@bubba/db"; +import { VendorCategory } from "@bubba/db"; +import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card"; +import { VendorCategoryChart } from "./category-chart"; +import { unstable_cache } from "next/cache"; +import { getI18n } from "@/locales/server"; + +const VENDOR_CATEGORIES = Object.values(VendorCategory); + +interface Props { + organizationId: string; +} + +export async function VendorsByCategory({ organizationId }: Props) { + const t = await getI18n(); + + const vendors = await getVendorsByCategory(organizationId); + + const data = VENDOR_CATEGORIES.map((category) => { + const found = vendors.find( + (vendor) => + (vendor.category || "other").toLowerCase() === category.toLowerCase(), + ); + + return { + name: category === "other" ? "Other" : category.toUpperCase(), + value: found ? found._count : 0, + }; + }).sort((a, b) => b.value - a.value); + + // Separate departments with values > 0 and departments with values = 0 + const categoriesWithValues = data.filter((dept) => dept.value > 0); + const categoriesWithoutValues = data.filter((dept) => dept.value === 0); + + // Determine which departments to show + let categoriesToShow = [...categoriesWithValues]; + + // If we have fewer than 4 departments with values, show up to 2 departments with no values + if (categoriesWithValues.length < 4 && categoriesWithoutValues.length > 0) { + categoriesToShow = [ + ...categoriesWithValues, + ...categoriesWithoutValues.slice(0, 2), + ]; + } + + return ( + + + {t("dashboard.vendors_by_category")} + + + + + + ); +} + +const getVendorsByCategory = unstable_cache( + async (organizationId: string) => { + const vendorsByCategory = await db.vendor.groupBy({ + by: ["category"], + where: { organizationId }, + _count: true, + }); + + return vendorsByCategory; + }, + ["vendors-by-category"], + { tags: ["vendors", "categories"] }, +); diff --git a/apps/app/src/components/vendors/charts/vendors-by-status.tsx b/apps/app/src/components/vendors/charts/vendors-by-status.tsx new file mode 100644 index 0000000000..6e5df999f9 --- /dev/null +++ b/apps/app/src/components/vendors/charts/vendors-by-status.tsx @@ -0,0 +1,42 @@ +import { db } from "@bubba/db"; +import { StatusChart } from "./status-chart"; +import { unstable_cache } from "next/cache"; +import { Card, CardHeader, CardTitle, CardContent } from "@bubba/ui/card"; +import { getI18n } from "@/locales/server"; +interface Props { + organizationId: string; +} + +export async function VendorsByStatus({ organizationId }: Props) { + const t = await getI18n(); + + const vendors = await getVendorsByStatus(organizationId); + + const data = vendors.map((vendor) => ({ + name: vendor.status, + value: vendor._count, + })); + + return ( + + + {t("dashboard.vendor_status")} + + + + + + ); +} + +const getVendorsByStatus = unstable_cache( + async (organizationId: string) => { + return await db.vendor.groupBy({ + by: ["status"], + where: { organizationId }, + _count: true, + }); + }, + ["vendors-by-status"], + { tags: ["vendors", "status"] }, +); diff --git a/apps/app/src/components/vendors/charts/vendors-overview.tsx b/apps/app/src/components/vendors/charts/vendors-overview.tsx new file mode 100644 index 0000000000..57d082dd68 --- /dev/null +++ b/apps/app/src/components/vendors/charts/vendors-overview.tsx @@ -0,0 +1,15 @@ +import { VendorsByCategory } from "./vendors-by-category"; +import { VendorsByStatus } from "./vendors-by-status"; + +interface VendorOverviewProps { + organizationId: string; +} + +export function VendorOverview({ organizationId }: VendorOverviewProps) { + return ( + <> + + + + ); +} diff --git a/apps/app/src/locales/en.ts b/apps/app/src/locales/en.ts index c00d910d9c..f4011ec632 100644 --- a/apps/app/src/locales/en.ts +++ b/apps/app/src/locales/en.ts @@ -684,8 +684,8 @@ export default { list_title: "API Keys", list_description: "API keys allow secure access to your organization's data via our API.", - create: "Create API Key", - create_title: "Create API Key", + create: "New API Key", + create_title: "New API Key", create_description: "Create a new API key for programmatic access to your organization's data.", created_title: "API Key Created", @@ -857,76 +857,6 @@ export default { }, }, }, - vendor: { - title: "Dashboard", - register_title: "Vendor Management", - dashboard: { - title: "Dashboard", - overview: "Vendor Overview", - vendor_status: "Vendor Status", - vendor_category: "Vendor Categories", - vendors_by_assignee: "Vendors by Assignee", - 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", - }, - }, - category: { - cloud: "Cloud", - infrastructure: "Infrastructure", - software_as_a_service: "SaaS", - finance: "Finance", - marketing: "Marketing", - sales: "Sales", - hr: "HR", - other: "Other", - }, - vendors: "vendors", - 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", - update_vendor_success: "Vendor updated successfully", - update_vendor_error: "Failed to update vendor", - contacts: "Vendor Contacts", - contact_name: "Contact Name", - contact_email: "Contact Email", - contact_role: "Contact Role", - add_contact: "Add Contact", - new_contact: "New Contact", - min_one_contact_required: "A vendor must have at least one contact", - }, - 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", - }, - }, - }, errors: { unexpected: "Something went wrong, please try again", }, @@ -983,4 +913,19 @@ export default { content: "Evidence Content", }, }, + vendors: { + title: "Vendors", + register: { + title: "Vendor Register", + }, + dashboard: { + title: "Vendor Overview", + }, + }, + dashboard: { + risk_status: "Risk Status", + risks_by_department: "Risks by Department", + vendor_status: "Vendor Status", + vendors_by_category: "Vendors by Category", + }, } as const; diff --git a/packages/db/prisma/migrations/20250306175455_vendor_management/migration.sql b/packages/db/prisma/migrations/20250306175455_vendor_management/migration.sql new file mode 100644 index 0000000000..225b8432df --- /dev/null +++ b/packages/db/prisma/migrations/20250306175455_vendor_management/migration.sql @@ -0,0 +1,168 @@ +-- CreateTable +CREATE TABLE "Vendor" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "organizationId" TEXT NOT NULL, + + CONSTRAINT "Vendor_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "VendorContact" ( + "id" TEXT NOT NULL, + "vendorId" TEXT NOT NULL, + "organizationId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "email" TEXT NOT NULL, + "phone" TEXT NOT NULL, + + CONSTRAINT "VendorContact_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "VendorComment" ( + "id" TEXT NOT NULL, + "vendorId" TEXT NOT NULL, + "organizationId" TEXT NOT NULL, + "content" TEXT NOT NULL, + + CONSTRAINT "VendorComment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "VendorAttachment" ( + "id" TEXT NOT NULL, + "vendorId" TEXT NOT NULL, + "organizationId" TEXT NOT NULL, + + CONSTRAINT "VendorAttachment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "VendorTask" ( + "id" TEXT NOT NULL, + "vendorId" TEXT NOT NULL, + "organizationId" TEXT NOT NULL, + + CONSTRAINT "VendorTask_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "VendorTaskAttachment" ( + "id" TEXT NOT NULL, + "vendorId" TEXT NOT NULL, + "organizationId" TEXT NOT NULL, + + CONSTRAINT "VendorTaskAttachment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "VendorTaskComment" ( + "id" TEXT NOT NULL, + "vendorId" TEXT NOT NULL, + "organizationId" TEXT NOT NULL, + + CONSTRAINT "VendorTaskComment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "VendorTaskAssignment" ( + "id" TEXT NOT NULL, + "vendorId" TEXT NOT NULL, + "organizationId" TEXT NOT NULL, + + CONSTRAINT "VendorTaskAssignment_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "VendorContact_organizationId_idx" ON "VendorContact"("organizationId"); + +-- CreateIndex +CREATE INDEX "VendorContact_vendorId_idx" ON "VendorContact"("vendorId"); + +-- CreateIndex +CREATE INDEX "VendorComment_organizationId_idx" ON "VendorComment"("organizationId"); + +-- CreateIndex +CREATE INDEX "VendorComment_vendorId_idx" ON "VendorComment"("vendorId"); + +-- CreateIndex +CREATE INDEX "VendorAttachment_organizationId_idx" ON "VendorAttachment"("organizationId"); + +-- CreateIndex +CREATE INDEX "VendorAttachment_vendorId_idx" ON "VendorAttachment"("vendorId"); + +-- CreateIndex +CREATE INDEX "VendorTask_organizationId_idx" ON "VendorTask"("organizationId"); + +-- CreateIndex +CREATE INDEX "VendorTask_vendorId_idx" ON "VendorTask"("vendorId"); + +-- CreateIndex +CREATE INDEX "VendorTaskAttachment_organizationId_idx" ON "VendorTaskAttachment"("organizationId"); + +-- CreateIndex +CREATE INDEX "VendorTaskAttachment_vendorId_idx" ON "VendorTaskAttachment"("vendorId"); + +-- CreateIndex +CREATE INDEX "VendorTaskComment_organizationId_idx" ON "VendorTaskComment"("organizationId"); + +-- CreateIndex +CREATE INDEX "VendorTaskComment_vendorId_idx" ON "VendorTaskComment"("vendorId"); + +-- CreateIndex +CREATE INDEX "VendorTaskAssignment_organizationId_idx" ON "VendorTaskAssignment"("organizationId"); + +-- CreateIndex +CREATE INDEX "VendorTaskAssignment_vendorId_idx" ON "VendorTaskAssignment"("vendorId"); + +-- CreateIndex +CREATE INDEX "Organization_id_idx" ON "Organization"("id"); + +-- AddForeignKey +ALTER TABLE "Vendor" ADD CONSTRAINT "Vendor_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VendorContact" ADD CONSTRAINT "VendorContact_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VendorContact" ADD CONSTRAINT "VendorContact_vendorId_fkey" FOREIGN KEY ("vendorId") REFERENCES "Vendor"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VendorComment" ADD CONSTRAINT "VendorComment_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VendorComment" ADD CONSTRAINT "VendorComment_vendorId_fkey" FOREIGN KEY ("vendorId") REFERENCES "Vendor"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VendorAttachment" ADD CONSTRAINT "VendorAttachment_vendorId_fkey" FOREIGN KEY ("vendorId") REFERENCES "Vendor"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VendorAttachment" ADD CONSTRAINT "VendorAttachment_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VendorTask" ADD CONSTRAINT "VendorTask_vendorId_fkey" FOREIGN KEY ("vendorId") REFERENCES "Vendor"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VendorTask" ADD CONSTRAINT "VendorTask_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VendorTaskAttachment" ADD CONSTRAINT "VendorTaskAttachment_vendorId_fkey" FOREIGN KEY ("vendorId") REFERENCES "Vendor"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VendorTaskAttachment" ADD CONSTRAINT "VendorTaskAttachment_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VendorTaskComment" ADD CONSTRAINT "VendorTaskComment_vendorId_fkey" FOREIGN KEY ("vendorId") REFERENCES "Vendor"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VendorTaskComment" ADD CONSTRAINT "VendorTaskComment_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VendorTaskAssignment" ADD CONSTRAINT "VendorTaskAssignment_vendorId_fkey" FOREIGN KEY ("vendorId") REFERENCES "Vendor"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VendorTaskAssignment" ADD CONSTRAINT "VendorTaskAssignment_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/packages/db/prisma/migrations/20250306181056_vendors/migration.sql b/packages/db/prisma/migrations/20250306181056_vendors/migration.sql new file mode 100644 index 0000000000..de9f1fca14 --- /dev/null +++ b/packages/db/prisma/migrations/20250306181056_vendors/migration.sql @@ -0,0 +1,20 @@ +-- AlterTable +ALTER TABLE "Vendor" ADD COLUMN "category" "VendorCategory" NOT NULL DEFAULT 'other', +ADD COLUMN "inherentRisk" "VendorInherentRisk" NOT NULL DEFAULT 'unknown', +ADD COLUMN "residualRisk" "VendorResidualRisk" NOT NULL DEFAULT 'unknown', +ADD COLUMN "status" "VendorStatus" NOT NULL DEFAULT 'not_assessed'; + +-- CreateIndex +CREATE INDEX "Vendor_organizationId_idx" ON "Vendor"("organizationId"); + +-- CreateIndex +CREATE INDEX "Vendor_category_idx" ON "Vendor"("category"); + +-- CreateIndex +CREATE INDEX "Vendor_status_idx" ON "Vendor"("status"); + +-- CreateIndex +CREATE INDEX "Vendor_inherentRisk_idx" ON "Vendor"("inherentRisk"); + +-- CreateIndex +CREATE INDEX "Vendor_residualRisk_idx" ON "Vendor"("residualRisk"); diff --git a/packages/db/prisma/schema/schema.prisma b/packages/db/prisma/schema/schema.prisma index 4bb6f4fef2..898e0f7e96 100644 --- a/packages/db/prisma/schema/schema.prisma +++ b/packages/db/prisma/schema/schema.prisma @@ -103,8 +103,17 @@ model Organization { PortalUser PortalUser[] apiKeys OrganizationApiKey[] OrganizationIntegrationResults OrganizationIntegrationResults[] + vendors Vendor[] + VendorContact VendorContact[] + VendorComment VendorComment[] + VendorAttachment VendorAttachment[] + VendorTask VendorTask[] + VendorTaskAttachment VendorTaskAttachment[] + VendorTaskComment VendorTaskComment[] + VendorTaskAssignment VendorTaskAssignment[] @@index([stripeCustomerId]) + @@index([id]) } model VerificationToken { @@ -275,37 +284,6 @@ enum RiskAttachmentType { url } -enum VendorCategory { - cloud - infrastructure - software_as_a_service - finance - marketing - sales - hr - other -} - -enum VendorStatus { - not_assessed - in_progress - assessed -} - -enum VendorInherentRisk { - low - medium - high - unknown -} - -enum VendorResidualRisk { - low - medium - high - unknown -} - enum MembershipRole { owner admin diff --git a/packages/db/prisma/schema/vendor.prisma b/packages/db/prisma/schema/vendor.prisma index 8b13789179..69fcaf8c73 100644 --- a/packages/db/prisma/schema/vendor.prisma +++ b/packages/db/prisma/schema/vendor.prisma @@ -1 +1,138 @@ +model Vendor { + id String @id @default(cuid()) + name String + description String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + organizationId String + organization Organization @relation(fields: [organizationId], references: [id]) + contacts VendorContact[] + comments VendorComment[] + attachments VendorAttachment[] + tasks VendorTask[] + taskAttachments VendorTaskAttachment[] + taskComments VendorTaskComment[] + taskAssignments VendorTaskAssignment[] + category VendorCategory @default(other) + status VendorStatus @default(not_assessed) + inherentRisk VendorInherentRisk @default(unknown) + residualRisk VendorResidualRisk @default(unknown) + @@index([organizationId]) + @@index([category]) + @@index([status]) + @@index([inherentRisk]) + @@index([residualRisk]) +} + +model VendorContact { + id String @id @default(cuid()) + vendorId String + organizationId String + name String + email String + phone String + organization Organization @relation(fields: [organizationId], references: [id]) + Vendor Vendor @relation(fields: [vendorId], references: [id]) + + @@index([organizationId]) + @@index([vendorId]) +} + +model VendorComment { + id String @id @default(cuid()) + vendorId String + organizationId String + content String + organization Organization @relation(fields: [organizationId], references: [id]) + Vendor Vendor @relation(fields: [vendorId], references: [id]) + + @@index([organizationId]) + @@index([vendorId]) +} + +model VendorAttachment { + id String @id @default(cuid()) + vendorId String + organizationId String + Vendor Vendor? @relation(fields: [vendorId], references: [id]) + Organization Organization @relation(fields: [organizationId], references: [id]) + + @@index([organizationId]) + @@index([vendorId]) +} + +model VendorTask { + id String @id @default(cuid()) + vendorId String + organizationId String + Vendor Vendor? @relation(fields: [vendorId], references: [id]) + Organization Organization @relation(fields: [organizationId], references: [id]) + + @@index([organizationId]) + @@index([vendorId]) +} + +model VendorTaskAttachment { + id String @id @default(cuid()) + vendorId String + organizationId String + Vendor Vendor? @relation(fields: [vendorId], references: [id]) + Organization Organization @relation(fields: [organizationId], references: [id]) + + @@index([organizationId]) + @@index([vendorId]) +} + +model VendorTaskComment { + id String @id @default(cuid()) + vendorId String + organizationId String + Vendor Vendor? @relation(fields: [vendorId], references: [id]) + Organization Organization @relation(fields: [organizationId], references: [id]) + + @@index([organizationId]) + @@index([vendorId]) +} + +model VendorTaskAssignment { + id String @id @default(cuid()) + vendorId String + organizationId String + Vendor Vendor? @relation(fields: [vendorId], references: [id]) + Organization Organization @relation(fields: [organizationId], references: [id]) + + @@index([organizationId]) + @@index([vendorId]) +} + +enum VendorCategory { + cloud + infrastructure + software_as_a_service + finance + marketing + sales + hr + other +} + +enum VendorStatus { + not_assessed + in_progress + assessed +} + +enum VendorInherentRisk { + low + medium + high + unknown +} + +enum VendorResidualRisk { + low + medium + high + unknown +} diff --git a/packages/integrations/src/aws/config.ts b/packages/integrations/src/aws/config.ts index c2dee8c59e..b3d153cff2 100644 --- a/packages/integrations/src/aws/config.ts +++ b/packages/integrations/src/aws/config.ts @@ -2,41 +2,40 @@ import image from "./assets/image.png"; import { Logo } from "./assets/logo"; export default { - name: "AWS", - id: "aws", - active: true, - logo: Logo, - short_description: - "Connect your AWS account to Comp AI to automate evidence collection for cloud resources", - description: - "Integrating with AWS allows you to automate evidence collection. This compliance analysis tool enables organizations to more quickly articulate their compliance posture and also generate supporting evidence artifacts", - images: [image], - settings: [ - { - id: "access_key_id", - label: "AWS access key ID", - description: "The API access key ID for your AWS account", - type: "text", - required: true, - value: "", - }, - { - id: "secret_access_key", - label: "AWS secret access key", - description: "The API secret access key for your AWS account", - type: "text", - required: true, - value: "", - }, - { - id: "session_token", - label: "AWS session token", - description: "The API session token AWS account", - type: "text", - required: true, - value: "", - }, - ], - config: {}, - category: "Cloud" + name: "AWS", + id: "aws", + active: true, + logo: Logo, + short_description: + "Connect your AWS account to Comp AI to automate evidence collection for cloud resources", + description: + "Integrating with AWS allows you to automate evidence collection. This compliance analysis tool enables organizations to more quickly articulate their compliance posture and also generate supporting evidence artifacts", + images: [image], + settings: [ + { + id: "access_key_id", + label: "AWS access key ID", + description: "The API access key ID for your AWS account", + type: "text", + required: true, + value: "", + }, + { + id: "secret_access_key", + label: "AWS secret access key", + description: "The API secret access key for your AWS account", + type: "text", + required: true, + value: "", + }, + { + id: "session_token", + label: "AWS session token", + description: "The API session token AWS account", + type: "text", + required: true, + value: "", + }, + ], + category: "Cloud", }; From 05a1dc56843ac4530b75872754e1c79074bd0985 Mon Sep 17 00:00:00 2001 From: Languine Bot Date: Thu, 6 Mar 2025 19:08:40 +0000 Subject: [PATCH 2/2] chore: (i18n) update translations using Languine.ai --- apps/app/languine.lock | 61 ++++++-------------------------------- apps/app/src/locales/es.ts | 19 ++++++++++-- apps/app/src/locales/fr.ts | 19 ++++++++++-- apps/app/src/locales/no.ts | 19 ++++++++++-- apps/app/src/locales/pt.ts | 19 ++++++++++-- 5 files changed, 77 insertions(+), 60 deletions(-) diff --git a/apps/app/languine.lock b/apps/app/languine.lock index 07998f1393..ec5411ce0e 100644 --- a/apps/app/languine.lock +++ b/apps/app/languine.lock @@ -451,8 +451,8 @@ files: settings.api_keys.description: a73bc7101920a4a2a821a6640ae95fc5 settings.api_keys.list_title: b4717f6ab3c2357c13c0c2cd32dfc4ca settings.api_keys.list_description: 43aeedd6a4d60ad7a7c81cd81386e63e - settings.api_keys.create: 35cd846f199e312c7fc78d231c4152c5 - settings.api_keys.create_title: 35cd846f199e312c7fc78d231c4152c5 + settings.api_keys.create: 17dcee9df9da51b6808235edb471700a + settings.api_keys.create_title: 17dcee9df9da51b6808235edb471700a settings.api_keys.create_description: b4e48d0aee029d3a32fb5ca5e2e82a3a settings.api_keys.created_title: 46906edf876a3f62534fe9dd55b42e80 settings.api_keys.created_description: 445e205d7cc8d54e322b0b1270060b31 @@ -560,56 +560,6 @@ files: frameworks.controls.statuses.completed: 07ca5050e697392c9ed47e6453f1453f frameworks.controls.statuses.in_progress: c9413dd7682a45da9ecc9efa8cea47d7 frameworks.controls.statuses.not_started: 10db56f8b10dc989c43fbe33202e5339 - vendor.title: 2938c7f7e560ed972f8a4f68e80ff834 - vendor.register_title: 97f6291ea1000272b34fdfaeb3a9e2d2 - vendor.dashboard.title: 2938c7f7e560ed972f8a4f68e80ff834 - vendor.dashboard.overview: 003dd1a6f6efb2ac33fa8519c89dedcc - vendor.dashboard.vendor_status: 17029fbe5ad4d2af5feaf37bb4604225 - vendor.dashboard.vendor_category: 273bcc0a45ba3b2256086f10966dfb99 - vendor.dashboard.vendors_by_assignee: efbb5ed45de13ae274678ea91b656d08 - vendor.dashboard.inherent_risk_description: f65c38062d37923bfd4850e78e25772a - vendor.dashboard.residual_risk_description: 7e1b11b661aa2704e378dd1ca7f33757 - vendor.register.title: 122b14e8fa33bc9d4f389700439dfc71 - vendor.register.table.name: 49ee3087348e8d44e1feda1917443987 - vendor.register.table.category: 3adbdb3ac060038aa0e6e6c138ef9873 - vendor.register.table.status: ec53a8c4f07baed5d8825072c89799be - vendor.register.table.owner: b6f4a2ec6356bbd56d49f2096bf9d3d3 - vendor.category.cloud: c8e2277ecfb1427838c488c217f99466 - vendor.category.infrastructure: cba4f4cee5c6a3e6d8be8da68fa1234e - vendor.category.software_as_a_service: b0ca9e41d61885345eef09f928c9ea58 - vendor.category.finance: c482980d384a9d0e7bc39e1140270870 - vendor.category.marketing: 7cb15e416d62919b1b40298324fbe30b - vendor.category.sales: 11ff9f68afb6b8b5b8eda218d7c83a65 - vendor.category.hr: fd4c638da5f85d025963f99fe90b1b1a - vendor.category.other: 6311ae17c1ee52b36e68aaf4ad066387 - vendor.vendors: 2bc28e228525b75e14c07dbc14850c34 - vendor.form.vendor_details: c594485dedf99a5bbae0ce0b1e08c57a - vendor.form.vendor_name: 49ee3087348e8d44e1feda1917443987 - vendor.form.vendor_name_placeholder: 76d22a64aa21a6f1fe356624bb42487e - vendor.form.vendor_website: 15bbb9d0bbf25e8d2978de1168c749dc - vendor.form.vendor_website_placeholder: 0edbf3ae0d36cb35fdae8df56396e8f0 - vendor.form.vendor_description: b5a7adde1af5c87d7fd797b6245c2a39 - vendor.form.vendor_description_placeholder: edd59fcb97919e8a914f29d1b8c94a84 - vendor.form.vendor_category: 3adbdb3ac060038aa0e6e6c138ef9873 - vendor.form.vendor_category_placeholder: 03368e3c1eb4d2a9048775874301b19f - vendor.form.vendor_status: ec53a8c4f07baed5d8825072c89799be - vendor.form.vendor_status_placeholder: 9aadb017cc5b335be77ba090d5029259 - vendor.form.create_vendor_success: 86783216c2055e4b01d9a6d920e418c7 - vendor.form.create_vendor_error: ca4cedd93fdb023d6f9748dd3b66b456 - vendor.form.update_vendor_success: eaedfa5b3ca431e1ca8900a8f0d5c924 - vendor.form.update_vendor_error: 92b965a71c44a134e2eae6d71493e4d9 - vendor.form.contacts: be184fa710244f6c783e6eb68fd427d7 - vendor.form.contact_name: 7cfd883d5d55fccfcfb13b3fcf29e3a6 - vendor.form.contact_email: 3c2292f30e636ecf522743c3fa73749d - vendor.form.contact_role: ccde9b292717d5a0133304d27acfa669 - vendor.form.add_contact: 4e58c159d031b956f9ee227f87df3fcf - vendor.form.new_contact: 636c575a94e9a0c957277e432fb1d49b - vendor.form.min_one_contact_required: 92b341dc8c72591d66252794dfb00f48 - vendor.empty_states.no_vendors.title: 69c843f9cf71049dacacff0c48edb7b2 - vendor.empty_states.no_vendors.description: 955707a384c1cfc46fafbdebc508f9e0 - vendor.empty_states.no_results.title: e576c23d915755d83e2d1f47bd9f6c22 - vendor.empty_states.no_results.description: d7fd5c52726c5016a59fb01e74069441 - vendor.empty_states.no_results.description_with_filters: 2b70f835ed5a08e817ec9388ce25b41a errors.unexpected: 325a357f60b95f37fec18b15cb39fbb0 editor.ai.thinking: aba464039b337eb2e75c2520039de58f editor.ai.thinking_spinner: aba464039b337eb2e75c2520039de58f @@ -648,3 +598,10 @@ files: evidence.departments.qms: 3b88c0772d938028236e5098c096b2d5 evidence.details.review_section: 659514b42799e5982d28793d9e003ae3 evidence.details.content: 393ae95538cd395eb8d1f7521f652b10 + vendors.title: dfcc5efe778e6340336015dc02617922 + vendors.register.title: 122b14e8fa33bc9d4f389700439dfc71 + vendors.dashboard.title: 003dd1a6f6efb2ac33fa8519c89dedcc + dashboard.risk_status: 6ce5e6fa832289748023cb773a4e89ac + dashboard.risks_by_department: 0ef508d7e840bc5f85547c3b25e5d87b + dashboard.vendor_status: 17029fbe5ad4d2af5feaf37bb4604225 + dashboard.vendors_by_category: cac07152524b0e5195fbf87c36646b2b diff --git a/apps/app/src/locales/es.ts b/apps/app/src/locales/es.ts index c5fbcdbacc..d78de610eb 100644 --- a/apps/app/src/locales/es.ts +++ b/apps/app/src/locales/es.ts @@ -580,8 +580,8 @@ export default { description: "Administra las claves API para el acceso programático a los datos de tu organización.", list_title: "Claves API", list_description: "Las claves API permiten el acceso seguro a los datos de tu organización a través de nuestra API.", - create: "Crear clave API", - create_title: "Crear clave API", + create: "Nueva clave de API", + create_title: "Nueva clave de API", create_description: "Crea una nueva clave API para el acceso programático a los datos de tu organización.", created_title: "Clave API creada", created_description: "Tu clave API ha sido creada. Asegúrate de copiarla ahora, ya que no podrás volver a verla.", @@ -990,5 +990,20 @@ export default { no_results: "No se encontraron resultados", status: "Estado" } + }, + vendors: { + title: "Proveedores", + register: { + title: "Registro de proveedores" + }, + dashboard: { + title: "Resumen de proveedores" + } + }, + dashboard: { + risk_status: "Estado de riesgo", + risks_by_department: "Riesgos por departamento", + vendor_status: "Estado del proveedor", + vendors_by_category: "Proveedores por categoría" } } as const; diff --git a/apps/app/src/locales/fr.ts b/apps/app/src/locales/fr.ts index 438fe9a6cc..afdd85f673 100644 --- a/apps/app/src/locales/fr.ts +++ b/apps/app/src/locales/fr.ts @@ -580,8 +580,8 @@ export default { description: "Gérez les clés API pour un accès programmatique aux données de votre organisation.", list_title: "Clés API", list_description: "Les clés API permettent un accès sécurisé aux données de votre organisation via notre API.", - create: "Créer une clé API", - create_title: "Créer une clé API", + create: "Nouvelle clé API", + create_title: "Nouvelle clé API", create_description: "Créez une nouvelle clé API pour un accès programmatique aux données de votre organisation.", created_title: "Clé API créée", created_description: "Votre clé API a été créée. Assurez-vous de la copier maintenant car vous ne pourrez plus la voir.", @@ -990,5 +990,20 @@ export default { no_results: "Aucun résultat trouvé", status: "Statut" } + }, + vendors: { + title: "Fournisseurs", + register: { + title: "Enregistrement des fournisseurs" + }, + dashboard: { + title: "Aperçu des fournisseurs" + } + }, + dashboard: { + risk_status: "Statut de risque", + risks_by_department: "Risques par département", + vendor_status: "Statut des fournisseurs", + vendors_by_category: "Fournisseurs par catégorie" } } as const; diff --git a/apps/app/src/locales/no.ts b/apps/app/src/locales/no.ts index e169a19ac5..5a4f1bf4a0 100644 --- a/apps/app/src/locales/no.ts +++ b/apps/app/src/locales/no.ts @@ -580,8 +580,8 @@ export default { description: "Administrer API-nøkler for programmatisk tilgang til dataene i organisasjonen din.", list_title: "API-nøkler", list_description: "API-nøkler gir sikker tilgang til dataene i organisasjonen din via vårt API.", - create: "Opprett API-nøkkel", - create_title: "Opprett API-nøkkel", + create: "Ny API-nøkkel", + create_title: "Ny API-nøkkel", create_description: "Opprett en ny API-nøkkel for programmatisk tilgang til dataene i organisasjonen din.", created_title: "API-nøkkel opprettet", created_description: "API-nøkkelen din har blitt opprettet. Sørg for å kopiere den nå, da du ikke vil kunne se den igjen.", @@ -990,5 +990,20 @@ export default { no_results: "Ingen resultater funnet", status: "Status" } + }, + vendors: { + title: "Leverandører", + register: { + title: "Leverandørregister" + }, + dashboard: { + title: "Oversikt over leverandører" + } + }, + dashboard: { + risk_status: "Risiko Status", + risks_by_department: "Risikoer etter avdeling", + vendor_status: "Leverandørstatus", + vendors_by_category: "Leverandører etter kategori" } } as const; diff --git a/apps/app/src/locales/pt.ts b/apps/app/src/locales/pt.ts index bff6d09a9c..2786d9bf64 100644 --- a/apps/app/src/locales/pt.ts +++ b/apps/app/src/locales/pt.ts @@ -580,8 +580,8 @@ export default { description: "Gerencie chaves da API para acesso programático aos dados da sua organização.", list_title: "Chaves da API", list_description: "As chaves da API permitem acesso seguro aos dados da sua organização através da nossa API.", - create: "Criar Chave da API", - create_title: "Criar Chave da API", + create: "Nova Chave da API", + create_title: "Nova Chave da API", create_description: "Crie uma nova chave da API para acesso programático aos dados da sua organização.", created_title: "Chave da API Criada", created_description: "Sua chave da API foi criada. Certifique-se de copiá-la agora, pois você não poderá vê-la novamente.", @@ -961,5 +961,20 @@ export default { no_results: "Nenhum resultado encontrado", status: "Status" } + }, + vendors: { + title: "Fornecedores", + register: { + title: "Registro de Fornecedor" + }, + dashboard: { + title: "Visão Geral do Fornecedor" + } + }, + dashboard: { + risk_status: "Status de Risco", + risks_by_department: "Riscos por Departamento", + vendor_status: "Status do Fornecedor", + vendors_by_category: "Fornecedores por Categoria" } } as const;