diff --git a/apps/app/src/actions/risk/task/update-task-action.ts b/apps/app/src/actions/risk/task/update-task-action.ts index 5521ad94ec..2cae22847a 100644 --- a/apps/app/src/actions/risk/task/update-task-action.ts +++ b/apps/app/src/actions/risk/task/update-task-action.ts @@ -9,55 +9,55 @@ import { authActionClient } from "../../safe-action"; import { updateTaskSchema } from "../../schema"; export const updateTaskAction = authActionClient - .schema(updateTaskSchema) - .metadata({ - name: "update-task", - track: { - event: "update-task", - channel: "server", - }, - }) - .action(async ({ parsedInput, ctx }) => { - const { id, dueDate, status, assigneeId, title, description } = parsedInput; - const { session } = ctx; - - if (!session.activeOrganizationId) { - throw new Error("Invalid user input"); - } - - try { - const task = await db.task.findUnique({ - where: { - id: id, - }, - }); - - if (!task) { - throw new Error("Task not found"); - } - - await db.task.update({ - where: { - id: id, - organizationId: session.activeOrganizationId, - }, - data: { - dueDate: dueDate, - status: status as TaskStatus, - assigneeId: assigneeId, - title: title, - description: description, - }, - }); - - revalidatePath(`/${session.activeOrganizationId}/risk`); - revalidatePath(`/${session.activeOrganizationId}/risk/${id}`); - revalidatePath(`/${session.activeOrganizationId}/risk/${id}/tasks/${id}`); - revalidateTag("risks"); - - return { success: true }; - } catch (error) { - console.error(error); - return { success: false }; - } - }); + .schema(updateTaskSchema) + .metadata({ + name: "update-task", + track: { + event: "update-task", + channel: "server", + }, + }) + .action(async ({ parsedInput, ctx }) => { + const { id, dueDate, status, assigneeId, title, description } = parsedInput; + const { session } = ctx; + + if (!session.activeOrganizationId) { + throw new Error("Invalid user input"); + } + + try { + const task = await db.task.findUnique({ + where: { + id: id, + }, + }); + + if (!task) { + throw new Error("Task not found"); + } + + await db.task.update({ + where: { + id: id, + organizationId: session.activeOrganizationId, + }, + data: { + dueDate: dueDate, + status: status as TaskStatus, + assigneeId, + title: title, + description: description, + }, + }); + + revalidatePath(`/${session.activeOrganizationId}/risk`); + revalidatePath(`/${session.activeOrganizationId}/risk/${id}`); + revalidatePath(`/${session.activeOrganizationId}/risk/${id}/tasks/${id}`); + revalidateTag("risks"); + + return { success: true }; + } catch (error) { + console.error(error); + return { success: false }; + } + }); diff --git a/apps/app/src/actions/schema.ts b/apps/app/src/actions/schema.ts index 5a32495d8c..44748a37da 100644 --- a/apps/app/src/actions/schema.ts +++ b/apps/app/src/actions/schema.ts @@ -1,292 +1,284 @@ import { - Departments, - FrameworkId, - Frequency, - Impact, - Likelihood, - PolicyStatus, - RiskCategory, - RiskStatus, - TaskStatus, + Departments, + FrameworkId, + Frequency, + Impact, + Likelihood, + PolicyStatus, + RiskCategory, + RiskStatus, + TaskStatus, } from "@comp/db/types"; import { z } from "zod"; export const organizationSchema = z.object({ - name: z.string().min(1, "Organization name is required"), - frameworks: z - .array(z.nativeEnum(FrameworkId)) - .min(1, "Please select at least one framework to get started with"), + name: z.string().min(1, "Organization name is required"), + frameworks: z + .array(z.nativeEnum(FrameworkId)) + .min(1, "Please select at least one framework to get started with"), }); export type OrganizationSchema = z.infer; export const organizationNameSchema = z.object({ - name: z - .string() - .min(1, "Organization name is required") - .max(255, "Organization name cannot exceed 255 characters"), + name: z + .string() + .min(1, "Organization name is required") + .max(255, "Organization name cannot exceed 255 characters"), }); export const subdomainAvailabilitySchema = z.object({ - subdomain: z - .string() - .min(1, "Subdomain is required") - .max(255, "Subdomain cannot exceed 255 characters") - .regex(/^[a-z0-9-]+$/, { - message: - "Subdomain can only contain lowercase letters, numbers, and hyphens", - }), + subdomain: z + .string() + .min(1, "Subdomain is required") + .max(255, "Subdomain cannot exceed 255 characters") + .regex(/^[a-z0-9-]+$/, { + message: + "Subdomain can only contain lowercase letters, numbers, and hyphens", + }), }); export const uploadSchema = z.object({ - file: z.instanceof(File), - organizationId: z.string(), + file: z.instanceof(File), + organizationId: z.string(), }); export const deleteOrganizationSchema = z.object({ - id: z.string(), - organizationId: z.string(), + id: z.string(), + organizationId: z.string(), }); export const sendFeedbackSchema = z.object({ - feedback: z.string(), + feedback: z.string(), }); export const updaterMenuSchema = z.array( - z.object({ - path: z.string(), - name: z.string(), - }), + z.object({ + path: z.string(), + name: z.string(), + }) ); export const organizationWebsiteSchema = z.object({ - website: z.string().url().max(255), + website: z.string().url().max(255), }); // Risks export const createRiskSchema = z.object({ - title: z - .string({ - required_error: "Risk name is required", - }) - .min(1, { - message: "Risk name should be at least 1 character", - }) - .max(100, { - message: "Risk name should be at most 100 characters", - }), - description: z - .string({ - required_error: "Risk description is required", - }) - .min(1, { - message: "Risk description should be at least 1 character", - }) - .max(255, { - message: "Risk description should be at most 255 characters", - }), - category: z.nativeEnum(RiskCategory, { - required_error: "Risk category is required", - }), - department: z.nativeEnum(Departments, { - required_error: "Risk department is required", - }), - assigneeId: z.string().nullable(), + title: z + .string({ + required_error: "Risk name is required", + }) + .min(1, { + message: "Risk name should be at least 1 character", + }) + .max(100, { + message: "Risk name should be at most 100 characters", + }), + description: z + .string({ + required_error: "Risk description is required", + }) + .min(1, { + message: "Risk description should be at least 1 character", + }) + .max(255, { + message: "Risk description should be at most 255 characters", + }), + category: z.nativeEnum(RiskCategory, { + required_error: "Risk category is required", + }), + department: z.nativeEnum(Departments, { + required_error: "Risk department is required", + }), + assigneeId: z.string().optional().nullable(), }); export const updateRiskSchema = z.object({ - id: z.string().min(1, { - message: "Risk ID is required", - }), - title: z.string().min(1, { - message: "Risk title is required", - }), - description: z.string().min(1, { - message: "Risk description is required", - }), - category: z.nativeEnum(RiskCategory, { - required_error: "Risk category is required", - }), - department: z.nativeEnum(Departments, { - required_error: "Risk department is required", - }), - assigneeId: z.string().nullable(), - status: z.nativeEnum(RiskStatus, { - required_error: "Risk status is required", - }), + id: z.string().min(1, { + message: "Risk ID is required", + }), + title: z.string().min(1, { + message: "Risk title is required", + }), + description: z.string().min(1, { + message: "Risk description is required", + }), + category: z.nativeEnum(RiskCategory, { + required_error: "Risk category is required", + }), + department: z.nativeEnum(Departments, { + required_error: "Risk department is required", + }), + assigneeId: z.string().optional().nullable(), + status: z.nativeEnum(RiskStatus, { + required_error: "Risk status is required", + }), }); export const createRiskCommentSchema = z.object({ - riskId: z.string().min(1, { - message: "Risk ID is required", - }), - content: z - .string() - .min(1, { - message: "Comment content is required", - }) - .max(1000, { - message: "Comment content should be at most 1000 characters", - }), + riskId: z.string().min(1, { + message: "Risk ID is required", + }), + content: z + .string() + .min(1, { + message: "Comment content is required", + }) + .max(1000, { + message: "Comment content should be at most 1000 characters", + }), }); export const createTaskSchema = z.object({ - riskId: z.string().min(1, { - message: "Risk ID is required", - }), - title: z.string().min(1, { - message: "Task title is required", - }), - description: z.string().min(1, { - message: "Task description is required", - }), - dueDate: z.date().optional(), - assigneeId: z - .string({ - required_error: "You must assign an owner to the task", - }) - .min(1, { - message: "You must assign an owner to the task", - }), + riskId: z.string().min(1, { + message: "Risk ID is required", + }), + title: z.string().min(1, { + message: "Task title is required", + }), + description: z.string().min(1, { + message: "Task description is required", + }), + dueDate: z.date().optional(), + assigneeId: z.string().optional().nullable(), }); export const updateTaskSchema = z.object({ - id: z.string().min(1, { - message: "Task ID is required", - }), - title: z.string().optional(), - description: z.string().optional(), - dueDate: z.date().optional(), - status: z.nativeEnum(TaskStatus, { - required_error: "Task status is required", - }), - assigneeId: z.string({ - required_error: "You must assign an owner to the task", - }), + id: z.string().min(1, { + message: "Task ID is required", + }), + title: z.string().optional(), + description: z.string().optional(), + dueDate: z.date().optional(), + status: z.nativeEnum(TaskStatus, { + required_error: "Task status is required", + }), + assigneeId: z.string().optional().nullable(), }); export const createTaskCommentSchema = z.object({ - riskId: z.string().min(1, { - message: "Risk ID is required", - }), - taskId: z.string().min(1, { - message: "Task ID is required", - }), - content: z - .string() - .min(1, { - message: "Comment content is required", - }) - .max(1000, { - message: "Comment content should be at most 1000 characters", - }), + riskId: z.string().min(1, { + message: "Risk ID is required", + }), + taskId: z.string().min(1, { + message: "Task ID is required", + }), + content: z + .string() + .min(1, { + message: "Comment content is required", + }) + .max(1000, { + message: "Comment content should be at most 1000 characters", + }), }); export const uploadTaskFileSchema = z.object({ - riskId: z.string().min(1, { - message: "Risk ID is required", - }), - taskId: z.string().min(1, { - message: "Task ID is required", - }), + riskId: z.string().min(1, { + message: "Risk ID is required", + }), + taskId: z.string().min(1, { + message: "Task ID is required", + }), }); // Integrations export const deleteIntegrationConnectionSchema = z.object({ - integrationId: z.string().min(1, { - message: "Integration ID is required", - }), + integrationId: z.string().min(1, { + message: "Integration ID is required", + }), }); export const createIntegrationSchema = z.object({ - integrationId: z.string().min(1, { - message: "Integration ID is required", - }), + integrationId: z.string().min(1, { + message: "Integration ID is required", + }), }); // Seed Data export const seedDataSchema = z.object({ - organizationId: z.string(), + organizationId: z.string(), }); export const updateInherentRiskSchema = z.object({ - id: z.string().min(1, { - message: "Risk ID is required", - }), - probability: z.nativeEnum(Likelihood), - impact: z.nativeEnum(Impact), + id: z.string().min(1, { + message: "Risk ID is required", + }), + probability: z.nativeEnum(Likelihood), + impact: z.nativeEnum(Impact), }); export const updateResidualRiskSchema = z.object({ - id: z.string().min(1, { - message: "Risk ID is required", - }), - probability: z.number().min(1).max(10), - impact: z.number().min(1).max(10), + id: z.string().min(1, { + message: "Risk ID is required", + }), + probability: z.number().min(1).max(10), + impact: z.number().min(1).max(10), }); // Policies export const createPolicySchema = z.object({ - title: z.string(), - description: z.string(), - frameworkIds: z.array(z.string()), - controlIds: z.array(z.string()), + title: z.string(), + description: z.string(), + frameworkIds: z.array(z.string()), + controlIds: z.array(z.string()), }); export const updatePolicySchema = z.object({ - id: z.string(), - content: z.any(), + id: z.string(), + content: z.any(), }); export const assistantSettingsSchema = z.object({ - enabled: z.boolean().optional(), + enabled: z.boolean().optional(), }); export const createEmployeeSchema = z.object({ - name: z.string().min(1, "Name is required"), - email: z.string().email("Invalid email address"), - department: z.nativeEnum(Departments, { - required_error: "Department is required", - }), - externalEmployeeId: z.string().optional(), - isActive: z.boolean().default(true), + name: z.string().min(1, "Name is required"), + email: z.string().email("Invalid email address"), + department: z.nativeEnum(Departments, { + required_error: "Department is required", + }), + externalEmployeeId: z.string().optional(), + isActive: z.boolean().default(true), }); export const updatePolicyOverviewSchema = z.object({ - id: z.string(), - title: z.string(), - description: z.string(), - isRequiredToSign: z.enum(["required", "not_required"]).optional(), + id: z.string(), + title: z.string(), + description: z.string(), + isRequiredToSign: z.enum(["required", "not_required"]).optional(), }); export const updatePolicyFormSchema = z.object({ - id: z.string(), - status: z.nativeEnum(PolicyStatus), - assigneeId: z.string().nullable(), - department: z.nativeEnum(Departments), - review_frequency: z.nativeEnum(Frequency), - review_date: z.date(), - isRequiredToSign: z.enum(["required", "not_required"]), + id: z.string(), + status: z.nativeEnum(PolicyStatus), + assigneeId: z.string().optional().nullable(), + department: z.nativeEnum(Departments), + review_frequency: z.nativeEnum(Frequency), + review_date: z.date(), + isRequiredToSign: z.enum(["required", "not_required"]), }); export const apiKeySchema = z.object({ - name: z - .string() - .min(1, { message: "Name is required" }) - .max(64, { message: "Name must be less than 64 characters" }), - expiresAt: z.enum(["30days", "90days", "1year", "never"]), + name: z + .string() + .min(1, { message: "Name is required" }) + .max(64, { message: "Name must be less than 64 characters" }), + expiresAt: z.enum(["30days", "90days", "1year", "never"]), }); export const createPolicyCommentSchema = z.object({ - policyId: z.string().min(1, { - message: "Policy ID is required", - }), - content: z - .string() - .min(1, { - message: "Comment content is required", - }) - .max(1000, { - message: "Comment content should be at most 1000 characters", - }), + policyId: z.string().min(1, { + message: "Policy ID is required", + }), + content: z + .string() + .min(1, { + message: "Comment content is required", + }) + .max(1000, { + message: "Comment content should be at most 1000 characters", + }), }); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/components/SelectAssignee.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/components/SelectAssignee.tsx index 9529d3d6c9..5530f108c7 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/components/SelectAssignee.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/components/SelectAssignee.tsx @@ -8,6 +8,7 @@ import { import { UserIcon } from "lucide-react"; import { Member, User } from "@comp/db/types"; import { useState, useEffect } from "react"; +import { db } from "@comp/db"; interface SelectAssigneeProps { assigneeId: string | null; diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/all/types/search-params.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/all/types/search-params.ts index 3f1579e77b..b51788dfc6 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/all/types/search-params.ts +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/employees/all/types/search-params.ts @@ -11,5 +11,5 @@ export const searchParamsCache = createSearchParamsCache({ end: parseAsString, status: parseAsString, department: parseAsString, - ownerId: parseAsString, + assigneeId: parseAsString, }); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/page.tsx index 6a9a3d337a..f3bb21065c 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/page.tsx @@ -9,9 +9,6 @@ export default async function DashboardPage() { const organizationId = session?.session.activeOrganizationId; - console.log("session", session); - console.log("organizationId", organizationId); - if (!organizationId) { redirect("/"); } diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/layout.tsx index 45a2d74ee3..34fff6454a 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/layout.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/layout.tsx @@ -97,6 +97,9 @@ const getAssignees = cache(async () => { where: { organizationId: activeOrganizationId, isActive: true, + role: { + notIn: ["employee"], + }, }, include: { user: true, 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 bd83caeb93..49a17b855e 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 @@ -13,221 +13,224 @@ import { headers } from "next/headers"; 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 }>; + 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 { riskId } = await params; - const risk = await getRisk(riskId); - - const assignees = await getAssignees(); - const t = await getI18n(); - - 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 { tasks: loadedTasks, total } = await getTasks({ - riskId, - search, - status: status as TaskStatus, - column, - order, - page: Number.parseInt(page), - per_page: Number.parseInt(per_page), - }); - - if (!risk) { - redirect("/"); - } - - return ( -
- - -
- - -
-
- ); + const { riskId } = await params; + const risk = await getRisk(riskId); + + const assignees = await getAssignees(); + const t = await getI18n(); + + 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 { tasks: loadedTasks, total } = await getTasks({ + riskId, + search, + status: status as TaskStatus, + column, + order, + page: Number.parseInt(page), + per_page: Number.parseInt(per_page), + }); + + if (!risk) { + redirect("/"); + } + + return ( +
+ + +
+ + +
+
+ ); } async function getRisk(riskId: string) { - const session = await auth.api.getSession({ - headers: await headers(), - }); - - if (!session || !session.session.activeOrganizationId) { - return null; - } - - const risk = await db.risk.findUnique({ - where: { - id: riskId, - organizationId: session.session.activeOrganizationId, - }, - include: { - assignee: { - include: { - user: true, - }, - }, - }, - }); - - return risk; + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session || !session.session.activeOrganizationId) { + return null; + } + + const risk = await db.risk.findUnique({ + where: { + id: riskId, + organizationId: session.session.activeOrganizationId, + }, + include: { + assignee: { + include: { + user: true, + }, + }, + }, + }); + + return risk; } async function getTasks({ - riskId, - search, - status, - column, - order, - page = 1, - per_page = 10, + riskId, + search, + status, + column, + order, + page = 1, + per_page = 10, }: { - riskId: string; - search?: string; - status?: TaskStatus; - column?: string; - order?: string; - page?: number; - per_page?: number; + riskId: string; + search?: string; + status?: TaskStatus; + column?: string; + order?: string; + page?: number; + per_page?: number; }) { - const session = await auth.api.getSession({ - headers: await headers(), - }); - - if (!session || !session.session.activeOrganizationId) { - return { tasks: [], total: 0 }; - } - - const skip = (page - 1) * per_page; - - const [tasks, total] = await Promise.all([ - db.task - .findMany({ - where: { - relatedId: riskId, - relatedType: "risk", - organizationId: session.session.activeOrganizationId, - 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: { - assignee: { - include: { - user: true, - }, - }, - }, - }) - .then((tasks) => - tasks.map( - (task): RiskTaskType => ({ - id: task.id, - riskId: task.relatedId, - title: task.title, - status: task.status, - dueDate: task.dueDate.toISOString(), - assigneeId: task.assigneeId, - assignee: { - name: task.assignee.user.name, - image: task.assignee.user.image ?? "", - }, - }), - ), - ), - db.task.count({ - where: { - relatedId: riskId, - relatedType: "risk", - organizationId: session.session.activeOrganizationId, - AND: [ - search - ? { - OR: [ - { title: { contains: search, mode: "insensitive" } }, - { description: { contains: search, mode: "insensitive" } }, - ], - } - : {}, - status ? { status } : {}, - ], - }, - }), - ]); - - return { tasks, total }; + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session || !session.session.activeOrganizationId) { + return { tasks: [], total: 0 }; + } + + const skip = (page - 1) * per_page; + + const [tasks, total] = await Promise.all([ + db.task + .findMany({ + where: { + relatedId: riskId, + relatedType: "risk", + organizationId: session.session.activeOrganizationId, + 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: { + assignee: { + include: { + user: true, + }, + }, + }, + }) + .then((tasks) => + tasks.map( + (task): RiskTaskType => ({ + id: task.id, + riskId: task.relatedId, + title: task.title, + status: task.status, + dueDate: task.dueDate.toISOString(), + assigneeId: task.assigneeId ?? "", + assignee: { + name: task.assignee?.user.name ?? "", + image: task.assignee?.user.image ?? "", + }, + }), + ), + ), + db.task.count({ + where: { + relatedId: riskId, + relatedType: "risk", + organizationId: session.session.activeOrganizationId, + AND: [ + search + ? { + OR: [ + { title: { contains: search, mode: "insensitive" } }, + { description: { contains: search, mode: "insensitive" } }, + ], + } + : {}, + status ? { status } : {}, + ], + }, + }), + ]); + + return { tasks, total }; } async function getAssignees() { - const session = await auth.api.getSession({ - headers: await headers(), - }); - - if (!session || !session.session.activeOrganizationId) { - return []; - } - - const assignees = await db.member.findMany({ - where: { - organizationId: session.session.activeOrganizationId, - }, - include: { - user: true, - }, - }); - - return assignees; + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session || !session.session.activeOrganizationId) { + return []; + } + + const assignees = await db.member.findMany({ + where: { + organizationId: session.session.activeOrganizationId, + role: { + notIn: ["employee"], + }, + }, + include: { + user: true, + }, + }); + + return assignees; } export async function generateMetadata({ - params, + params, }: { - params: Promise<{ locale: string }>; + params: Promise<{ locale: string }>; }): Promise { - const { locale } = await params; - setStaticParamsLocale(locale); - const t = await getI18n(); + const { locale } = await params; + setStaticParamsLocale(locale); + const t = await getI18n(); - return { - title: t("risk.risk_overview"), - }; + return { + title: t("risk.risk_overview"), + }; } diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/tasks/search-params.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/tasks/search-params.ts index 55c0834e4e..1f1d026e22 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/tasks/search-params.ts +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/[riskId]/tasks/search-params.ts @@ -10,5 +10,5 @@ export const searchParamsCache = createSearchParamsCache({ start: parseAsString, end: parseAsString, status: parseAsString, - ownerId: parseAsString, + assigneeId: parseAsString, }); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/layout.tsx index f8cf8215f8..23dc53daef 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/layout.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/(overview)/layout.tsx @@ -21,8 +21,8 @@ export default async function Layout({ }); const orgId = activeOrganizationId; - const overview = await getVendorOverview(); + const assignees = await getAssignees(); if (overview?.vendors === 0) { return ( @@ -51,7 +51,7 @@ export default async function Layout({ }, ]} /> - + @@ -77,6 +77,32 @@ export default async function Layout({ ); } +const getAssignees = cache(async () => { + const { + session: { activeOrganizationId }, + } = await getServersideSession({ + headers: await headers(), + }); + + if (!activeOrganizationId) { + return []; + } + + const assignees = await db.member.findMany({ + where: { + organizationId: activeOrganizationId, + role: { + notIn: ["employee"], + }, + }, + include: { + user: true, + }, + }); + + return assignees; +}); + const getVendorOverview = cache(async () => { const { session: { activeOrganizationId }, diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/actions/task/create-task-action.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/actions/task/create-task-action.ts index 71fa7dc8b0..9bc58a6411 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/actions/task/create-task-action.ts +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/actions/task/create-task-action.ts @@ -8,51 +8,47 @@ import { authActionClient } from "@/actions/safe-action"; import { createVendorTaskSchema } from "../schema"; export const createVendorTaskAction = authActionClient - .schema(createVendorTaskSchema) - .metadata({ - name: "create-vendor-task", - track: { - event: "create-vendor-task", - channel: "server", - }, - }) - .action(async ({ parsedInput, ctx }) => { - const { vendorId, title, description, dueDate, assigneeId } = parsedInput; - const { - session: { activeOrganizationId }, - user, - } = ctx; - - if (!user.id || !activeOrganizationId) { - throw new Error("Invalid user input"); - } - - if (!assigneeId) { - throw new Error("Assignee ID is required"); - } - - try { - await db.task.create({ - data: { - title, - description, - dueDate, - assigneeId, - organizationId: activeOrganizationId, - relatedId: vendorId, - relatedType: "vendor", - }, - }); - - revalidatePath(`/${activeOrganizationId}/vendor/${vendorId}`); - revalidateTag(`vendor_${activeOrganizationId}`); - - return { - success: true, - }; - } catch (error) { - return { - success: false, - }; - } - }); + .schema(createVendorTaskSchema) + .metadata({ + name: "create-vendor-task", + track: { + event: "create-vendor-task", + channel: "server", + }, + }) + .action(async ({ parsedInput, ctx }) => { + const { vendorId, title, description, dueDate, assigneeId } = parsedInput; + const { + session: { activeOrganizationId }, + user, + } = ctx; + + if (!user.id || !activeOrganizationId) { + throw new Error("Invalid user input"); + } + + try { + await db.task.create({ + data: { + title, + description, + dueDate, + assigneeId, + organizationId: activeOrganizationId, + relatedId: vendorId, + relatedType: "vendor", + }, + }); + + revalidatePath(`/${activeOrganizationId}/vendor/${vendorId}`); + revalidateTag(`vendor_${activeOrganizationId}`); + + return { + success: true, + }; + } catch (error) { + return { + success: false, + }; + } + }); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/actions/update-vendor-action.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/actions/update-vendor-action.ts index 22a9f279f6..2f5b718415 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/actions/update-vendor-action.ts +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/actions/update-vendor-action.ts @@ -8,49 +8,50 @@ import { authActionClient } from "@/actions/safe-action"; import { updateVendorSchema } from "./schema"; export const updateVendorAction = authActionClient - .schema(updateVendorSchema) - .metadata({ - name: "update-vendor", - track: { - event: "update-vendor", - channel: "server", - }, - }) - .action(async ({ parsedInput, ctx }) => { - const { id, name, description, category, status } = parsedInput; - const { session } = ctx; - - if (!session.activeOrganizationId) { - throw new Error("Invalid user input"); - } - - try { - await db.vendor.update({ - where: { - id, - organizationId: session.activeOrganizationId, - }, - data: { - name, - description, - category, - status, - }, - }); - - revalidatePath(`/${session.activeOrganizationId}/vendors`); - revalidatePath(`/${session.activeOrganizationId}/vendors/register`); - revalidatePath(`/${session.activeOrganizationId}/vendors/${id}`); - revalidateTag("vendors"); - - return { - success: true, - }; - } catch (error) { - console.error("Error updating vendor:", error); - - return { - success: false, - }; - } - }); + .schema(updateVendorSchema) + .metadata({ + name: "update-vendor", + track: { + event: "update-vendor", + channel: "server", + }, + }) + .action(async ({ parsedInput, ctx }) => { + const { id, name, description, category, assigneeId, status } = parsedInput; + const { session } = ctx; + + if (!session.activeOrganizationId) { + throw new Error("Invalid user input"); + } + + try { + await db.vendor.update({ + where: { + id, + organizationId: session.activeOrganizationId, + }, + data: { + name, + description, + assigneeId, + category, + status, + }, + }); + + revalidatePath(`/${session.activeOrganizationId}/vendors`); + revalidatePath(`/${session.activeOrganizationId}/vendors/register`); + revalidatePath(`/${session.activeOrganizationId}/vendors/${id}`); + revalidateTag("vendors"); + + return { + success: true, + }; + } catch (error) { + console.error("Error updating vendor:", error); + + return { + success: false, + }; + } + }); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/actions/update-vendor-residual-risk.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/actions/update-vendor-residual-risk.ts index 08f0e4a05d..160e5fcc8b 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/actions/update-vendor-residual-risk.ts +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/actions/update-vendor-residual-risk.ts @@ -4,33 +4,36 @@ import { appErrors } from "@/lib/errors"; import type { ActionResponse } from "@/types/actions"; import { db } from "@comp/db"; import { createSafeActionClient } from "next-safe-action"; +import { revalidatePath } from "next/cache"; import { z } from "zod"; import { Impact, Likelihood } from "@prisma/client"; const schema = z.object({ - vendorId: z.string(), - residualProbability: z.nativeEnum(Likelihood), - residualImpact: z.nativeEnum(Impact), + vendorId: z.string(), + residualProbability: z.nativeEnum(Likelihood), + residualImpact: z.nativeEnum(Impact), }); export const updateVendorResidualRisk = createSafeActionClient() - .schema(schema) - .action(async ({ parsedInput }): Promise => { - try { - await db.vendor.update({ - where: { id: parsedInput.vendorId }, - data: { - residualProbability: parsedInput.residualProbability, - residualImpact: parsedInput.residualImpact, - }, - }); + .schema(schema) + .action(async ({ parsedInput }): Promise => { + try { + await db.vendor.update({ + where: { id: parsedInput.vendorId }, + data: { + residualProbability: parsedInput.residualProbability, + residualImpact: parsedInput.residualImpact, + }, + }); - return { success: true }; - } catch (error) { - return { - success: false, - error: - error instanceof Error ? error.message : appErrors.UNEXPECTED_ERROR, - }; - } - }); + revalidatePath(`/vendors/${parsedInput.vendorId}`); + + return { success: true }; + } catch (error) { + return { + success: false, + error: + error instanceof Error ? error.message : appErrors.UNEXPECTED_ERROR, + }; + } + }); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/inherent-risk-vendor-chart.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/inherent-risk-vendor-chart.tsx index e1e44b6878..5f6204cb5c 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/inherent-risk-vendor-chart.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/inherent-risk-vendor-chart.tsx @@ -11,140 +11,238 @@ import { CardHeader, CardTitle, } from "@comp/ui/card"; -import { - ChartContainer, - ChartTooltip, - ChartTooltipContent, -} from "@comp/ui/chart"; import { PencilIcon } from "lucide-react"; import { useQueryState } from "nuqs"; -import { - PolarAngleAxis, - PolarGrid, - PolarRadiusAxis, - Radar, - RadarChart, -} from "recharts"; import { InherentRiskSheet } from "./inherent-risk"; -import { useCallback } from "react"; interface InherentRiskChartProps { vendor: Vendor; } +// Map enum values to numeric scores (1-5) +const LIKELIHOOD_SCORES: Record = { + very_unlikely: 1, + unlikely: 2, + possible: 3, + likely: 4, + very_likely: 5, +}; + +const IMPACT_SCORES: Record = { + insignificant: 1, + minor: 2, + moderate: 3, + major: 4, + severe: 5, +}; + +// Risk level colors +const RISK_COLORS = { + low: "#22c55e", // green + medium: "#f59e0b", // amber + high: "#f97316", // orange + critical: "#ef4444", // red +}; + export function InherentRiskVendorChart({ vendor }: InherentRiskChartProps) { const t = useI18n(); const [open, setOpen] = useQueryState("inherent-risk-sheet"); - // Create a stable callback function for onSuccess - const handleSuccess = useCallback(() => { - console.log("Closing inherent risk sheet"); - setOpen(null); - }, [setOpen]); - - // Convert the enum values to numeric scores for display - const getRiskScore = (probability: Likelihood, impact: Impact) => { - const probabilityScores: Record = { - very_unlikely: 10, - unlikely: 30, - possible: 50, - likely: 70, - very_likely: 90, - }; - - const impactScores: Record = { - insignificant: 10, - minor: 30, - moderate: 50, - major: 70, - severe: 90, - }; - - return (probabilityScores[probability] * impactScores[impact]) / 100; - }; - - const inherentRiskScore = getRiskScore( - vendor.inherentProbability, - vendor.inherentImpact, - ); + // Debug information + console.log("VENDOR DATA:", { + inherentProbability: vendor.inherentProbability, + inherentImpact: vendor.inherentImpact, + probabilityScore: LIKELIHOOD_SCORES[vendor.inherentProbability], + impactScore: IMPACT_SCORES[vendor.inherentImpact], + }); + + // Calculate risk score from probability and impact + const riskScore = + LIKELIHOOD_SCORES[vendor.inherentProbability] * + IMPACT_SCORES[vendor.inherentImpact]; + + // Determine risk level + let riskLevel = "low"; + if (riskScore > 16) riskLevel = "critical"; + else if (riskScore > 9) riskLevel = "high"; + else if (riskScore > 4) riskLevel = "medium"; + + // Get color based on risk level + const riskColor = RISK_COLORS[riskLevel as keyof typeof RISK_COLORS]; + + // Define the visual order of likelihood for rendering + const VISUAL_LIKELIHOOD_ORDER: Likelihood[] = [ + Likelihood.very_likely, + Likelihood.likely, + Likelihood.possible, + Likelihood.unlikely, + Likelihood.very_unlikely, + ]; - const data = [ - { - metric: t("vendors.risks.inherent_risk"), - value: inherentRiskScore, - fullMark: 100, - }, + // We'll determine the active cell while building the grid + const yAxisLabels = [ + "V.Likely", // Corresponds to VISUAL_LIKELIHOOD_ORDER[0] + "Likely", + "Possible", + "Unlikely", + "V.Unlikely", // Corresponds to VISUAL_LIKELIHOOD_ORDER[4] ]; + const xAxisLabels = ["Insig", "Minor", "Mod", "Major", "Severe"]; - const chartConfig = { - risk: { - label: t("vendors.risks.inherent_risk"), - theme: { - light: "#ef4444", - dark: "#dc2626", - }, - }, - }; + // Store the active cell position values + const activeProbability = vendor.inherentProbability; + const activeImpact = vendor.inherentImpact; return ( - - -
-
- -
- {t("vendors.risks.inherent_risk")} - + <> + + +
+
+ +
+ {t("vendors.risks.inherent_risk")} + +
+
+ + {t("vendors.risks.update_inherent_risk_description")} + +
+
+
+ +
+ {/* 5x5 Risk Matrix */} +
+ {/* Y-axis label */} +
+ Probability +
+ + {/* Main grid container */} +
+
+ {/* Grid rows */} + {[...Array(5)].map((_, rowIndex) => { + // Map row index to likelihood enum based on visual order + const rowLikelihood = VISUAL_LIKELIHOOD_ORDER[rowIndex]; + + return ( +
+ {/* Grid cells for this row */} + {[...Array(5)].map((_, colIndex) => { + // Map column index to impact enum + const colImpact = Object.keys(IMPACT_SCORES)[ + colIndex + ] as Impact; + + // Calculate cell score and color + const likelihoodScore = + LIKELIHOOD_SCORES[rowLikelihood]; + const impactScore = IMPACT_SCORES[colImpact]; + const cellScore = likelihoodScore * impactScore; + + let cellColor = RISK_COLORS.low; + if (cellScore > 16) cellColor = RISK_COLORS.critical; + else if (cellScore > 9) cellColor = RISK_COLORS.high; + else if (cellScore > 4) + cellColor = RISK_COLORS.medium; + + // Check if this is the active cell + const isActive = + rowLikelihood === activeProbability && + colImpact === activeImpact; + + return ( +
+ {isActive && ( +
+
+
+ )} +
+ ); + })} +
+ ); + })} + + {/* Y-axis labels - positioned outside the grid */} + {yAxisLabels.map((label, i) => ( +
+ {label} +
+ ))} + + {/* X-axis labels */} + {xAxisLabels.map((label, i) => ( +
+ {label} +
+ ))} + + {/* X-axis title */} +
+ Impact +
+
- - - {t("vendors.risks.update_inherent_risk_description")} - +
-
- - - - - - - - - } - /> - - - - -
+ + + + ); } diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/inherent-risk.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/inherent-risk.tsx index ffe6fac184..b7a7e91287 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/inherent-risk.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/inherent-risk.tsx @@ -15,7 +15,7 @@ import { import { X } from "lucide-react"; import { InherentRiskForm } from "@/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/forms/risks/inherent-risk-form"; import { Impact, Likelihood } from "@prisma/client"; -import { useState } from "react"; +import { useQueryState } from "nuqs"; export function InherentRiskSheet({ vendorId, @@ -30,18 +30,22 @@ export function InherentRiskSheet({ }) { const t = useI18n(); const isDesktop = useMediaQuery("(min-width: 768px)"); - const [isOpen, setIsOpen] = useState(true); + const [open, setOpen] = useQueryState("inherent-risk-sheet"); + const isOpen = open === "true"; - const handleClose = () => setIsOpen(false); + const handleClose = () => setOpen(null); const handleFormSuccess = () => { - setIsOpen(false); + setOpen(null); if (onSuccess) onSuccess(); }; if (isDesktop) { return ( - + setOpen(value ? "true" : null)} + >
@@ -74,7 +78,10 @@ export function InherentRiskSheet({ } return ( - + setOpen(value ? "true" : null)} + > diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/residual-risk-vendor-chart.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/residual-risk-vendor-chart.tsx index 1961fa16f1..62d22920fc 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/residual-risk-vendor-chart.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/residual-risk-vendor-chart.tsx @@ -2,6 +2,7 @@ import { useI18n } from "@/locales/client"; import type { Vendor } from "@comp/db/types"; +import { Impact, Likelihood } from "@prisma/client"; import { Button } from "@comp/ui/button"; import { Card, @@ -10,138 +11,226 @@ import { CardHeader, CardTitle, } from "@comp/ui/card"; -import { - ChartContainer, - ChartTooltip, - ChartTooltipContent, -} from "@comp/ui/chart"; import { PencilIcon } from "lucide-react"; import { useQueryState } from "nuqs"; -import { - PolarAngleAxis, - PolarGrid, - PolarRadiusAxis, - Radar, - RadarChart, -} from "recharts"; import { ResidualRiskSheet } from "./residual-risk"; -import { useCallback } from "react"; interface ResidualRiskChartProps { vendor: Vendor; } +// Map enum values to numeric scores (1-5) +const LIKELIHOOD_SCORES: Record = { + very_unlikely: 1, + unlikely: 2, + possible: 3, + likely: 4, + very_likely: 5, +}; + +const IMPACT_SCORES: Record = { + insignificant: 1, + minor: 2, + moderate: 3, + major: 4, + severe: 5, +}; + +// Risk level colors +const RISK_COLORS = { + low: "#22c55e", // green + medium: "#f59e0b", // amber + high: "#f97316", // orange + critical: "#ef4444", // red +}; + export function ResidualRiskVendorChart({ vendor }: ResidualRiskChartProps) { const t = useI18n(); const [open, setOpen] = useQueryState("residual-risk-sheet"); - // Log when the sheet open state changes for debugging - console.log("Residual risk sheet query state:", { open }); - - // Log the vendor data to check the structure and values - console.log("Vendor residual risk data:", { - id: vendor.id, - residualRisk: vendor.residualProbability, - }); - - // Create a stable callback function for onSuccess - const handleSuccess = useCallback(() => { - console.log("Closing residual risk sheet"); - setOpen(null); - }, [setOpen]); - - // Convert the enum values to numeric scores for display - const getRiskScore = (risk: string) => { - switch (risk) { - case "low": - return 30; - case "medium": - return 60; - case "high": - return 90; - default: - return 0; - } - }; - - const residualRiskScore = getRiskScore(vendor.residualProbability); - - const data = [ - { - metric: t("vendors.risks.residual_risk"), - value: residualRiskScore, - fullMark: 100, - }, + // Calculate risk score from probability and impact + const riskScore = + LIKELIHOOD_SCORES[vendor.residualProbability] * + IMPACT_SCORES[vendor.residualImpact]; + + // Determine risk level + let riskLevel = "low"; + if (riskScore > 16) riskLevel = "critical"; + else if (riskScore > 9) riskLevel = "high"; + else if (riskScore > 4) riskLevel = "medium"; + + // Get color based on risk level + const riskColor = RISK_COLORS[riskLevel as keyof typeof RISK_COLORS]; + + // Define the visual order of likelihood for rendering + const VISUAL_LIKELIHOOD_ORDER: Likelihood[] = [ + Likelihood.very_likely, + Likelihood.likely, + Likelihood.possible, + Likelihood.unlikely, + Likelihood.very_unlikely, ]; - const chartConfig = { - risk: { - label: t("vendors.risks.residual_risk"), - theme: { - light: "#ef4444", - dark: "#dc2626", - }, - }, - }; + // We'll determine the active cell while building the grid + const yAxisLabels = [ + "V.Likely", // Corresponds to VISUAL_LIKELIHOOD_ORDER[0] + "Likely", + "Possible", + "Unlikely", + "V.Unlikely", // Corresponds to VISUAL_LIKELIHOOD_ORDER[4] + ]; + const xAxisLabels = ["Insig", "Minor", "Mod", "Major", "Severe"]; + + // Store the active cell position values + const activeProbability = vendor.residualProbability; + const activeImpact = vendor.residualImpact; return ( - - -
-
- -
- {t("vendors.risks.residual_risk")} - + <> + + +
+
+ +
+ {t("vendors.risks.residual_risk")} + +
+
+ + {t("vendors.risks.update_residual_risk_description")} + +
+
+
+ +
+ {/* 5x5 Risk Matrix */} +
+ {/* Y-axis label */} +
+ Probability +
+ + {/* Main grid container */} +
+
+ {/* Grid rows */} + {[...Array(5)].map((_, rowIndex) => { + // Map row index to likelihood enum based on visual order + const rowLikelihood = VISUAL_LIKELIHOOD_ORDER[rowIndex]; + + return ( +
+ {/* Grid cells for this row */} + {[...Array(5)].map((_, colIndex) => { + // Map column index to impact enum + const colImpact = Object.keys(IMPACT_SCORES)[ + colIndex + ] as Impact; + + // Calculate cell score and color + const likelihoodScore = + LIKELIHOOD_SCORES[rowLikelihood]; + const impactScore = IMPACT_SCORES[colImpact]; + const cellScore = likelihoodScore * impactScore; + + let cellColor = RISK_COLORS.low; + if (cellScore > 16) cellColor = RISK_COLORS.critical; + else if (cellScore > 9) cellColor = RISK_COLORS.high; + else if (cellScore > 4) + cellColor = RISK_COLORS.medium; + + // Check if this is the active cell + const isActive = + rowLikelihood === activeProbability && + colImpact === activeImpact; + + return ( +
+ {isActive && ( +
+
+
+ )} +
+ ); + })} +
+ ); + })} + + {/* Y-axis labels - positioned outside the grid */} + {yAxisLabels.map((label, i) => ( +
+ {label} +
+ ))} + + {/* X-axis labels */} + {xAxisLabels.map((label, i) => ( +
+ {label} +
+ ))} + + {/* X-axis title */} +
+ Impact +
+
- - - {t("vendors.risks.update_residual_risk_description")} - +
-
- - - - - - - - - } - /> - - - - -
+ + + + ); } diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/residual-risk.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/residual-risk.tsx index 8be4641f8b..4fe6157dbc 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/residual-risk.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/residual-risk.tsx @@ -15,7 +15,8 @@ import { import { X } from "lucide-react"; import { ResidualRiskForm } from "@/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/forms/risks/residual-risk-form"; import type { Vendor } from "@comp/db/types"; -import { useState } from "react"; +import { useQueryState } from "nuqs"; +import { useRouter, useSearchParams } from "next/navigation"; export function ResidualRiskSheet({ vendorId, @@ -27,19 +28,50 @@ export function ResidualRiskSheet({ onSuccess?: () => void; }) { const t = useI18n(); + const router = useRouter(); + const searchParams = useSearchParams(); const isDesktop = useMediaQuery("(min-width: 768px)"); - const [isOpen, setIsOpen] = useState(true); + const [open, setOpen] = useQueryState("residual-risk-sheet"); + const isOpen = open === "true"; - const handleClose = () => setIsOpen(false); + const handleClose = () => { + setOpen(null); + + // Create new URLSearchParams without the residual-risk-sheet parameter + const params = new URLSearchParams(searchParams); + params.delete("residual-risk-sheet"); + + // Create the new URL path with the updated query parameters + const newPath = + window.location.pathname + + (params.toString() ? `?${params.toString()}` : ""); + + // Update the URL without refreshing the page + router.replace(newPath); + }; const handleFormSuccess = () => { - setIsOpen(false); + setOpen(null); + + // Remove query params on success + const params = new URLSearchParams(searchParams); + params.delete("residual-risk-sheet"); + + const newPath = + window.location.pathname + + (params.toString() ? `?${params.toString()}` : ""); + + router.replace(newPath); + if (onSuccess) onSuccess(); }; if (isDesktop) { return ( - + (value ? setOpen("true") : handleClose())} + >
@@ -72,7 +104,10 @@ export function ResidualRiskSheet({ } return ( - + (value ? setOpen("true") : handleClose())} + > diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/secondary-fields/secondary-fields.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/secondary-fields/secondary-fields.tsx index 6019d4c087..2547557a14 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/secondary-fields/secondary-fields.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/secondary-fields/secondary-fields.tsx @@ -1,33 +1,33 @@ "use client"; import { useI18n } from "@/locales/client"; -import type { User, Vendor } from "@comp/db/types"; +import type { Member, User, Vendor } from "@comp/db/types"; import { Card, CardContent, CardHeader, CardTitle } from "@comp/ui/card"; import { UpdateSecondaryFieldsForm } from "./update-secondary-fields-form"; export function SecondaryFields({ - vendor, - users, + vendor, + assignees, }: { - vendor: Vendor & { owner: User | null }; - users: User[]; + vendor: Vendor & { assignee: { user: User | null } | null }; + assignees: (Member & { user: User })[]; }) { - const t = useI18n(); + const t = useI18n(); - return ( -
- - - -
- {t("risk.dashboard.overview")} -
-
-
- - - -
-
- ); + return ( +
+ + + +
+ {t("risk.dashboard.overview")} +
+
+
+ + + +
+
+ ); } diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/secondary-fields/update-secondary-fields-form.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/secondary-fields/update-secondary-fields-form.tsx index 4ace1b829d..4afcc3bcbd 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/secondary-fields/update-secondary-fields-form.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/secondary-fields/update-secondary-fields-form.tsx @@ -1,24 +1,23 @@ "use client"; -import { SelectUser } from "@/components/select-user"; import { VENDOR_STATUS_TYPES, VendorStatus } from "@/components/vendor-status"; import { useI18n } from "@/locales/client"; -import { VendorCategory, type User, type Vendor } from "@comp/db/types"; +import { Member, VendorCategory, type User, type Vendor } from "@comp/db/types"; import { Button } from "@comp/ui/button"; import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@comp/ui/form"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from "@comp/ui/select"; import { zodResolver } from "@hookform/resolvers/zod"; import { Loader2 } from "lucide-react"; @@ -26,172 +25,161 @@ import { useAction } from "next-safe-action/hooks"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import type { z } from "zod"; -import { updateVendorAction } from "../../actions/update-vendor-action"; +import { SelectAssignee } from "../../../../components/SelectAssignee"; import { updateVendorSchema } from "../../actions/schema"; +import { updateVendorAction } from "../../actions/update-vendor-action"; export function UpdateSecondaryFieldsForm({ - vendor, - users, + vendor, + assignees, }: { - vendor: Vendor & { owner: User | null }; - users: User[]; + vendor: Vendor; + assignees: (Member & { user: User })[]; }) { - const t = useI18n(); + const t = useI18n(); - const updateVendor = useAction(updateVendorAction, { - onSuccess: () => { - toast.success(t("vendors.form.update_vendor_success")); - }, - onError: () => { - toast.error(t("vendors.form.update_vendor_error")); - }, - }); + const updateVendor = useAction(updateVendorAction, { + onSuccess: () => { + toast.success(t("vendors.form.update_vendor_success")); + }, + onError: () => { + toast.error(t("vendors.form.update_vendor_error")); + }, + }); - const form = useForm>({ - resolver: zodResolver(updateVendorSchema), - defaultValues: { - id: vendor.id, - name: vendor.name, - description: vendor.description, - assigneeId: vendor.ownerId ?? undefined, - category: vendor.category, - status: vendor.status, - }, - }); + const form = useForm>({ + resolver: zodResolver(updateVendorSchema), + defaultValues: { + id: vendor.id, + name: vendor.name, + description: vendor.description, + assigneeId: vendor.assigneeId, + category: vendor.category, + status: vendor.status, + }, + }); - const onSubmit = (data: z.infer) => { - updateVendor.execute({ - id: data.id, - name: data.name, - description: data.description, - assigneeId: data.assigneeId, - category: data.category, - status: data.status, - }); - }; + const onSubmit = (data: z.infer) => { + updateVendor.execute({ + id: data.id, + name: data.name, + description: data.description, + assigneeId: data.assigneeId, + category: data.category, + status: data.status, + }); + }; - return ( -
- -
- ( - - {t("common.assignee.label")} - - - - - - )} - /> - ( - - {t("vendors.form.vendor_status")} - - - - - - )} - /> - ( - - {t("vendors.form.vendor_category")} - - - - - - )} - /> -
-
- -
-
- - ); + return ( +
+ +
+ ( + + {t("common.assignee.label")} + + + + + + )} + /> + ( + + {t("vendors.form.vendor_status")} + + + + + + )} + /> + ( + + {t("vendors.form.vendor_category")} + + + + + + )} + /> +
+
+ +
+
+ + ); } diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/tasks/create-vendor-task-form.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/tasks/create-vendor-task-form.tsx index cb33a51829..619d3e064f 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/tasks/create-vendor-task-form.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/components/tasks/create-vendor-task-form.tsx @@ -6,29 +6,29 @@ import { createVendorTaskSchema } from "../../actions/schema"; import { SelectUser } from "@/components/select-user"; import { useI18n } from "@/locales/client"; import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, } from "@comp/ui/accordion"; import { Button } from "@comp/ui/button"; import { Calendar } from "@comp/ui/calendar"; import { cn } from "@comp/ui/cn"; import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@comp/ui/form"; import { Input } from "@comp/ui/input"; import { Popover, PopoverContent, PopoverTrigger } from "@comp/ui/popover"; import { - Select, - SelectContent, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectTrigger, + SelectValue, } from "@comp/ui/select"; import { Textarea } from "@comp/ui/textarea"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -41,211 +41,185 @@ import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import type { z } from "zod"; +import { SelectAssignee } from "../../../../components/SelectAssignee"; +import { Member, User } from "@comp/db/types"; -interface User { - id: string; - image?: string | null; - name: string | null; -} - -export function CreateVendorTaskForm() { - const t = useI18n(); - - const [users, setUsers] = useState([]); - const [isLoadingUsers, setIsLoadingUsers] = useState(true); - const [_, setCreateVendorTaskSheet] = useQueryState( - "create-vendor-task-sheet", - ); - const params = useParams<{ vendorId: string }>(); - - useEffect(() => { - async function loadUsers() { - const result = await getOrganizationUsersAction(); - if (result?.data?.success && result?.data?.data) { - setUsers(result.data.data); - } - setIsLoadingUsers(false); - } +export function CreateVendorTaskForm({ + assignees, +}: { + assignees: (Member & { user: User })[]; +}) { + const t = useI18n(); - loadUsers(); - }, []); + const [_, setCreateVendorTaskSheet] = useQueryState( + "create-vendor-task-sheet", + ); + const params = useParams<{ vendorId: string }>(); - const createTask = useAction(createVendorTaskAction, { - onSuccess: () => { - toast.success(t("risk.tasks.form.success")); - setCreateVendorTaskSheet(null); - }, - onError: () => { - toast.error(t("risk.tasks.form.error")); - }, - }); + const createTask = useAction(createVendorTaskAction, { + onSuccess: () => { + toast.success(t("risk.tasks.form.success")); + setCreateVendorTaskSheet(null); + }, + onError: () => { + toast.error(t("risk.tasks.form.error")); + }, + }); - const form = useForm>({ - resolver: zodResolver(createVendorTaskSchema), - defaultValues: { - title: "", - description: "", - dueDate: new Date(), - assigneeId: "", - vendorId: params.vendorId, - }, - }); + const form = useForm>({ + resolver: zodResolver(createVendorTaskSchema), + defaultValues: { + title: "", + description: "", + dueDate: new Date(), + assigneeId: "", + vendorId: params.vendorId, + }, + }); - const onSubmit = (data: z.infer) => { - createTask.execute(data); - }; + const onSubmit = (data: z.infer) => { + createTask.execute(data); + }; - return ( -
- -
-
- - - - {t("risk.tasks.form.title")} - - -
- ( - - - {t("risk.tasks.form.task_title")} - - - - - - - )} - /> + return ( + + +
+
+ + + + {t("risk.tasks.form.title")} + + +
+ ( + + + {t("risk.tasks.form.task_title")} + + + + + + + )} + /> - ( - - - {t("risk.tasks.form.description")} - - -