From 27665d35bb98ba2d3225da6bdbe12aa56ecea05a Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 3 Apr 2025 16:44:28 -0400 Subject: [PATCH] refactor: replace ownerId with assigneeId across risk and task schemas, actions, and components for improved clarity and consistency --- .../src/actions/risk/create-risk-action.ts | 79 +++---- .../src/actions/risk/update-risk-action.ts | 98 ++++---- apps/app/src/actions/schema.ts | 8 +- .../[orgId]/components/SelectAssignee.tsx | 19 +- .../[orgId]/risk/(overview)/layout.tsx | 26 ++- .../[orgId]/risk/(overview)/page.tsx | 2 +- .../[orgId]/risk/[riskId]/page.tsx | 51 ++++- .../risk/register/RiskRegisterTable.tsx | 22 +- .../components/table/RiskRegisterColumns.tsx | 25 ++- .../components/table/RiskRegisterFilters.tsx | 9 +- .../[orgId]/risk/register/data/getRisks.ts | 113 +++++----- .../[orgId]/risk/register/page.tsx | 37 ++- .../[orgId]/risk/register/search-params.ts | 2 +- .../vendors/[vendorId]/actions/schema.ts | 116 +++++----- .../secondary-fields/secondary-fields.tsx | 57 ++--- .../tasks/[taskId]/components/title/title.tsx | 9 +- .../components/title/update-task-sheet.tsx | 64 ++---- .../[vendorId]/tasks/[taskId]/page.tsx | 45 ++-- .../forms/risks/create-risk-form.tsx | 44 ++-- .../components/forms/risks/risk-overview.tsx | 38 ++-- .../{risks-assignee.tsx => RisksAssignee.tsx} | 51 +++-- .../risks/charts/risks-by-assignee.tsx | 212 ------------------ .../src/components/risks/risk-overview.tsx | 10 +- .../components/sheets/create-risk-sheet.tsx | 11 +- .../components/tables/risk-tasks/columns.tsx | 12 +- .../tables/risk-tasks/filter-toolbar.tsx | 13 +- .../migration.sql | 18 ++ .../migration.sql | 16 ++ packages/db/prisma/schema/auth.prisma | 4 +- packages/db/prisma/schema/risk.prisma | 18 +- packages/db/prisma/schema/task.prisma | 15 +- 31 files changed, 548 insertions(+), 696 deletions(-) rename apps/app/src/components/risks/charts/{risks-assignee.tsx => RisksAssignee.tsx} (86%) delete mode 100644 apps/app/src/components/risks/charts/risks-by-assignee.tsx create mode 100644 packages/db/prisma/migrations/20250403194504_risk_assignee/migration.sql create mode 100644 packages/db/prisma/migrations/20250403201319_tasks_use_asignee/migration.sql diff --git a/apps/app/src/actions/risk/create-risk-action.ts b/apps/app/src/actions/risk/create-risk-action.ts index 90ae7a55e0..5a71f13b27 100644 --- a/apps/app/src/actions/risk/create-risk-action.ts +++ b/apps/app/src/actions/risk/create-risk-action.ts @@ -10,46 +10,47 @@ import { Likelihood } from "@comp/db/types"; import { Impact } from "@comp/db/types"; export const createRiskAction = authActionClient - .schema(createRiskSchema) - .metadata({ - name: "create-risk", - track: { - event: "create-risk", - channel: "server", - }, - }) - .action(async ({ parsedInput, ctx }) => { - const { title, description, category, department } = parsedInput; - const { user, session } = ctx; + .schema(createRiskSchema) + .metadata({ + name: "create-risk", + track: { + event: "create-risk", + channel: "server", + }, + }) + .action(async ({ parsedInput, ctx }) => { + const { title, description, category, department, assigneeId } = + parsedInput; + const { user, session } = ctx; - if (!user.id || !session.activeOrganizationId) { - throw new Error("Invalid user input"); - } + if (!user.id || !session.activeOrganizationId) { + throw new Error("Invalid user input"); + } - try { - await db.risk.create({ - data: { - title, - description, - category, - department, - likelihood: Likelihood.very_unlikely, - impact: Impact.insignificant, - ownerId: user.id, - organizationId: session.activeOrganizationId, - }, - }); + try { + await db.risk.create({ + data: { + title, + description, + category, + department, + likelihood: Likelihood.very_unlikely, + impact: Impact.insignificant, + assigneeId: assigneeId, + organizationId: session.activeOrganizationId, + }, + }); - revalidatePath(`/${session.activeOrganizationId}/risk`); - revalidatePath(`/${session.activeOrganizationId}/risk/register`); - revalidateTag(`risk_${session.activeOrganizationId}`); + revalidatePath(`/${session.activeOrganizationId}/risk`); + revalidatePath(`/${session.activeOrganizationId}/risk/register`); + revalidateTag(`risk_${session.activeOrganizationId}`); - return { - success: true, - }; - } catch (error) { - return { - success: false, - }; - } - }); + return { + success: true, + }; + } catch (error) { + return { + success: false, + }; + } + }); diff --git a/apps/app/src/actions/risk/update-risk-action.ts b/apps/app/src/actions/risk/update-risk-action.ts index 22a70c1f2d..fb1420b816 100644 --- a/apps/app/src/actions/risk/update-risk-action.ts +++ b/apps/app/src/actions/risk/update-risk-action.ts @@ -8,52 +8,52 @@ import { authActionClient } from "../safe-action"; import { updateRiskSchema } from "../schema"; export const updateRiskAction = authActionClient - .schema(updateRiskSchema) - .metadata({ - name: "update-risk", - track: { - event: "update-risk", - channel: "server", - }, - }) - .action(async ({ parsedInput, ctx }) => { - const { id, title, description, category, department, ownerId, status } = - parsedInput; - const { session } = ctx; - - if (!session.activeOrganizationId) { - throw new Error("Invalid user input"); - } - - try { - await db.risk.update({ - where: { - id, - organizationId: session.activeOrganizationId, - }, - data: { - title: title, - description: description, - ownerId: ownerId, - category: category, - department: department, - status: status, - }, - }); - - revalidatePath(`/${session.activeOrganizationId}/risk`); - revalidatePath(`/${session.activeOrganizationId}/risk/register`); - revalidatePath(`/${session.activeOrganizationId}/risk/${id}`); - revalidateTag("risks"); - - return { - success: true, - }; - } catch (error) { - console.error("Error updating risk:", error); - - return { - success: false, - }; - } - }); + .schema(updateRiskSchema) + .metadata({ + name: "update-risk", + track: { + event: "update-risk", + channel: "server", + }, + }) + .action(async ({ parsedInput, ctx }) => { + const { id, title, description, category, department, assigneeId, status } = + parsedInput; + const { session } = ctx; + + if (!session.activeOrganizationId) { + throw new Error("Invalid user input"); + } + + try { + await db.risk.update({ + where: { + id, + organizationId: session.activeOrganizationId, + }, + data: { + title: title, + description: description, + assigneeId: assigneeId, + category: category, + department: department, + status: status, + }, + }); + + revalidatePath(`/${session.activeOrganizationId}/risk`); + revalidatePath(`/${session.activeOrganizationId}/risk/register`); + revalidatePath(`/${session.activeOrganizationId}/risk/${id}`); + revalidateTag("risks"); + + return { + success: true, + }; + } catch (error) { + console.error("Error updating risk:", error); + + return { + success: false, + }; + } + }); diff --git a/apps/app/src/actions/schema.ts b/apps/app/src/actions/schema.ts index ed254a0fbf..5390cc8dec 100644 --- a/apps/app/src/actions/schema.ts +++ b/apps/app/src/actions/schema.ts @@ -95,9 +95,7 @@ export const createRiskSchema = z.object({ department: z.nativeEnum(Departments, { required_error: "Risk department is required", }), - ownerId: z.string({ - required_error: "You must assign an owner to the risk", - }), + assigneeId: z.string().nullable(), }); export const updateRiskSchema = z.object({ @@ -116,9 +114,7 @@ export const updateRiskSchema = z.object({ department: z.nativeEnum(Departments, { required_error: "Risk department is required", }), - ownerId: z.string({ - required_error: "You must assign an owner to the risk", - }), + assigneeId: z.string().nullable(), status: z.nativeEnum(RiskStatus, { required_error: "Risk status is required", }), 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 39866f2940..ac7727cad7 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 @@ -68,6 +68,13 @@ export const SelectAssignee = ({ return image; }; + // Render the none fallback avatar + const renderNoneAvatar = () => ( +
+ +
+ ); + return (
{withTitle && ( @@ -101,11 +108,7 @@ export const SelectAssignee = ({
) : (
- - - - - + {renderNoneAvatar()} None
)} @@ -118,11 +121,7 @@ export const SelectAssignee = ({ >
- - - - - + {renderNoneAvatar()} None
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 5e807abd0f..45a2d74ee3 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 @@ -18,10 +18,10 @@ export default async function Layout({ headers: await headers(), }); - const user = session?.user; const orgId = session?.session.activeOrganizationId; const overview = await getRiskOverview(); + const assignees = await getAssignees(); if (overview?.risks === 0) { return ( @@ -56,7 +56,7 @@ export default async function Layout({ }, ]} /> - + @@ -82,6 +82,28 @@ export default async function Layout({ ); } +const getAssignees = cache(async () => { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session || !session.session.activeOrganizationId) { + return []; + } + + const activeOrganizationId = session.session.activeOrganizationId; + + return await db.member.findMany({ + where: { + organizationId: activeOrganizationId, + isActive: true, + }, + include: { + user: true, + }, + }); +}); + const getRiskOverview = cache(async () => { const session = await auth.api.getSession({ headers: await headers(), diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/page.tsx index 92f29c8e7c..553afbefdf 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/(overview)/page.tsx @@ -1,6 +1,6 @@ import { auth } from "@comp/auth"; import { RiskOverview } from "@/components/risks/charts/risk-overview"; -import { RisksAssignee } from "@/components/risks/charts/risks-assignee"; +import { RisksAssignee } from "@/components/risks/charts/RisksAssignee"; import { getI18n } from "@/locales/server"; import type { Metadata } from "next"; import { setStaticParamsLocale } from "next-international/server"; 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 8d90e2d660..82c9925aec 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 @@ -36,7 +36,7 @@ export default async function RiskPage({ searchParams, params }: PageProps) { const { riskId } = await params; const risk = await getRisk(riskId); - const users = await useUsers(); + const assignees = await getAssignees(); const t = await getI18n(); const { @@ -66,7 +66,7 @@ export default async function RiskPage({ searchParams, params }: PageProps) { return (
- + @@ -78,7 +78,10 @@ export default async function RiskPage({ searchParams, params }: PageProps) {
- + {loadedTasks.length > 0 ? ( { const t = useI18n(); const session = useSession(); @@ -75,12 +83,10 @@ export const RiskRegisterTable = ({ "qms", ] as const; - const { data: admins } = useOrganizationAdmins(); - const filterCategories = RiskRegisterFilters({ setPage: (newPage: number) => setPage(newPage), departments: departments, - assignees: admins || [], + assignees, status, setStatus, department, @@ -91,7 +97,7 @@ export const RiskRegisterTable = ({ return ( <> - + , }} /> - + ); }; diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/components/table/RiskRegisterColumns.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/components/table/RiskRegisterColumns.tsx index 8944e45535..cc23852a08 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/components/table/RiskRegisterColumns.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/components/table/RiskRegisterColumns.tsx @@ -4,10 +4,10 @@ import { Badge } from "@comp/ui/badge"; import Link from "next/link"; import { Avatar, AvatarFallback, AvatarImage } from "@comp/ui/avatar"; import { Status } from "@/components/status"; +import { RiskRegisterTableRow } from "../../RiskRegisterTable"; +import { UserIcon } from "lucide-react"; -export const columns = ( - orgId: string, -): ColumnDef[] => [ +export const columns = (orgId: string): ColumnDef[] => [ { header: "Risk", accessorKey: "title", @@ -41,18 +41,29 @@ export const columns = ( header: "Assignee", accessorKey: "assignee", cell: ({ row }) => { + if (!row.original.assignee) { + return ( +
+
+ +
+

None

+
+ ); + } + return (
- {row.original.owner?.name?.charAt(0) || "?"} + {row.original.assignee.name?.charAt(0) || "?"} -

{row.original.owner?.name}

+

{row.original.assignee.name}

); }, diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/components/table/RiskRegisterFilters.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/components/table/RiskRegisterFilters.tsx index 22edc6aff4..4c5f63154b 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/components/table/RiskRegisterFilters.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/components/table/RiskRegisterFilters.tsx @@ -1,6 +1,5 @@ import { Status } from "@/components/status"; -import { type Departments, RiskStatus } from "@comp/db/types"; -import type { Admin } from "../../../../hooks/useOrganizationAdmins"; +import { type Departments, Member, RiskStatus, User } from "@comp/db/types"; import { AssigneeAvatar } from "../../../../evidence/list/components/table/components/AssigneeAvatar"; export const RiskRegisterFilters = ({ @@ -16,7 +15,7 @@ export const RiskRegisterFilters = ({ }: { setPage: (page: number) => void; departments: Departments[]; - assignees: Admin[] | undefined; + assignees: (Member & { user: User })[] | undefined; status: RiskStatus | null; setStatus: (status: RiskStatus | null) => void; department: Departments | null; @@ -57,14 +56,14 @@ export const RiskRegisterFilters = ({ { label: "Filter by Assignee", items: (assignees || []).map((assignee) => ({ - label: assignee.name || "Unknown", + label: assignee.user.name || "Unknown", value: assignee.id, checked: assigneeId === assignee.id, onChange: (checked: boolean) => { setAssigneeId(checked ? assignee.id : null); setPage(1); }, - icon: , + icon: , })), maxHeight: "150px", }, diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/data/getRisks.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/data/getRisks.ts index bf26bf7be1..3f5a38e1ad 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/data/getRisks.ts +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/data/getRisks.ts @@ -6,56 +6,69 @@ import { type Departments, Prisma, type RiskStatus } from "@comp/db/types"; import { headers } from "next/headers"; export async function getRisks({ - search, - page, - pageSize, - status, - department, - assigneeId, + search, + page, + pageSize, + status, + department, + assigneeId, }: { - search?: string; - page?: number; - pageSize?: number; - status?: RiskStatus | null; - department?: Departments | null; - assigneeId?: string | null; + search?: string; + page?: number; + pageSize?: number; + status?: RiskStatus | null; + department?: Departments | null; + assigneeId?: string | null; }) { - const session = await auth.api.getSession({ - headers: await headers(), - }); - - if (!session || !session.session.activeOrganizationId) { - return { - success: false, - error: "Unauthorized", - }; - } - - const where = { - organizationId: session.session.activeOrganizationId, - ...(search && { - title: { - contains: search, - mode: Prisma.QueryMode.insensitive, - }, - }), - ...(status ? { status } : {}), - ...(department ? { department } : {}), - ...(assigneeId ? { ownerId: assigneeId } : {}), - }; - - const skip = ((page ?? 1) - 1) * (pageSize ?? 10); - - const risks = await db.risk.findMany({ - where, - skip, - take: pageSize, - include: { - owner: true, - }, - }); - - return { - risks, - }; + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session || !session.session.activeOrganizationId) { + return { + success: false, + error: "Unauthorized", + }; + } + + const where = { + organizationId: session.session.activeOrganizationId, + ...(search && { + title: { + contains: search, + mode: Prisma.QueryMode.insensitive, + }, + }), + ...(status ? { status } : {}), + ...(department ? { department } : {}), + ...(assigneeId ? { assigneeId } : {}), + }; + + const skip = ((page ?? 1) - 1) * (pageSize ?? 10); + + const risks = await db.risk.findMany({ + where, + skip, + take: pageSize, + include: { + assignee: { + include: { + user: true, + }, + }, + }, + orderBy: { + createdAt: "desc", + }, + }); + + // Transform the data to match the expected structure by RiskRegisterTable + const transformedRisks = risks.map((risk) => ({ + ...risk, + assignee: risk.assignee ? risk.assignee.user : null, + })); + + return { + risks: transformedRisks, + }; } diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/page.tsx index d96a773a0c..1776c97d1e 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/page.tsx @@ -4,6 +4,10 @@ import { getI18n } from "@/locales/server"; import { setStaticParamsLocale } from "next-international/server"; import type { RiskStatus, Departments } from "@comp/db/types"; import { getRisks } from "./data/getRisks"; +import { auth } from "@comp/auth"; +import { cache } from "react"; +import { db } from "@comp/db"; +import { headers } from "next/headers"; export default async function RiskRegisterPage({ params, @@ -30,7 +34,15 @@ export default async function RiskRegisterPage({ assigneeId: assigneeId || null, }); - return ; + const assignees = await getAssignees(); + + return ( + + ); } export async function generateMetadata({ @@ -46,3 +58,26 @@ export async function generateMetadata({ title: t("risk.register.title"), }; } + +const getAssignees = cache(async () => { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session || !session.session.activeOrganizationId) { + return []; + } + + return await db.member.findMany({ + where: { + organizationId: session.session.activeOrganizationId, + isActive: true, + role: { + notIn: ["employee"], + }, + }, + include: { + user: true, + }, + }); +}); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/search-params.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/search-params.ts index 3f1579e77b..b51788dfc6 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/search-params.ts +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/risk/register/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]/vendors/[vendorId]/actions/schema.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/actions/schema.ts index 5fbfe4259e..a0e94dbea0 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/actions/schema.ts +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/actions/schema.ts @@ -2,83 +2,83 @@ import { VendorStatus, VendorCategory, TaskStatus } from "@comp/db/types"; import { z } from "zod"; export const createVendorTaskCommentSchema = z.object({ - vendorId: z.string().min(1, { - message: "Vendor ID is required", - }), - vendorTaskId: z.string().min(1, { - message: "Task ID is required", - }), - content: z.string().min(1, { - message: "Task content is required", - }), + vendorId: z.string().min(1, { + message: "Vendor ID is required", + }), + vendorTaskId: z.string().min(1, { + message: "Task ID is required", + }), + content: z.string().min(1, { + message: "Task content is required", + }), }); export const createVendorTaskSchema = z.object({ - vendorId: z.string().min(1, { - message: "Vendor ID is required", - }), - title: z.string().min(1, { - message: "Title is required", - }), - description: z.string().min(1, { - message: "Description is required", - }), - dueDate: z.date({ - required_error: "Due date is required", - }), - ownerId: z.string().optional(), + vendorId: z.string().min(1, { + message: "Vendor ID is required", + }), + title: z.string().min(1, { + message: "Title is required", + }), + description: z.string().min(1, { + message: "Description is required", + }), + dueDate: z.date({ + required_error: "Due date is required", + }), + assigneeId: z.string().nullable(), }); export const vendorContactSchema = z.object({ - name: z.string().min(1, "Name is required"), - email: z.string().email("Invalid email address"), - role: z.string().min(1, "Role is required"), + name: z.string().min(1, "Name is required"), + email: z.string().email("Invalid email address"), + role: z.string().min(1, "Role is required"), }); export const createVendorSchema = z.object({ - name: z.string().min(1, "Name is required"), - website: z.string().url("Must be a valid URL"), - description: z.string().min(1, "Description is required"), - category: z.nativeEnum(VendorCategory), - ownerId: z.string().optional(), - contacts: z - .array(vendorContactSchema) - .min(1, "At least one contact is required"), + name: z.string().min(1, "Name is required"), + website: z.string().url("Must be a valid URL"), + description: z.string().min(1, "Description is required"), + category: z.nativeEnum(VendorCategory), + assigneeId: z.string().nullable(), + contacts: z + .array(vendorContactSchema) + .min(1, "At least one contact is required"), }); export const updateVendorSchema = z.object({ - id: z.string(), - name: z.string().min(1, "Name is required"), - description: z.string().min(1, "Description is required"), - category: z.nativeEnum(VendorCategory), - status: z.nativeEnum(VendorStatus), - ownerId: z.string().optional(), + id: z.string(), + name: z.string().min(1, "Name is required"), + description: z.string().min(1, "Description is required"), + category: z.nativeEnum(VendorCategory), + status: z.nativeEnum(VendorStatus), + assigneeId: z.string().nullable(), }); export const createVendorCommentSchema = z.object({ - vendorId: z.string(), - content: z.string().min(1), + vendorId: z.string(), + content: z.string().min(1), }); export const updateVendorRiskSchema = z.object({ - id: z.string(), - inherent_risk: z.enum(["low", "medium", "high", "unknown"]).optional(), - residual_risk: z.enum(["low", "medium", "high", "unknown"]).optional(), + id: z.string(), + inherent_risk: z.enum(["low", "medium", "high", "unknown"]).optional(), + residual_risk: z.enum(["low", "medium", "high", "unknown"]).optional(), }); export const updateVendorTaskSchema = z.object({ - id: z.string().min(1, { - message: "Task ID is required", - }), - title: z.string().min(1, { - message: "Title is required", - }), - description: z.string().min(1, { - message: "Description is required", - }), - dueDate: z.date().optional(), - status: z.nativeEnum(TaskStatus, { - required_error: "Task status is required", - }), - userId: z.string().optional(), + id: z.string().min(1, { + message: "Task ID is required", + }), + title: z.string().min(1, { + message: "Title is required", + }), + description: z.string().min(1, { + message: "Description is required", + }), + dueDate: z.date().optional(), + status: z.nativeEnum(TaskStatus, { + required_error: "Task status is required", + }), + assigneeId: z.string().nullable(), }); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/tasks/[taskId]/components/secondary-fields/secondary-fields.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/tasks/[taskId]/components/secondary-fields/secondary-fields.tsx index 1f89f3ef39..fa238776e9 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/tasks/[taskId]/components/secondary-fields/secondary-fields.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/tasks/[taskId]/components/secondary-fields/secondary-fields.tsx @@ -1,11 +1,10 @@ "use client"; -import { SelectUser } from "@/components/select-user"; -import { useI18n } from "@/locales/client"; -import type { Task } from "@comp/db/types"; +import { SelectAssignee } from "@/app/[locale]/(app)/(dashboard)/[orgId]/components/SelectAssignee"; +import type { Member, Task, User } from "@comp/db/types"; import { Button } from "@comp/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@comp/ui/card"; import { Calendar } from "@comp/ui/calendar"; +import { Card, CardContent, CardHeader, CardTitle } from "@comp/ui/card"; import { cn } from "@comp/ui/cn"; import { Form, @@ -33,18 +32,12 @@ import type { z } from "zod"; import { updateVendorTaskSchema } from "../../../../actions/schema"; import { updateVendorTaskAction } from "../../../../actions/task/update-task-action"; -interface UserBasic { - id: string; - name: string | null; - image: string | null; -} - export default function SecondaryFields({ task, - users, + assignees, }: { - task: Task & { user: { name: string | null; image: string | null } | null }; - users: UserBasic[]; + task: Task & { assignee: { user: User } | null }; + assignees: (Member & { user: User })[]; }) { return (
@@ -57,7 +50,7 @@ export default function SecondaryFields({ - +
@@ -66,12 +59,12 @@ export default function SecondaryFields({ function TaskSecondaryFieldsForm({ task, - users, + assignees, }: { task: Task & { - user: { name: string | null; image: string | null } | null; + assignee: { user: User } | null; }; - users: UserBasic[]; + assignees: (Member & { user: User })[]; }) { const updateTask = useAction(updateVendorTaskAction, { onSuccess: () => { @@ -90,7 +83,7 @@ function TaskSecondaryFieldsForm({ description: task.description, dueDate: task.dueDate ? new Date(task.dueDate) : undefined, status: task.status, - userId: task.userId || undefined, + assigneeId: task.assigneeId || null, }, }); @@ -132,30 +125,18 @@ function TaskSecondaryFieldsForm({
( Assignee - + diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/tasks/[taskId]/components/title/title.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/tasks/[taskId]/components/title/title.tsx index 7bdae25209..64eed9cb7c 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/tasks/[taskId]/components/title/title.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/tasks/[taskId]/components/title/title.tsx @@ -1,7 +1,7 @@ "use client"; import { useI18n } from "@/locales/client"; -import type { Task } from "@comp/db/types"; +import type { Member, Task, User } from "@comp/db/types"; import { Alert, AlertDescription, AlertTitle } from "@comp/ui/alert"; import { Button } from "@comp/ui/button"; import { Icons } from "@comp/ui/icons"; @@ -18,11 +18,12 @@ const UpdateTaskSheet = dynamic( interface TitleProps { task: Task & { - user: { name: string | null; image: string | null } | null; + assignee: { user: User } | null; }; + assignees: (Member & { user: User })[]; } -export default function Title({ task }: TitleProps) { +export default function Title({ task, assignees }: TitleProps) { const t = useI18n(); const [isOpen, setOpen] = useQueryState("task-overview-sheet"); const open = isOpen === "true"; @@ -52,7 +53,7 @@ export default function Title({ task }: TitleProps) { onOpenChange={(isOpen) => setOpen(isOpen ? "true" : null)} > - {open && } + {open && }
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/tasks/[taskId]/components/title/update-task-sheet.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/tasks/[taskId]/components/title/update-task-sheet.tsx index 70a786eca6..276d48d4c0 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/tasks/[taskId]/components/title/update-task-sheet.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/tasks/[taskId]/components/title/update-task-sheet.tsx @@ -1,8 +1,7 @@ "use client"; -import { getOrganizationUsersAction } from "@/actions/organization/get-organization-users-action"; -import { useI18n } from "@/locales/client"; -import type { Task, VendorStatus } from "@comp/db/types"; +import { SelectAssignee } from "@/app/[locale]/(app)/(dashboard)/[orgId]/components/SelectAssignee"; +import type { Member, Task, User } from "@comp/db/types"; import { Accordion, AccordionContent, @@ -36,44 +35,21 @@ import { ArrowRightIcon, CalendarIcon } from "lucide-react"; import { useAction } from "next-safe-action/hooks"; import { useParams } from "next/navigation"; import { useQueryState } from "nuqs"; -import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import type { z } from "zod"; import { updateVendorTaskSchema } from "../../../../actions/schema"; import { updateVendorTaskAction } from "../../../../actions/task/update-task-action"; -import { SelectUser } from "@/components/select-user"; - -interface User { - id: string; - image?: string | null; - name: string | null; -} interface UpdateTaskSheetProps { - task: Task & { user: { name: string | null; image: string | null } | null }; + task: Task & { assignee: { user: User } | null }; + assignees: (Member & { user: User })[]; } -export function UpdateTaskSheet({ task }: UpdateTaskSheetProps) { - const t = useI18n(); - - const [users, setUsers] = useState([]); - const [isLoadingUsers, setIsLoadingUsers] = useState(true); +export function UpdateTaskSheet({ task, assignees }: UpdateTaskSheetProps) { const [_, setTaskOverviewSheet] = useQueryState("task-overview-sheet"); const params = useParams<{ taskId: string }>(); - useEffect(() => { - async function loadUsers() { - const result = await getOrganizationUsersAction(); - if (result?.data?.success && result?.data?.data) { - setUsers(result.data.data); - } - setIsLoadingUsers(false); - } - - loadUsers(); - }, []); - const updateTask = useAction(updateVendorTaskAction, { onSuccess: () => { toast.success("Task updated successfully"); @@ -92,7 +68,7 @@ export function UpdateTaskSheet({ task }: UpdateTaskSheetProps) { description: task.description, dueDate: task.dueDate ? new Date(task.dueDate) : undefined, status: task.status, - userId: task.userId || undefined, + assigneeId: task.assigneeId || null, }, }); @@ -264,30 +240,18 @@ export function UpdateTaskSheet({ task }: UpdateTaskSheetProps) { ( Assignee - + diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/tasks/[taskId]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/tasks/[taskId]/page.tsx index 9534551d1e..e2a2b5327d 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/tasks/[taskId]/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/vendors/[vendorId]/tasks/[taskId]/page.tsx @@ -13,13 +13,12 @@ interface PageProps { params: Promise<{ locale: string; orgId: string; - vendorId: string; taskId: string; }>; } export default async function TaskPage({ params }: PageProps) { - const { locale, orgId, vendorId, taskId } = await params; + const { locale, orgId, taskId } = await params; setStaticParamsLocale(locale); const t = await getI18n(); @@ -38,10 +37,9 @@ export default async function TaskPage({ params }: PageProps) { organizationId: orgId, }, include: { - user: { - select: { - name: true, - image: true, + assignee: { + include: { + user: true, }, }, }, @@ -51,32 +49,25 @@ export default async function TaskPage({ params }: PageProps) { notFound(); } - // Fetch organization users - const users = await db.member.findMany({ - where: { - organizationId: orgId, - }, - select: { - user: { - select: { - id: true, - name: true, - image: true, - }, + const getAssignees = async () => { + const assignees = await db.member.findMany({ + where: { + organizationId: orgId, }, - }, - }); + include: { + user: true, + }, + }); + + return assignees; + }; - const formattedUsers = users.map((member) => ({ - id: member.user.id, - name: member.user.name, - image: member.user.image, - })); + const assignees = await getAssignees(); return (
- - <SecondaryFields task={task} users={formattedUsers} /> + <Title task={task} assignees={assignees} /> + <SecondaryFields task={task} assignees={assignees} /> </div> ); } diff --git a/apps/app/src/components/forms/risks/create-risk-form.tsx b/apps/app/src/components/forms/risks/create-risk-form.tsx index 60a7dcaf9c..8b07712f1c 100644 --- a/apps/app/src/components/forms/risks/create-risk-form.tsx +++ b/apps/app/src/components/forms/risks/create-risk-form.tsx @@ -2,10 +2,9 @@ import { createRiskAction } from "@/actions/risk/create-risk-action"; import { createRiskSchema } from "@/actions/schema"; -import { useOrganizationAdmins } from "@/app/[locale]/(app)/(dashboard)/[orgId]/hooks/useOrganizationAdmins"; -import { SelectUser } from "@/components/select-user"; +import { SelectAssignee } from "@/app/[locale]/(app)/(dashboard)/[orgId]/components/SelectAssignee"; import { useI18n } from "@/locales/client"; -import type { RiskStatus } from "@comp/db/types"; +import type { Member, RiskStatus, User } from "@comp/db/types"; import { Departments, RiskCategory } from "@comp/db/types"; import { Accordion, @@ -39,13 +38,9 @@ import { useForm } from "react-hook-form"; import { toast } from "sonner"; import type { z } from "zod"; -interface User { - id: string; - image?: string | null; - name: string | null; -} - -export function CreateRisk() { +export function CreateRisk({ + assignees, +}: { assignees: (Member & { user: User })[] }) { const t = useI18n(); // Get the same query parameters as the table @@ -71,7 +66,6 @@ export function CreateRisk() { parse: (value) => value, }); - const { data: admins, isLoading: isLoadingAdmins } = useOrganizationAdmins(); const [_, setCreateRiskSheet] = useQueryState("create-risk-sheet"); const createRisk = useAction(createRiskAction, { @@ -91,6 +85,7 @@ export function CreateRisk() { description: "", category: RiskCategory.operations, department: Departments.admin, + assigneeId: null, }, }); @@ -241,29 +236,18 @@ export function CreateRisk() { /> <FormField control={form.control} - name="ownerId" + name="assigneeId" render={({ field }) => ( <FormItem> <FormLabel>{t("common.assignee.label")}</FormLabel> <FormControl> - <Select - value={field.value} - onValueChange={field.onChange} - > - <SelectTrigger> - <SelectValue - placeholder={t("common.assignee.placeholder")} - /> - </SelectTrigger> - <SelectContent> - <SelectUser - users={admins || []} - isLoading={isLoadingAdmins} - onSelect={field.onChange} - selectedId={field.value} - /> - </SelectContent> - </Select> + <SelectAssignee + assigneeId={field.value} + assignees={assignees} + onAssigneeChange={field.onChange} + disabled={createRisk.status === "executing"} + withTitle={false} + /> </FormControl> <FormMessage /> </FormItem> diff --git a/apps/app/src/components/forms/risks/risk-overview.tsx b/apps/app/src/components/forms/risks/risk-overview.tsx index 6c51241cb7..ade33f5de8 100644 --- a/apps/app/src/components/forms/risks/risk-overview.tsx +++ b/apps/app/src/components/forms/risks/risk-overview.tsx @@ -2,11 +2,13 @@ import { updateRiskAction } from "@/actions/risk/update-risk-action"; import { updateRiskSchema } from "@/actions/schema"; +import { SelectAssignee } from "@/app/[locale]/(app)/(dashboard)/[orgId]/components/SelectAssignee"; import { SelectUser } from "@/components/select-user"; import { STATUS_TYPES, Status, type StatusType } from "@/components/status"; import { useI18n } from "@/locales/client"; import { Departments, + Member, type Risk, RiskCategory, RiskStatus, @@ -38,10 +40,10 @@ import type { z } from "zod"; export function UpdateRiskOverview({ risk, - users, + assignees, }: { risk: Risk; - users: User[]; + assignees: (Member & { user: User })[]; }) { const t = useI18n(); @@ -60,7 +62,7 @@ export function UpdateRiskOverview({ id: risk.id, title: risk.title ?? "", description: risk.description ?? "", - ownerId: risk.ownerId ?? undefined, + assigneeId: risk.assigneeId ?? null, category: risk.category ?? RiskCategory.operations, department: risk.department ?? Departments.admin, status: risk.status ?? RiskStatus.open, @@ -72,7 +74,7 @@ export function UpdateRiskOverview({ id: data.id, title: data.title, description: data.description, - ownerId: data.ownerId, + assigneeId: data.assigneeId, category: data.category, department: data.department, status: data.status, @@ -87,30 +89,18 @@ export function UpdateRiskOverview({ <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <FormField control={form.control} - name="ownerId" + name="assigneeId" render={({ field }) => ( <FormItem> <FormLabel>{t("common.assignee.label")}</FormLabel> <FormControl> - <Select - value={field.value} - onValueChange={field.onChange} - onOpenChange={() => form.handleSubmit(onSubmit)} - > - <SelectTrigger> - <SelectValue - placeholder={t("common.assignee.placeholder")} - /> - </SelectTrigger> - <SelectContent> - <SelectUser - isLoading={false} - onSelect={field.onChange} - selectedId={field.value} - users={users} - /> - </SelectContent> - </Select> + <SelectAssignee + assigneeId={field.value} + assignees={assignees} + onAssigneeChange={field.onChange} + disabled={updateRisk.status === "executing"} + withTitle={false} + /> </FormControl> <FormMessage /> </FormItem> diff --git a/apps/app/src/components/risks/charts/risks-assignee.tsx b/apps/app/src/components/risks/charts/RisksAssignee.tsx similarity index 86% rename from apps/app/src/components/risks/charts/risks-assignee.tsx rename to apps/app/src/components/risks/charts/RisksAssignee.tsx index 995fa413ef..2bc5e482e2 100644 --- a/apps/app/src/components/risks/charts/risks-assignee.tsx +++ b/apps/app/src/components/risks/charts/RisksAssignee.tsx @@ -33,18 +33,24 @@ const riskStatusColors = { export async function RisksAssignee() { const t = await getI18n(); const userStats = await userData(); + const session = await auth.api.getSession({ + headers: await headers(), + }); + + const orgId = session?.session.activeOrganizationId; - const stats: UserRiskStats[] = userStats.map((user) => ({ + const stats: UserRiskStats[] = userStats.map((member) => ({ user: { - id: user.id, - name: user.name, - image: user.image, + id: member.id, + name: member.user.name, + image: member.user.image, }, - totalRisks: user.risk.length, - openRisks: user.risk.filter((risk) => risk.status === "open").length, - pendingRisks: user.risk.filter((risk) => risk.status === "pending").length, - closedRisks: user.risk.filter((risk) => risk.status === "closed").length, - archivedRisks: user.risk.filter((risk) => risk.status === "archived") + totalRisks: member.Risk.length, + openRisks: member.Risk.filter((risk) => risk.status === "open").length, + pendingRisks: member.Risk.filter((risk) => risk.status === "pending") + .length, + closedRisks: member.Risk.filter((risk) => risk.status === "closed").length, + archivedRisks: member.Risk.filter((risk) => risk.status === "archived") .length, })); @@ -60,7 +66,7 @@ export async function RisksAssignee() { <div className="space-y-4"> {stats.map((stat) => ( <Link - href={`/risk/register?ownerId=${stat.user.id}`} + href={`/${orgId}/risk/register?assigneeId=${stat.user.id}`} key={stat.user.id} > <div className="flex items-center gap-4 p-3 rounded-lg hover:bg-muted/50"> @@ -188,24 +194,13 @@ const userData = cache(async () => { return []; } - return await db.user.findMany({ + const members = await db.member.findMany({ where: { - members: { - some: { - organizationId: session.session.activeOrganizationId, - }, - }, - risk: { - some: { - organizationId: session.session.activeOrganizationId, - }, - }, + organizationId: session.session.activeOrganizationId, }, select: { id: true, - name: true, - image: true, - risk: { + Risk: { where: { organizationId: session.session.activeOrganizationId, }, @@ -213,6 +208,14 @@ const userData = cache(async () => { status: true, }, }, + user: { + select: { + name: true, + image: true, + }, + }, }, }); + + return members; }); diff --git a/apps/app/src/components/risks/charts/risks-by-assignee.tsx b/apps/app/src/components/risks/charts/risks-by-assignee.tsx deleted file mode 100644 index b825b47b1e..0000000000 --- a/apps/app/src/components/risks/charts/risks-by-assignee.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import { getInitials } from "@/lib/utils"; -import { getI18n } from "@/locales/server"; -import { db } from "@comp/db"; -import { Avatar, AvatarFallback, AvatarImage } from "@comp/ui/avatar"; -import { Card, CardContent, CardHeader, CardTitle } from "@comp/ui/card"; -import { ScrollArea } from "@comp/ui/scroll-area"; -import Link from "next/link"; -import type { RiskStatus } from "@comp/db/types"; - -interface Props { - organizationId: string; -} - -interface UserRiskStats { - user: { - id: string; - name: string | null; - image: string | null; - }; - totalRisks: number; - openRisks: number; - pendingRisks: number; - closedRisks: number; - archivedRisks: number; -} - -const riskStatusColors = { - open: "bg-yellow-500", - pending: "bg-blue-500", - closed: "bg-green-500", - archived: "bg-gray-500", -}; - -export async function RisksByAssignee({ organizationId }: Props) { - const t = await getI18n(); - - const userStats = await userData(organizationId); - - const stats: UserRiskStats[] = userStats.map((user) => ({ - user: { - id: user.id, - name: user.name, - image: user.image, - }, - totalRisks: user.risk.length, - openRisks: user.risk.filter((risk) => risk.status === "open").length, - pendingRisks: user.risk.filter((risk) => risk.status === "pending").length, - closedRisks: user.risk.filter((risk) => risk.status === "closed").length, - archivedRisks: user.risk.filter((risk) => risk.status === "archived") - .length, - })); - - stats.sort((a, b) => b.totalRisks - a.totalRisks); - - return ( - <Card> - <CardHeader> - <CardTitle>{t("risk.dashboard.risks_by_assignee")}</CardTitle> - </CardHeader> - <CardContent> - <ScrollArea> - <div className="space-y-4"> - {stats.map((stat) => ( - <Link - href={`/${organizationId}/risk/register?ownerId=${stat.user.id}`} - key={stat.user.id} - > - <div className="flex items-center gap-4 p-3 rounded-lg hover:bg-muted/50"> - <Avatar> - <AvatarImage src={stat.user.image || undefined} /> - <AvatarFallback> - {getInitials(stat.user.name || "Unknown User")} - </AvatarFallback> - </Avatar> - - <div className="flex-1 min-w-0"> - <div className="flex items-center justify-between"> - <p className="text-sm font-medium leading-none truncate"> - {stat.user.name || "Unknown User"} - </p> - <span className="text-sm text-muted-foreground"> - {stat.totalRisks} {t("risk.risks")} - </span> - </div> - - <div className="mt-2 w-full bg-muted rounded-full h-2 overflow-hidden"> - {stat.totalRisks > 0 && ( - <div className="h-full flex"> - {stat.openRisks > 0 && ( - <div - className={`${riskStatusColors.open} h-full`} - style={{ - width: `${(stat.openRisks / stat.totalRisks) * 100}%`, - }} - title={`${t("common.status.open")}: ${stat.openRisks}`} - /> - )} - {stat.pendingRisks > 0 && ( - <div - className={`${riskStatusColors.pending} h-full`} - style={{ - width: `${ - (stat.pendingRisks / stat.totalRisks) * 100 - }%`, - }} - title={`${t("common.status.pending")}: ${stat.pendingRisks}`} - /> - )} - {stat.closedRisks > 0 && ( - <div - className={`${riskStatusColors.closed} h-full`} - style={{ - width: `${(stat.closedRisks / stat.totalRisks) * 100}%`, - }} - title={`${t("common.status.closed")}: ${stat.closedRisks}`} - /> - )} - {stat.archivedRisks > 0 && ( - <div - className={`${riskStatusColors.archived} h-full`} - style={{ - width: `${(stat.archivedRisks / stat.totalRisks) * 100}%`, - }} - title={`${t("common.status.archived")}: ${stat.archivedRisks}`} - /> - )} - </div> - )} - </div> - - <div className="mt-1.5 hidden lg:flex items-center gap-3 text-xs text-muted-foreground"> - {stat.openRisks > 0 && ( - <div className="flex items-center gap-1"> - <div - className={`size-2 rounded-full ${riskStatusColors.open}`} - /> - <span> - {t("common.status.open")} ({stat.openRisks}) - </span> - </div> - )} - {stat.pendingRisks > 0 && ( - <div className="flex items-center gap-1"> - <div - className={`size-2 rounded-full ${riskStatusColors.pending}`} - /> - <span> - {t("common.status.pending")} ({stat.pendingRisks}) - </span> - </div> - )} - {stat.closedRisks > 0 && ( - <div className="flex items-center gap-1"> - <div - className={`size-2 rounded-full ${riskStatusColors.closed}`} - /> - <span> - {t("common.status.closed")} ({stat.closedRisks}) - </span> - </div> - )} - {stat.archivedRisks > 0 && ( - <div className="flex items-center gap-1"> - <div - className={`size-2 rounded-full ${riskStatusColors.archived}`} - /> - <span> - {t("common.status.archived")} ({stat.archivedRisks}) - </span> - </div> - )} - </div> - </div> - </div> - </Link> - ))} - </div> - </ScrollArea> - </CardContent> - </Card> - ); -} - -const userData = async (organizationId: string) => { - return await db.user.findMany({ - where: { - members: { - some: { - organizationId, - }, - }, - risk: { - some: { - organizationId, - }, - }, - }, - select: { - id: true, - name: true, - image: true, - risk: { - where: { - organizationId, - }, - select: { - status: true, - }, - }, - }, - }); -}; diff --git a/apps/app/src/components/risks/risk-overview.tsx b/apps/app/src/components/risks/risk-overview.tsx index 87b7bf9cda..a25858a8cd 100644 --- a/apps/app/src/components/risks/risk-overview.tsx +++ b/apps/app/src/components/risks/risk-overview.tsx @@ -1,7 +1,7 @@ "use client"; import { useI18n } from "@/locales/client"; -import type { Risk, User } from "@comp/db/types"; +import type { Member, Risk, User } from "@comp/db/types"; import { Alert, AlertDescription, AlertTitle } from "@comp/ui/alert"; import { Button } from "@comp/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@comp/ui/card"; @@ -13,10 +13,10 @@ import { RiskOverviewSheet } from "../sheets/risk-overview-sheet"; export function RiskOverview({ risk, - users, + assignees, }: { - risk: Risk & { owner: User | null }; - users: User[]; + risk: Risk & { assignee: { user: User } | null }; + assignees: (Member & { user: User })[]; }) { const t = useI18n(); const [open, setOpen] = useQueryState("risk-overview-sheet"); @@ -50,7 +50,7 @@ export function RiskOverview({ </CardTitle> </CardHeader> <CardContent> - <UpdateRiskOverview risk={risk} users={users} /> + <UpdateRiskOverview risk={risk} assignees={assignees} /> </CardContent> </Card> diff --git a/apps/app/src/components/sheets/create-risk-sheet.tsx b/apps/app/src/components/sheets/create-risk-sheet.tsx index 8d9b7de80f..c168cb8aa4 100644 --- a/apps/app/src/components/sheets/create-risk-sheet.tsx +++ b/apps/app/src/components/sheets/create-risk-sheet.tsx @@ -9,8 +9,13 @@ import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@comp/ui/sheet"; import { X } from "lucide-react"; import { useQueryState } from "nuqs"; import { CreateRisk } from "../forms/risks/create-risk-form"; +import { Member, User } from "@comp/db/types"; -export function CreateRiskSheet() { +export function CreateRiskSheet({ + assignees, +}: { + assignees: (Member & { user: User })[]; +}) { const t = useI18n(); const isDesktop = useMediaQuery("(min-width: 768px)"); const [open, setOpen] = useQueryState("create-risk-sheet"); @@ -37,7 +42,7 @@ export function CreateRiskSheet() { </SheetHeader> <ScrollArea className="h-full p-0 pb-[100px]" hideScrollbar> - <CreateRisk /> + <CreateRisk assignees={assignees} /> </ScrollArea> </SheetContent> </Sheet> @@ -48,7 +53,7 @@ export function CreateRiskSheet() { <Drawer open={isOpen} onOpenChange={handleOpenChange}> <DrawerTitle hidden>{t("risk.create")}</DrawerTitle> <DrawerContent className="p-6"> - <CreateRisk /> + <CreateRisk assignees={assignees} /> </DrawerContent> </Drawer> ); diff --git a/apps/app/src/components/tables/risk-tasks/columns.tsx b/apps/app/src/components/tables/risk-tasks/columns.tsx index 38b3e6b6ac..041c9be275 100644 --- a/apps/app/src/components/tables/risk-tasks/columns.tsx +++ b/apps/app/src/components/tables/risk-tasks/columns.tsx @@ -15,8 +15,8 @@ export type RiskTaskType = { title: string; status: RiskStatus; dueDate: string; - ownerId: string; - owner: { + assigneeId: string; + assignee: { image: string; name: string; }; @@ -81,8 +81,8 @@ export function columns(): ColumnDef<RiskTaskType>[] { }, }, { - id: "ownerId", - accessorKey: "ownerId", + id: "assigneeId", + accessorKey: "assigneeId", header: () => ( <span className="hidden sm:table-cell"> {t("common.table.assigned_to")} @@ -92,8 +92,8 @@ export function columns(): ColumnDef<RiskTaskType>[] { return ( <div className="hidden sm:table-cell"> <AssignedUser - fullName={row.original.owner?.name} - avatarUrl={row.original.owner?.image} + fullName={row.original.assignee?.name} + avatarUrl={row.original.assignee?.image} /> </div> ); diff --git a/apps/app/src/components/tables/risk-tasks/filter-toolbar.tsx b/apps/app/src/components/tables/risk-tasks/filter-toolbar.tsx index 982c673e9a..84a1af3f05 100644 --- a/apps/app/src/components/tables/risk-tasks/filter-toolbar.tsx +++ b/apps/app/src/components/tables/risk-tasks/filter-toolbar.tsx @@ -1,25 +1,20 @@ "use client"; import { useI18n } from "@/locales/client"; -import { Button } from "@comp/ui/button"; +import type { Member, User } from "@comp/db/types"; import { cn } from "@comp/ui/cn"; import { Input } from "@comp/ui/input"; import { Skeleton } from "@comp/ui/skeleton"; -import { Search, X } from "lucide-react"; -import { Plus } from "lucide-react"; +import { Search } from "lucide-react"; import { useQueryState } from "nuqs"; import { useTransition } from "react"; -import { useCallback } from "react"; type Props = { isEmpty?: boolean; - users: { - id: string; - name: string | null; - }[]; + assignees: (Member & { user: User })[]; }; -export function FilterToolbar({ isEmpty, users }: Props) { +export function FilterToolbar({ isEmpty, assignees }: Props) { const t = useI18n(); const [isPending, startTransition] = useTransition(); const [open, setOpen] = useQueryState("create-task-sheet"); diff --git a/packages/db/prisma/migrations/20250403194504_risk_assignee/migration.sql b/packages/db/prisma/migrations/20250403194504_risk_assignee/migration.sql new file mode 100644 index 0000000000..01a9205ec9 --- /dev/null +++ b/packages/db/prisma/migrations/20250403194504_risk_assignee/migration.sql @@ -0,0 +1,18 @@ +/* + Warnings: + + - You are about to drop the column `ownerId` on the `Risk` table. All the data in the column will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "Risk" DROP CONSTRAINT "Risk_ownerId_fkey"; + +-- DropIndex +DROP INDEX "Risk_ownerId_idx"; + +-- AlterTable +ALTER TABLE "Risk" DROP COLUMN "ownerId", +ADD COLUMN "assigneeId" TEXT; + +-- AddForeignKey +ALTER TABLE "Risk" ADD CONSTRAINT "Risk_assigneeId_fkey" FOREIGN KEY ("assigneeId") REFERENCES "Member"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/packages/db/prisma/migrations/20250403201319_tasks_use_asignee/migration.sql b/packages/db/prisma/migrations/20250403201319_tasks_use_asignee/migration.sql new file mode 100644 index 0000000000..74820a4232 --- /dev/null +++ b/packages/db/prisma/migrations/20250403201319_tasks_use_asignee/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - You are about to drop the column `userId` on the `Task` table. All the data in the column will be lost. + - Added the required column `assigneeId` to the `Task` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "Task" DROP CONSTRAINT "Task_userId_fkey"; + +-- AlterTable +ALTER TABLE "Task" DROP COLUMN "userId", +ADD COLUMN "assigneeId" TEXT NOT NULL; + +-- AddForeignKey +ALTER TABLE "Task" ADD CONSTRAINT "Task_assigneeId_fkey" FOREIGN KEY ("assigneeId") REFERENCES "Member"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/packages/db/prisma/schema/auth.prisma b/packages/db/prisma/schema/auth.prisma index c2905ae708..0c3753ab5b 100644 --- a/packages/db/prisma/schema/auth.prisma +++ b/packages/db/prisma/schema/auth.prisma @@ -13,10 +13,8 @@ model User { integrationResults IntegrationResult[] invitations Invitation[] members Member[] - risk Risk[] sessions Session[] vendor Vendor[] - tasks Task[] @@unique([email]) } @@ -88,6 +86,8 @@ model Member { EmployeeTrainingVideoCompletion EmployeeTrainingVideoCompletion[] Evidence Evidence[] Policy Policy[] + Risk Risk[] + Task Task[] } model Invitation { diff --git a/packages/db/prisma/schema/risk.prisma b/packages/db/prisma/schema/risk.prisma index fe0b7cadb0..a51666f9ae 100644 --- a/packages/db/prisma/schema/risk.prisma +++ b/packages/db/prisma/schema/risk.prisma @@ -1,4 +1,5 @@ model Risk { + // Metadata id String @id @default(dbgenerated("generate_prefixed_cuid('rsk'::text)")) title String description String @@ -9,17 +10,20 @@ model Risk { impact Impact @default(insignificant) residualLikelihood Likelihood @default(very_unlikely) residualImpact Impact @default(insignificant) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt treatmentStrategyDescription String? treatmentStrategy RiskTreatmentType @default(accept) - organizationId String - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - ownerId String? - owner User? @relation(fields: [ownerId], references: [id]) + + // Dates + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Relationships + organizationId String + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + assigneeId String? + assignee Member? @relation(fields: [assigneeId], references: [id]) @@index([organizationId]) - @@index([ownerId]) @@index([category]) @@index([status]) } diff --git a/packages/db/prisma/schema/task.prisma b/packages/db/prisma/schema/task.prisma index b3a1ede4be..e2d765e5fe 100644 --- a/packages/db/prisma/schema/task.prisma +++ b/packages/db/prisma/schema/task.prisma @@ -1,17 +1,20 @@ model Task { + // Metadata id String @id @default(dbgenerated("generate_prefixed_cuid('tsk'::text)")) title String description String - dueDate DateTime status TaskStatus @default(open) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - relatedId String relatedType TaskType - userId String - user User @relation(fields: [userId], references: [id]) + // Dates + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + dueDate DateTime + + // Relationships + assigneeId String + assignee Member @relation(fields: [assigneeId], references: [id]) organizationId String organization Organization @relation(fields: [organizationId], references: [id])