From 8be69c14630c159543f495ef904573278b018d7b Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Wed, 26 Feb 2025 10:24:19 -0800 Subject: [PATCH 1/2] fix filters spacing --- .../evidence/Actions/getEvidenceTasksStats.ts | 9 +- .../EvidenceFilters/FilterDropdown.tsx | 184 ++++++++++-------- .../evidence/Components/EvidenceList.tsx | 2 +- .../{data-table.tsx => EvidenceListTable.tsx} | 0 .../evidence/Components/data-table/index.ts | 2 +- 5 files changed, 105 insertions(+), 92 deletions(-) rename apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/{data-table.tsx => EvidenceListTable.tsx} (100%) diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Actions/getEvidenceTasksStats.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Actions/getEvidenceTasksStats.ts index 8cfd322777..9f22c4e1cf 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Actions/getEvidenceTasksStats.ts +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Actions/getEvidenceTasksStats.ts @@ -87,7 +87,11 @@ export const getEvidenceTasksStats = authActionClient const isPastDue = nextReviewDate && nextReviewDate < now; - if (!hasContent) { + if (isPublished) { + upToDateCount++; + } + + if (!hasContent && !isPublished) { // No files or links emptyCount++; } else if (!isPublished) { @@ -96,9 +100,6 @@ export const getEvidenceTasksStats = authActionClient } else if (isPastDue) { // Published but needs review needsReviewCount++; - } else { - // Published and up to date - upToDateCount++; } } diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/EvidenceFilters/FilterDropdown.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/EvidenceFilters/FilterDropdown.tsx index 6adfc34e16..0341c7532f 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/EvidenceFilters/FilterDropdown.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/EvidenceFilters/FilterDropdown.tsx @@ -48,107 +48,119 @@ export function FilterDropdown() { )} - - Filter by Status - { - setStatus(status === "published" ? null : "published"); - setPage("1"); // Reset to first page when filtering - }} - > -
- - Published -
-
- { - setStatus(status === "draft" ? null : "draft"); - setPage("1"); // Reset to first page when filtering - }} - > -
- - Draft -
-
- - - - Filter by Frequency - {frequencies.map((freq) => ( - { - setFrequency(frequency === freq ? null : freq); - setPage("1"); // Reset to first page when filtering - }} - > - {freq} - - ))} - - + +
+ {/* Status and Frequency in first column */} +
+ Filter by Status +
+ { + setStatus(status === "published" ? null : "published"); + setPage("1"); // Reset to first page when filtering + }} + > +
+ + Published +
+
+ { + setStatus(status === "draft" ? null : "draft"); + setPage("1"); // Reset to first page when filtering + }} + > +
+ + Draft +
+
+
- Filter by Department - {departments.map((dept) => ( - { - setDepartment(department === dept ? null : dept); - setPage("1"); // Reset to first page when filtering - }} - > -
- - {dept.replace(/_/g, " ").toUpperCase()} + Filter by Frequency +
+ {frequencies.map((freq) => ( + { + setFrequency(frequency === freq ? null : freq); + setPage("1"); // Reset to first page when filtering + }} + > + {freq} + + ))}
- - ))} +
- + {/* Department and Assignee in second column */} +
+ Filter by Department +
+ {departments.map((dept) => ( + { + setDepartment(department === dept ? null : dept); + setPage("1"); // Reset to first page when filtering + }} + > +
+ + {dept.replace(/_/g, " ").toUpperCase()} +
+
+ ))} +
- Filter by Assignee - {assignees.map((assignee) => ( - { - setAssigneeId(assigneeId === assignee.id ? null : assignee.id); - setPage("1"); // Reset to first page when filtering - }} - > -
- - - - {assignee.name ? assignee.name.charAt(0) : "?"} - - - {assignee.name} + Filter by Assignee +
+ {assignees.map((assignee) => ( + { + setAssigneeId( + assigneeId === assignee.id ? null : assignee.id + ); + setPage("1"); // Reset to first page when filtering + }} + > +
+ + + + {assignee.name ? assignee.name.charAt(0) : "?"} + + + {assignee.name} +
+
+ ))}
- - ))} +
+
{hasActiveFilters && ( - <> +
- +
)} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/EvidenceList.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/EvidenceList.tsx index 2290425c14..05abe93635 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/EvidenceList.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/EvidenceList.tsx @@ -1,6 +1,6 @@ "use client"; -import { DataTable } from "./data-table/data-table"; +import { DataTable } from "./data-table/EvidenceListTable"; import { useI18n } from "@/locales/client"; import { SkeletonTable } from "./SkeletonTable"; import { diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/data-table.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/EvidenceListTable.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/data-table.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/EvidenceListTable.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/index.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/index.ts index 3ca442232e..5764381d7d 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/index.ts +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/index.ts @@ -1,3 +1,3 @@ -export { DataTable } from "./data-table"; +export { DataTable } from "./EvidenceListTable"; export { columns } from "./columns"; export type { EvidenceTaskRow } from "./types"; From 58890f1ea313037b392fe2a3e17cc929eb16ce2b Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Wed, 26 Feb 2025 11:39:53 -0800 Subject: [PATCH 2/2] feat(evidence): Add relevance filtering and toggle for evidence tasks - Implement new `relevance` filter in evidence tasks list - Add `isNotRelevant` field to OrganizationEvidence model - Create server action `toggleRelevance` to mark evidence as relevant/not relevant - Update evidence details, filters, and data table to support relevance status - Enhance EvidenceTableProvider with relevance state management - Add relevance column and filter dropdown in evidence tasks view - Improve UI with visual indicators for evidence relevance --- .../Actions/getOrganizationEvidenceTasks.ts | 5 + .../EvidenceFilters/ActiveFilterBadges.tsx | 16 +- .../EvidenceFilters/FilterDropdown.tsx | 45 +- .../Components/data-table/columns.tsx | 30 +- .../evidence/[id]/Actions/toggleRelevance.ts | 77 ++ .../[id]/Components/AssigneeSection.tsx | 114 +- .../[id]/Components/EvidenceDetails.tsx | 18 +- .../evidence/[id]/Components/FileSection.tsx | 42 +- .../evidence/[id]/Components/FileUpload.tsx | 96 +- .../[id]/Components/ReviewSection.tsx | 79 +- .../hooks/useEvidenceTableContext.tsx | 12 +- .../evidence/hooks/useEvidenceTasks.ts | 4 + .../migration.sql | 2 + packages/db/prisma/schema.prisma | 1223 ++++++++--------- 14 files changed, 951 insertions(+), 812 deletions(-) create mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Actions/toggleRelevance.ts create mode 100644 packages/db/prisma/migrations/20250226183500_add_is_relevant_to_evidence/migration.sql diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Actions/getOrganizationEvidenceTasks.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Actions/getOrganizationEvidenceTasks.ts index 42e9cd09ad..171c421879 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Actions/getOrganizationEvidenceTasks.ts +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Actions/getOrganizationEvidenceTasks.ts @@ -28,6 +28,7 @@ export const getOrganizationEvidenceTasks = authActionClient frequency: z.nativeEnum(Frequency).optional().nullable(), department: z.nativeEnum(Departments).optional().nullable(), assigneeId: z.string().optional().nullable(), + relevance: z.enum(["relevant", "not-relevant"]).optional().nullable(), page: z.number().int().positive().optional().default(1), pageSize: z.number().int().positive().optional().default(10), }) @@ -47,6 +48,7 @@ export const getOrganizationEvidenceTasks = authActionClient frequency, department, assigneeId, + relevance, page, pageSize, } = parsedInput; @@ -72,6 +74,9 @@ export const getOrganizationEvidenceTasks = authActionClient ...(department ? { department } : {}), // Assignee filter ...(assigneeId ? { assigneeId } : {}), + // Relevance filter + ...(relevance === "relevant" ? { isNotRelevant: false } : {}), + ...(relevance === "not-relevant" ? { isNotRelevant: true } : {}), // Search filter ...(search ? { diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/EvidenceFilters/ActiveFilterBadges.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/EvidenceFilters/ActiveFilterBadges.tsx index 844b775f58..275ba472c6 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/EvidenceFilters/ActiveFilterBadges.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/EvidenceFilters/ActiveFilterBadges.tsx @@ -14,6 +14,8 @@ export function ActiveFilterBadges() { setDepartment, assigneeId, setAssigneeId, + relevance, + setRelevance, setPage, assignees, } = useEvidenceTable(); @@ -24,7 +26,7 @@ export function ActiveFilterBadges() { : null; // Only render if there are active filters - if (!status && !frequency && !department && !assigneeId) { + if (!status && !frequency && !department && !assigneeId && !relevance) { return null; } @@ -42,6 +44,18 @@ export function ActiveFilterBadges() { /> )} + {relevance && ( + + Relevance: {relevance === "relevant" ? "Relevant" : "Not Relevant"} + { + setRelevance(null); + setPage("1"); + }} + /> + + )} {frequency && ( Frequency: {frequency} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/EvidenceFilters/FilterDropdown.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/EvidenceFilters/FilterDropdown.tsx index 0341c7532f..14779bdcac 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/EvidenceFilters/FilterDropdown.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/EvidenceFilters/FilterDropdown.tsx @@ -10,7 +10,13 @@ import { DropdownMenuTrigger, DropdownMenuCheckboxItem, } from "@bubba/ui/dropdown-menu"; -import { CheckCircle2, Filter, XCircle, Building } from "lucide-react"; +import { + CheckCircle2, + Filter, + XCircle, + Building, + AlertTriangle, +} from "lucide-react"; import { Avatar, AvatarFallback, AvatarImage } from "@bubba/ui/avatar"; import { useEvidenceTable } from "../../hooks/useEvidenceTableContext"; @@ -24,6 +30,8 @@ export function FilterDropdown() { setDepartment, assigneeId, setAssigneeId, + relevance, + setRelevance, setPage, frequencies, departments, @@ -43,14 +51,15 @@ export function FilterDropdown() { {(status ? 1 : 0) + (frequency ? 1 : 0) + (department ? 1 : 0) + - (assigneeId ? 1 : 0)} + (assigneeId ? 1 : 0) + + (relevance ? 1 : 0)} )}
- {/* Status and Frequency in first column */} + {/* Status, Frequency, and Relevance in first column */}
Filter by Status
@@ -80,6 +89,36 @@ export function FilterDropdown() {
+ Filter by Relevance +
+ { + setRelevance(relevance === "relevant" ? null : "relevant"); + setPage("1"); // Reset to first page when filtering + }} + > +
+ + Relevant +
+
+ { + setRelevance( + relevance === "not-relevant" ? null : "not-relevant" + ); + setPage("1"); // Reset to first page when filtering + }} + > +
+ + Not Relevant +
+
+
+ Filter by Frequency
{frequencies.map((freq) => ( diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/columns.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/columns.tsx index 54d84072b6..74811c0133 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/columns.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/columns.tsx @@ -1,7 +1,7 @@ "use client"; import type { ColumnDef } from "@tanstack/react-table"; -import { CheckCircle2, XCircle, Building } from "lucide-react"; +import { CheckCircle2, XCircle, Building, AlertTriangle } from "lucide-react"; import { Tooltip, TooltipContent, @@ -162,4 +162,32 @@ export const columns: ColumnDef[] = [ ); }, }, + { + id: "relevance", + accessorKey: "isNotRelevant", + header: "Relevance", + enableResizing: true, + enableSorting: true, + size: 150, + minSize: 120, + cell: ({ row }) => { + const isNotRelevant = row.original.isNotRelevant; + + if (!isNotRelevant) { + return ( +
+ + Relevant +
+ ); + } + + return ( +
+ + Not Relevant +
+ ); + }, + }, ]; diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Actions/toggleRelevance.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Actions/toggleRelevance.ts new file mode 100644 index 0000000000..3e7d8558e0 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Actions/toggleRelevance.ts @@ -0,0 +1,77 @@ +"use server"; + +import { authActionClient } from "@/actions/safe-action"; +import { db } from "@bubba/db"; +import { z } from "zod"; +import type { ActionResponse } from "@/actions/types"; + +export const toggleRelevance = authActionClient + .schema( + z.object({ + id: z.string(), + isNotRelevant: z.boolean(), + }) + ) + .metadata({ + name: "toggleRelevance", + track: { + event: "toggle-evidence-relevance", + channel: "server", + }, + }) + .action(async ({ ctx, parsedInput }) => { + const { user } = ctx; + const { id, isNotRelevant } = parsedInput; + + if (!user.organizationId) { + return { + success: false, + error: "Not authorized - no organization found", + }; + } + + try { + // Check if the evidence exists + const evidence = await db.organizationEvidence.findFirst({ + where: { + id, + organizationId: user.organizationId, + }, + }); + + if (!evidence) { + return { + success: false, + error: "Evidence not found", + }; + } + + // Update the evidence with the new relevance status + // If marking as not relevant, also unpublish it + const updatedEvidence = await db.organizationEvidence.update({ + where: { + id, + organizationId: user.organizationId, + }, + data: { + isNotRelevant, + // If marking as not relevant, also unpublish it + ...(isNotRelevant === true && { + published: false, + lastPublishedAt: null, + }), + }, + }); + + return { + success: true, + data: updatedEvidence, + }; + } catch (error) { + console.error("Error toggling evidence relevance:", error); + return { + success: false, + error: "Failed to update evidence relevance status", + }; + } + }); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/AssigneeSection.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/AssigneeSection.tsx index 939cb31199..ed428d0565 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/AssigneeSection.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/AssigneeSection.tsx @@ -70,85 +70,67 @@ export function AssigneeSection({ }; if (isLoading) { - return ( -
- Assignee: -
-
- ); + return
; } if (error || !admins) { - return ( -
- Assignee: -

Failed to load

-
- ); + return

Failed to load

; } return ( -
- - Assignee: - - -
- + + {selectedAdmin ? ( +
+ + + {selectedAdmin.name.charAt(0)} + + {selectedAdmin.name} +
+ ) : ( + + )} +
+ - - {selectedAdmin ? ( -
+ +
+ None +
+
+ {admins.map((admin) => ( + +
- - {selectedAdmin.name.charAt(0)} - + {admin.name.charAt(0)} - {selectedAdmin.name} -
- ) : ( - - )} - - - -
- None + {admin.name}
- {admins.map((admin) => ( - -
- - - {admin.name.charAt(0)} - - {admin.name} -
-
- ))} -
- -
+ ))} +
+
); } diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/EvidenceDetails.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/EvidenceDetails.tsx index 1de3df2e38..328b7148b8 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/EvidenceDetails.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/EvidenceDetails.tsx @@ -3,7 +3,7 @@ import { useEffect } from "react"; import { useRouter } from "next/navigation"; import { Button } from "@bubba/ui/button"; -import { ArrowLeft, CheckCircle2, XCircle } from "lucide-react"; +import { ArrowLeft, CheckCircle2, MoreVertical, XCircle } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card"; import { Skeleton } from "@bubba/ui/skeleton"; import { useOrganizationEvidence } from "../hooks/useOrganizationEvidence"; @@ -14,6 +14,13 @@ import { publishEvidence } from "../Actions/publishEvidence"; import { useAction } from "next-safe-action/hooks"; import { toast } from "sonner"; import type { EvidenceDetailsProps } from "../types"; +import { toggleRelevance } from "../Actions/toggleRelevance"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@bubba/ui/dropdown-menu"; export function EvidenceDetails({ id }: EvidenceDetailsProps) { const router = useRouter(); @@ -106,13 +113,22 @@ export function EvidenceDetails({ id }: EvidenceDetailsProps) {
+ {evidence.isNotRelevant && ( +
+ This evidence has been marked as not relevant and will not be included + in compliance reports. +
+ )} + diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/FileSection.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/FileSection.tsx index 8843ef51bd..366a6fb540 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/FileSection.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/FileSection.tsx @@ -6,8 +6,6 @@ import { useFileUpload } from "../hooks/useFileUpload"; import { useFileDelete } from "../hooks/useFileDelete"; import { useFilePreview } from "../hooks/useFilePreview"; import { FileCard } from "./FileCard"; -import { Card, CardContent } from "@bubba/ui/card"; -import { Plus } from "lucide-react"; interface FileSectionProps { evidenceId: string; @@ -44,7 +42,6 @@ export function FileSection({ Record >({}); const [openDialogId, setOpenDialogId] = useState(null); - const [showUploadDropzone, setShowUploadDropzone] = useState(false); const handlePreviewClick = useCallback( async (fileUrl: string) => { @@ -102,37 +99,16 @@ export function FileSection({
- {/* Only show the full dropzone when no files exist or when explicitly shown */} - {(fileUrls.length === 0 || showUploadDropzone) && ( -
- { - handleFileUpload(file); - setShowUploadDropzone(false); - }} - isUploading={isUploading} - /> -
- )} -
- {/* Add Files Card - Always first in the grid */} - {!showUploadDropzone && fileUrls.length > 0 && ( - setShowUploadDropzone(true)} - > - -
- -
-

Add Files

-

- Upload additional evidence files -

-
-
- )} + {/* Add Files Card - Using the card variant of FileUpload */} + { + handleFileUpload(file); + }} + isUploading={isUploading} + variant="card" + cardHeight="h-[220px]" + /> {/* File Cards */} {fileUrls.map((url) => { diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/FileUpload.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/FileUpload.tsx index ee997dcd5b..edb63aac72 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/FileUpload.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/FileUpload.tsx @@ -2,14 +2,29 @@ import { useCallback } from "react"; import { useDropzone } from "react-dropzone"; -import { Cloud, Loader2 } from "lucide-react"; +import { Cloud, Loader2, Plus, Upload } from "lucide-react"; import { cn } from "@bubba/ui/cn"; +import { Card, CardContent } from "@bubba/ui/card"; interface FileUploadProps { onFileSelect: (file: File) => void; isUploading: boolean; accept?: Record; maxSize?: number; + /** + * The variant of the file upload component + * @default "default" + */ + variant?: "default" | "card"; + /** + * The height of the card variant + * @default "h-[220px]" + */ + cardHeight?: string; + /** + * Optional click handler for the card variant + */ + onClick?: (e: React.MouseEvent) => void; } export function FileUpload({ @@ -28,6 +43,9 @@ export function FileUpload({ ], }, maxSize = 10 * 1024 * 1024, // 10MB + variant = "default", + cardHeight = "h-[220px]", + onClick, }: FileUploadProps) { const onDrop = useCallback( (acceptedFiles: File[]) => { @@ -38,14 +56,76 @@ export function FileUpload({ [onFileSelect] ); - const { getRootProps, getInputProps, isDragActive, isDragReject } = - useDropzone({ - onDrop, - accept, - maxSize, - multiple: false, - }); + const { + getRootProps, + getInputProps, + isDragActive, + isDragAccept, + isDragReject, + } = useDropzone({ + onDrop, + accept, + maxSize, + multiple: false, + disabled: isUploading, + }); + // Card variant + if (variant === "card") { + return ( +
+ + + + {isUploading ? ( +
+
+ +
+

Uploading...

+
+ ) : isDragActive ? ( +
+
+ +
+

+ Drop file here +

+

+ Release to upload +

+
+ ) : ( + <> +
+ +
+

Add Files

+

+ Upload additional evidence files +

+

+ Drag & drop or click to upload +

+ + )} +
+
+
+ ); + } + + // Default variant return (
Promise; + id: string; } export function ReviewSection({ @@ -31,28 +38,51 @@ export function ReviewSection({ department, currentAssigneeId, onSuccess, + id, + evidence, }: ReviewSectionProps) { + const { mutate } = useOrganizationEvidence({ id }); const reviewInfo = calculateNextReview(lastPublishedAt, frequency); + const { execute: toggleRelevanceAction, isExecuting: isTogglingRelevance } = + useAction(toggleRelevance, { + onSuccess: () => { + toast.success("Evidence relevance updated successfully"); + mutate(); + }, + onError: () => { + toast.error("Failed to update evidence relevance, please try again."); + }, + }); + return ( - -
- - Evidence Details - - - Manage review frequency, department assignment, and track upcoming - review dates - -
- + + + Evidence Details + + + + Manage review frequency, department assignment, and track upcoming + review dates + - +
@@ -101,6 +131,19 @@ export function ReviewSection({ )}
+
+
+ +

+ ASSIGNEE +

+
+ +
); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/hooks/useEvidenceTableContext.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/hooks/useEvidenceTableContext.tsx index 1e6c69fc29..05ade32e0a 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/hooks/useEvidenceTableContext.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/hooks/useEvidenceTableContext.tsx @@ -20,6 +20,7 @@ interface EvidenceTableContextType { frequency: string | null; department: string | null; assigneeId: string | null; + relevance: string | null; page: string; pageSize: string; @@ -29,6 +30,7 @@ interface EvidenceTableContextType { setFrequency: (value: string | null) => void; setDepartment: (value: string | null) => void; setAssigneeId: (value: string | null) => void; + setRelevance: (value: string | null) => void; setPage: (value: string) => void; setPageSize: (value: string) => void; @@ -66,6 +68,7 @@ export function EvidenceTableProvider({ children }: { children: ReactNode }) { const [frequency, setFrequency] = useQueryState("frequency"); const [department, setDepartment] = useQueryState("department"); const [assigneeId, setAssigneeId] = useQueryState("assigneeId"); + const [relevance, setRelevance] = useQueryState("relevance"); const [page, setPage] = useQueryState("page", { defaultValue: "1" }); const [pageSize, setPageSize] = useQueryState("pageSize", { defaultValue: "10", @@ -86,6 +89,7 @@ export function EvidenceTableProvider({ children }: { children: ReactNode }) { frequency: frequency as any, department: department as any, assigneeId, + relevance: relevance as "relevant" | "not-relevant" | null, page: currentPage, pageSize: currentPageSize, }); @@ -134,9 +138,10 @@ export function EvidenceTableProvider({ children }: { children: ReactNode }) { status !== null || frequency !== null || department !== null || - assigneeId !== null + assigneeId !== null || + relevance !== null ); - }, [status, frequency, department, assigneeId]); + }, [status, frequency, department, assigneeId, relevance]); // Clear all filters const clearFilters = () => { @@ -144,6 +149,7 @@ export function EvidenceTableProvider({ children }: { children: ReactNode }) { setFrequency(null); setDepartment(null); setAssigneeId(null); + setRelevance(null); setPage("1"); // Reset to first page when clearing filters }; @@ -155,6 +161,7 @@ export function EvidenceTableProvider({ children }: { children: ReactNode }) { frequency, department, assigneeId, + relevance, page, pageSize, @@ -164,6 +171,7 @@ export function EvidenceTableProvider({ children }: { children: ReactNode }) { setFrequency, setDepartment, setAssigneeId, + setRelevance, setPage, setPageSize, diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/hooks/useEvidenceTasks.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/hooks/useEvidenceTasks.ts index 36175f1370..e3e78d29d9 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/hooks/useEvidenceTasks.ts +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/hooks/useEvidenceTasks.ts @@ -12,6 +12,7 @@ interface UseOrganizationEvidenceTasksProps { frequency?: Frequency | null; department?: Departments | null; assigneeId?: string | null; + relevance?: "relevant" | "not-relevant" | null; page?: number; pageSize?: number; } @@ -65,6 +66,7 @@ export function useOrganizationEvidenceTasks( frequency, department, assigneeId, + relevance, page = 1, pageSize = 10, } = props; @@ -77,6 +79,7 @@ export function useOrganizationEvidenceTasks( frequency, department, assigneeId, + relevance, page, pageSize, ], @@ -87,6 +90,7 @@ export function useOrganizationEvidenceTasks( frequency, department, assigneeId, + relevance, page, pageSize, }) diff --git a/packages/db/prisma/migrations/20250226183500_add_is_relevant_to_evidence/migration.sql b/packages/db/prisma/migrations/20250226183500_add_is_relevant_to_evidence/migration.sql new file mode 100644 index 0000000000..c58f81d291 --- /dev/null +++ b/packages/db/prisma/migrations/20250226183500_add_is_relevant_to_evidence/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "OrganizationEvidence" ADD COLUMN "isNotRelevant" BOOLEAN NOT NULL DEFAULT false; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 97a92ad47f..32daecfdb6 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -9,41 +9,6 @@ datasource db { url = env("DATABASE_URL") } -enum Tier { - free - pro -} - -enum Role { - member - admin -} - -enum Departments { - none - admin - gov - hr - it - itsm - qms -} - -enum RequirementType { - policy - file - link - procedure - evidence - training -} - -enum Frequency { - monthly - quarterly - yearly -} - model Account { id String @id @default(cuid()) userId String @@ -58,8 +23,7 @@ model Account { id_token String? session_state String? refresh_token_expires_in Int? - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([provider, providerAccountId]) @@index([userId]) @@ -70,99 +34,92 @@ model Session { sessionToken String @unique userId String expires DateTime - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) } model User { - id String @id @default(cuid()) - name String? - full_name String? - email String? @unique - role Role @default(member) - onboarded Boolean @default(false) - emailVerified DateTime? - image String? - lastLogin DateTime? - - organizationId String? - organization Organization? @relation("CurrentOrganization", fields: [organizationId], references: [id], onDelete: SetNull) - + id String @id @default(cuid()) + name String? + full_name String? + email String? @unique + role Role @default(member) + onboarded Boolean @default(false) + emailVerified DateTime? + image String? + lastLogin DateTime? + organizationId String? accounts Account[] - sessions Session[] + Artifact Artifact[] + AuditLog AuditLog[] + Employee Employee[] + AssignedEvidence OrganizationEvidence[] + organizationMemberships OrganizationMember[] Risk Risk[] - RiskMitigationTask RiskMitigationTask[] + RiskAttachment RiskAttachment[] RiskComment RiskComment[] - AuditLog AuditLog[] + RiskMitigationTask RiskMitigationTask[] + sessions Session[] TaskAttachment TaskAttachment[] TaskComments TaskComments[] - Artifact Artifact[] + organization Organization? @relation("CurrentOrganization", fields: [organizationId], references: [id]) VendorComment VendorComment[] VendorMitigationTask VendorMitigationTask[] - VendorTaskComments VendorTaskComments[] + VendorRiskAssessment VendorRiskAssessment[] VendorTaskAttachment VendorTaskAttachment[] + VendorTaskComments VendorTaskComments[] Vendors Vendors[] - VendorRiskAssessment VendorRiskAssessment[] - RiskAttachment RiskAttachment[] - organizationMemberships OrganizationMember[] - Employee Employee[] - AssignedEvidence OrganizationEvidence[] @@index([email]) @@index([organizationId]) } model Organization { - id String @id @default(cuid()) - stripeCustomerId String? - name String - subdomain String @unique - website String - tier Tier @default(free) - policiesCreated Boolean @default(false) - - users User[] @relation("CurrentOrganization") - - Risk Risk[] - AuditLog AuditLog[] - RiskMitigationTask RiskMitigationTask[] - TaskAttachment TaskAttachment[] - TaskComments TaskComments[] - RiskComment RiskComment[] - employees Employee[] - - Framework Framework? @relation(fields: [frameworkId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + stripeCustomerId String? + name String + subdomain String @unique + website String + tier Tier @default(free) + policiesCreated Boolean @default(false) frameworkId String? - OrganizationControl OrganizationControl[] Artifact Artifact[] + AuditLog AuditLog[] + employees Employee[] + Framework Framework? @relation(fields: [frameworkId], references: [id], onDelete: Cascade) + OrganizationCategory OrganizationCategory[] + OrganizationControl OrganizationControl[] + OrganizationEvidence OrganizationEvidence[] OrganizationFramework OrganizationFramework[] OrganizationIntegrations OrganizationIntegrations[] - Vendors Vendors[] + members OrganizationMember[] + OrganizationPolicy OrganizationPolicy[] + Risk Risk[] + RiskAttachment RiskAttachment[] + RiskComment RiskComment[] + RiskMitigationTask RiskMitigationTask[] + TaskAttachment TaskAttachment[] + TaskComments TaskComments[] + users User[] @relation("CurrentOrganization") VendorComment VendorComment[] + VendorContact VendorContact[] VendorMitigationTask VendorMitigationTask[] - VendorTaskComments VendorTaskComments[] - VendorTaskAttachment VendorTaskAttachment[] VendorRiskAssessment VendorRiskAssessment[] - VendorContact VendorContact[] - RiskAttachment RiskAttachment[] - members OrganizationMember[] - OrganizationPolicy OrganizationPolicy[] - OrganizationCategory OrganizationCategory[] + VendorTaskAttachment VendorTaskAttachment[] + VendorTaskComments VendorTaskComments[] + Vendors Vendors[] PortalUser PortalUser[] - OrganizationEvidence OrganizationEvidence[] @@index([stripeCustomerId]) } model OrganizationIntegrations { - id String @id @default(cuid()) - name String @unique + id String @id @default(cuid()) + name String @unique integration_id String settings Json user_settings Json - organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) @@ -170,16 +127,13 @@ model OrganizationIntegrations { } model AuditLog { - id String @id @default(cuid()) - - timestamp DateTime @default(now()) - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - + id String @id @default(cuid()) + timestamp DateTime @default(now()) + userId String organizationId String + data Json organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - - data Json + user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId]) @@index([organizationId]) @@ -193,110 +147,76 @@ model VerificationToken { @@unique([identifier, token]) } -// Frameworks and Controls -enum FrameworkStatus { - not_started - in_progress - compliant - non_compliant -} - -enum ComplianceStatus { - not_started - in_progress - compliant - non_compliant -} - -enum ArtifactType { - policy - evidence - procedure - training -} - model Framework { - id String @id @default(cuid()) - name String @unique // e.g., "GDPR", "SOC 2" - description String? - version String // e.g., "2016/679" for GDPR - + id String @id @default(cuid()) + name String @unique + description String? + version String categories FrameworkCategory[] organizations Organization[] + OrganizationCategory OrganizationCategory[] OrganizationFramework OrganizationFramework[] PolicyFramework PolicyFramework[] - OrganizationCategory OrganizationCategory[] @@index([id]) } model OrganizationFramework { - id String @id @default(cuid()) - organizationId String - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - frameworkId String - framework Framework @relation(fields: [frameworkId], references: [id], onDelete: Cascade) - - status FrameworkStatus @default(not_started) - adoptedAt DateTime @default(now()) - lastAssessed DateTime? - nextAssessment DateTime? - + id String @id @default(cuid()) + organizationId String + frameworkId String + status FrameworkStatus @default(not_started) + adoptedAt DateTime @default(now()) + lastAssessed DateTime? + nextAssessment DateTime? organizationControl OrganizationControl[] + framework Framework @relation(fields: [frameworkId], references: [id], onDelete: Cascade) + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) @@unique([organizationId, frameworkId]) } model FrameworkCategory { - id String @id @default(cuid()) + id String @id @default(cuid()) name String - code String @unique + code String @unique description String? - frameworkId String + controls Control[] framework Framework @relation(fields: [frameworkId], references: [id], onDelete: Cascade) - controls Control[] - @@index([frameworkId]) } model Control { - id String @id @default(cuid()) - code String @unique - name String - description String? - domain String? - + id String @id @default(cuid()) + code String @unique + name String + description String? + domain String? + frameworkCategoryId String? + frameworkCategory FrameworkCategory? @relation(fields: [frameworkCategoryId], references: [id]) + requirements ControlRequirement[] organizationControls OrganizationControl[] PolicyControl PolicyControl[] - // NEW: Requirements associated with this control. - requirements ControlRequirement[] - - frameworkCategoryId String? - frameworkCategory FrameworkCategory? @relation(fields: [frameworkCategoryId], references: [id], onDelete: SetNull) - @@index([frameworkCategoryId]) } model OrganizationControl { - id String @id @default(cuid()) - organizationId String - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - controlId String - control Control @relation(fields: [controlId], references: [id], onDelete: Cascade) - - status ComplianceStatus @default(not_started) - lastReviewDate DateTime? - nextReviewDate DateTime? - - artifacts ControlArtifact[] - OrganizationFramework OrganizationFramework? @relation(fields: [organizationFrameworkId], references: [id]) - organizationFrameworkId String? - OrganizationCategory OrganizationCategory? @relation(fields: [organizationCategoryId], references: [id]) - organizationCategoryId String? - + id String @id @default(cuid()) + organizationId String + controlId String + status ComplianceStatus @default(not_started) + lastReviewDate DateTime? + nextReviewDate DateTime? + organizationFrameworkId String? + organizationCategoryId String? + artifacts ControlArtifact[] + control Control @relation(fields: [controlId], references: [id], onDelete: Cascade) + OrganizationCategory OrganizationCategory? @relation(fields: [organizationCategoryId], references: [id]) + OrganizationFramework OrganizationFramework? @relation(fields: [organizationFrameworkId], references: [id]) + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) OrganizationControlRequirement OrganizationControlRequirement[] @@index([organizationId]) @@ -305,26 +225,21 @@ model OrganizationControl { } model Artifact { - id String @id @default(cuid()) - name String - published Boolean @default(false) - needsReview Boolean @default(true) - - department Departments @default(none) - - type ArtifactType - content Json? - fileUrl String? - version Int @default(1) - lastUpdated DateTime? - + id String @id @default(cuid()) + name String + published Boolean @default(false) + needsReview Boolean @default(true) + department Departments @default(none) + type ArtifactType + content Json? + fileUrl String? + version Int @default(1) + lastUpdated DateTime? organizationId String - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - - ownerId String? - owner User? @relation(fields: [ownerId], references: [id], onDelete: SetNull) - - controls ControlArtifact[] + ownerId String? + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + owner User? @relation(fields: [ownerId], references: [id]) + controls ControlArtifact[] @@index([organizationId]) @@index([type]) @@ -333,84 +248,36 @@ model Artifact { model ControlArtifact { id String @id @default(cuid()) organizationControlId String - organizationControl OrganizationControl @relation(fields: [organizationControlId], references: [id], onDelete: Cascade) artifactId String artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade) + organizationControl OrganizationControl @relation(fields: [organizationControlId], references: [id], onDelete: Cascade) @@index([organizationControlId]) @@index([artifactId]) } -// Risk Register - -enum RiskLevel { - low - medium - high -} - -enum RiskCategory { - customer - governance - operations - other - people - regulatory - reporting - resilience - technology - vendor_management -} - -enum TreatmentType { - accept - avoid - mitigate - transfer -} - -enum RiskStatus { - open - pending - closed - archived -} - -enum RiskTaskStatus { - open - pending - closed -} - -enum RiskAttachmentType { - file - url -} - model Risk { - id String @id @default(cuid()) + id String @id @default(cuid()) title String description String category RiskCategory department Departments? - status RiskStatus @default(open) - probability Int @default(0) - impact Int @default(0) - residual_probability Int @default(0) - residual_impact Int @default(0) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - organizationId String - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - ownerId String? - owner User? @relation(fields: [ownerId], references: [id], onDelete: SetNull) - - mitigationTasks RiskMitigationTask[] - comments RiskComment[] - treatmentStrategy RiskTreatmentStrategy? - TaskComments TaskComments[] - attachments RiskAttachment[] + status RiskStatus @default(open) + probability Int @default(0) + impact Int @default(0) + residual_probability Int @default(0) + residual_impact Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + organizationId String + ownerId String? + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + owner User? @relation(fields: [ownerId], references: [id]) + attachments RiskAttachment[] + comments RiskComment[] + mitigationTasks RiskMitigationTask[] + treatmentStrategy RiskTreatmentStrategy? + TaskComments TaskComments[] @@index([organizationId]) @@index([ownerId]) @@ -419,20 +286,16 @@ model Risk { } model RiskComment { - id String @id @default(cuid()) - - riskId String - risk Risk @relation(fields: [riskId], references: [id], onDelete: Cascade) - - ownerId String - owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) - + id String @id @default(cuid()) + riskId String + ownerId String organizationId String + content String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - - content String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) + risk Risk @relation(fields: [riskId], references: [id], onDelete: Cascade) @@index([riskId]) @@index([ownerId]) @@ -440,26 +303,22 @@ model RiskComment { } model RiskMitigationTask { - id String @id @default(cuid()) - title String - description String - status RiskTaskStatus @default(open) - dueDate DateTime? - notifiedAt DateTime? - completedAt DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - riskId String - risk Risk @relation(fields: [riskId], references: [id], onDelete: Cascade) - ownerId String? - owner User? @relation(fields: [ownerId], references: [id], onDelete: SetNull) - - // Many-to-many relationship through join table - assignments RiskTaskAssignment[] - + id String @id @default(cuid()) + title String + description String + status RiskTaskStatus @default(open) + dueDate DateTime? + notifiedAt DateTime? + completedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + riskId String + ownerId String? organizationId String - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + owner User? @relation(fields: [ownerId], references: [id]) + risk Risk @relation(fields: [riskId], references: [id], onDelete: Cascade) + assignments RiskTaskAssignment[] TaskAttachment TaskAttachment[] TaskComments TaskComments[] @@ -469,45 +328,38 @@ model RiskMitigationTask { } model TaskComments { - id String @id @default(cuid()) - - riskId String - risk Risk @relation(fields: [riskId], references: [id], onDelete: Cascade) - + id String @id @default(cuid()) + riskId String riskMitigationTaskId String + ownerId String + organizationId String + content String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) + risk Risk @relation(fields: [riskId], references: [id], onDelete: Cascade) riskMitigationTask RiskMitigationTask @relation(fields: [riskMitigationTaskId], references: [id], onDelete: Cascade) - ownerId String - owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) - - organizationId String - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - - content String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - @@index([riskId]) @@index([riskMitigationTaskId]) @@index([organizationId]) } model TaskAttachment { - id String @id @default(cuid()) - + id String @id @default(cuid()) riskMitigationTaskId String + name String + fileUrl String + fileKey String? + type RiskAttachmentType @default(file) + uploadedAt DateTime @default(now()) + ownerId String? + organizationId String + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + owner User? @relation(fields: [ownerId], references: [id]) riskMitigationTask RiskMitigationTask @relation(fields: [riskMitigationTaskId], references: [id], onDelete: Cascade) - name String - fileUrl String - fileKey String? - type RiskAttachmentType @default(file) - uploadedAt DateTime @default(now()) - ownerId String? - owner User? @relation(fields: [ownerId], references: [id], onDelete: SetNull) - organizationId String - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - @@index([riskMitigationTaskId]) @@index([organizationId]) } @@ -518,107 +370,65 @@ model RiskTreatmentStrategy { description String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - - riskId String @unique - risk Risk @relation(fields: [riskId], references: [id], onDelete: Cascade) + riskId String @unique + risk Risk @relation(fields: [riskId], references: [id], onDelete: Cascade) @@index([riskId]) } -enum VendorCategory { - cloud - infrastructure - software_as_a_service - finance - marketing - sales - hr - other -} +model Vendors { + id String @id @default(cuid()) + name String + website String + description String + category VendorCategory + status VendorStatus @default(not_assessed) + inherent_risk VendorInherentRisk @default(unknown) + residual_risk VendorResidualRisk @default(unknown) + lastAssessed DateTime? + nextAssessment DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + ownerId String? + organizationId String + VendorComment VendorComment[] + VendorContact VendorContact[] + VendorMitigationTask VendorMitigationTask[] + VendorRiskAssessment VendorRiskAssessment[] + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + owner User? @relation(fields: [ownerId], references: [id]) -enum VendorStatus { - not_assessed - in_progress - assessed -} - -enum VendorInherentRisk { - low - medium - high - unknown -} - -enum VendorResidualRisk { - low - medium - high - unknown -} - -model Vendors { - id String @id @default(cuid()) - name String - website String - description String - category VendorCategory - status VendorStatus @default(not_assessed) - inherent_risk VendorInherentRisk @default(unknown) - residual_risk VendorResidualRisk @default(unknown) - - lastAssessed DateTime? - nextAssessment DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - ownerId String? - owner User? @relation(fields: [ownerId], references: [id], onDelete: SetNull) - organizationId String - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - - VendorComment VendorComment[] - VendorMitigationTask VendorMitigationTask[] - VendorRiskAssessment VendorRiskAssessment[] - VendorContact VendorContact[] - - @@index([organizationId]) - @@index([ownerId]) - @@index([category]) - @@index([status]) + @@index([organizationId]) + @@index([ownerId]) + @@index([category]) + @@index([status]) } model VendorContact { - id String @id @default(cuid()) - - vendorId String - vendor Vendors @relation(fields: [vendorId], references: [id], onDelete: Cascade) - - name String - email String - role String - + id String @id @default(cuid()) + vendorId String + name String + email String + role String organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + vendor Vendors @relation(fields: [vendorId], references: [id], onDelete: Cascade) @@index([vendorId]) @@index([organizationId]) } model VendorComment { - id String @id @default(cuid()) - - vendorId String - vendor Vendors @relation(fields: [vendorId], references: [id], onDelete: Cascade) - - ownerId String - owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) - + id String @id @default(cuid()) + vendorId String + ownerId String organizationId String + content String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - - content String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) + vendor Vendors @relation(fields: [vendorId], references: [id], onDelete: Cascade) @@index([vendorId]) @@index([ownerId]) @@ -626,26 +436,21 @@ model VendorComment { } model VendorMitigationTask { - id String @id @default(cuid()) - title String - description String - status RiskTaskStatus @default(open) - dueDate DateTime? - completedAt DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - ownerId String? - owner User? @relation(fields: [ownerId], references: [id], onDelete: SetNull) - vendorId String - vendor Vendors @relation(fields: [vendorId], references: [id], onDelete: Cascade) - - // Many-to-many relationship through join table - assignments VendorTaskAssignment[] - - organizationId String - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - + id String @id @default(cuid()) + title String + description String + status RiskTaskStatus @default(open) + dueDate DateTime? + completedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + ownerId String? + vendorId String + organizationId String + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + owner User? @relation(fields: [ownerId], references: [id]) + vendor Vendors @relation(fields: [vendorId], references: [id], onDelete: Cascade) + assignments VendorTaskAssignment[] VendorTaskAttachment VendorTaskAttachment[] VendorTaskComments VendorTaskComments[] @@ -655,196 +460,158 @@ model VendorMitigationTask { } model VendorTaskComments { - id String @id @default(cuid()) - + id String @id @default(cuid()) vendorMitigationTaskId String + ownerId String + organizationId String + content String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) vendorMitigationTask VendorMitigationTask @relation(fields: [vendorMitigationTaskId], references: [id], onDelete: Cascade) - ownerId String - owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) - - organizationId String - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - - content String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - @@index([vendorMitigationTaskId]) @@index([organizationId]) } model VendorTaskAttachment { - id String @id @default(cuid()) - + id String @id @default(cuid()) vendorMitigationTaskId String + name String + fileUrl String + fileKey String? + type RiskAttachmentType @default(file) + uploadedAt DateTime @default(now()) + ownerId String? + organizationId String + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + owner User? @relation(fields: [ownerId], references: [id]) vendorMitigationTask VendorMitigationTask @relation(fields: [vendorMitigationTaskId], references: [id], onDelete: Cascade) - name String - fileUrl String - fileKey String? - type RiskAttachmentType @default(file) - uploadedAt DateTime @default(now()) - ownerId String? - owner User? @relation(fields: [ownerId], references: [id], onDelete: SetNull) - organizationId String - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - @@index([vendorMitigationTaskId]) @@index([organizationId]) } model VendorRiskAssessment { - id String @id @default(cuid()) - - vendorId String - vendor Vendors @relation(fields: [vendorId], references: [id], onDelete: Cascade) - - ownerId String? - owner User? @relation(fields: [ownerId], references: [id], onDelete: SetNull) - + id String @id @default(cuid()) + vendorId String + ownerId String? organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + owner User? @relation(fields: [ownerId], references: [id]) + vendor Vendors @relation(fields: [vendorId], references: [id], onDelete: Cascade) @@index([vendorId]) @@index([organizationId]) } model RiskAttachment { - id String @id @default(cuid()) - - riskId String - risk Risk @relation(fields: [riskId], references: [id], onDelete: Cascade) - + id String @id @default(cuid()) + riskId String name String fileUrl String fileKey String? type RiskAttachmentType @default(file) uploadedAt DateTime @default(now()) ownerId String? - owner User? @relation(fields: [ownerId], references: [id], onDelete: SetNull) organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + owner User? @relation(fields: [ownerId], references: [id]) + risk Risk @relation(fields: [riskId], references: [id], onDelete: Cascade) @@index([riskId]) @@index([organizationId]) } -enum MembershipRole { - owner - admin - member - viewer -} - model OrganizationMember { - id String @id @default(cuid()) - role MembershipRole @default(member) - invitedEmail String? - accepted Boolean @default(false) - department Departments @default(none) - joinedAt DateTime @default(now()) - lastActive DateTime? - - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - + id String @id @default(cuid()) + role MembershipRole @default(member) + invitedEmail String? + accepted Boolean @default(false) + department Departments @default(none) + joinedAt DateTime @default(now()) + lastActive DateTime? + userId String organizationId String - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([userId, organizationId]) @@index([organizationId]) @@index([userId]) } -// Join table for risk task assignments model RiskTaskAssignment { - id String @id @default(cuid()) - assignedAt DateTime @default(now()) - - taskId String - task RiskMitigationTask @relation(fields: [taskId], references: [id], onDelete: Cascade) - + id String @id @default(cuid()) + assignedAt DateTime @default(now()) + taskId String employeeId String - employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) + employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) + task RiskMitigationTask @relation(fields: [taskId], references: [id], onDelete: Cascade) @@unique([taskId, employeeId]) @@index([taskId]) @@index([employeeId]) } -// Join table for vendor task assignments model VendorTaskAssignment { - id String @id @default(cuid()) - assignedAt DateTime @default(now()) - - taskId String - task VendorMitigationTask @relation(fields: [taskId], references: [id], onDelete: Cascade) - + id String @id @default(cuid()) + assignedAt DateTime @default(now()) + taskId String employeeId String - employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) + employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) + task VendorMitigationTask @relation(fields: [taskId], references: [id], onDelete: Cascade) @@unique([taskId, employeeId]) @@index([taskId]) @@index([employeeId]) } -enum EmployeeTaskStatus { - assigned - in_progress - completed - overdue -} - model EmployeeRequiredTask { - id String @id @default(cuid()) - code String @unique // e.g. "SEC-TRAINING", "NDA-SIGN" - name String // e.g. "Security Awareness Training" - description String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - + id String @id @default(cuid()) + code String @unique + name String + description String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt employeeTasks EmployeeTask[] } model EmployeeTask { id String @id @default(cuid()) employeeId String - employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) requiredTaskId String - requiredTask EmployeeRequiredTask @relation(fields: [requiredTaskId], references: [id], onDelete: Cascade) status EmployeeTaskStatus @default(assigned) completedAt DateTime? overrideCompliant Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) + requiredTask EmployeeRequiredTask @relation(fields: [requiredTaskId], references: [id], onDelete: Cascade) @@unique([employeeId, requiredTaskId]) } model Employee { - id String @id @default(cuid()) - name String - email String - department Departments @default(none) - isActive Boolean @default(true) - externalEmployeeId String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - organizationId String - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - - userId String? - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - - linkId String? - link PortalUser? @relation(fields: [linkId], references: [id], onDelete: SetNull) - + id String @id @default(cuid()) + name String + email String + department Departments @default(none) + isActive Boolean @default(true) + externalEmployeeId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + organizationId String + userId String? + linkId String? + link PortalUser? @relation(fields: [linkId], references: [id]) + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [id]) + EmployeePolicyAcceptance EmployeePolicyAcceptance[] employeeTasks EmployeeTask[] RiskTaskAssignment RiskTaskAssignment[] VendorTaskAssignment VendorTaskAssignment[] - EmployeePolicyAcceptance EmployeePolicyAcceptance[] @@unique([email, organizationId]) @@index([organizationId]) @@ -852,27 +619,23 @@ model Employee { @@index([department]) } -// --- Policy Templates and Files --- - model Policy { - id String @id @default(cuid()) - slug String @unique - name String - description String? - content Json[] - version String? - usedBy Json - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - frequency Frequency? - - // Relations - policyFrameworks PolicyFramework[] - employeeAcceptances EmployeePolicyAcceptance[] - policyFiles PolicyFile[] - PolicyControl PolicyControl[] + id String @id @default(cuid()) + slug String @unique + name String + description String? + content Json[] + version String? + usedBy Json + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + frequency Frequency? ControlRequirement ControlRequirement[] + employeeAcceptances EmployeePolicyAcceptance[] OrganizationPolicy OrganizationPolicy[] + PolicyControl PolicyControl[] + policyFiles PolicyFile[] + policyFrameworks PolicyFramework[] } model PolicyFile { @@ -883,25 +646,19 @@ model PolicyFile { fileType String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - - policy Policy @relation(fields: [policyId], references: [id], onDelete: Cascade) + policy Policy @relation(fields: [policyId], references: [id], onDelete: Cascade) } -// --- Policy to Framework Mapping --- - model PolicyFramework { - id String @id @default(cuid()) + id String @id @default(cuid()) policyId String frameworkId String - - policy Policy @relation(fields: [policyId], references: [id], onDelete: Cascade) - framework Framework @relation(fields: [frameworkId], references: [id], onDelete: Cascade) + framework Framework @relation(fields: [frameworkId], references: [id], onDelete: Cascade) + policy Policy @relation(fields: [policyId], references: [id], onDelete: Cascade) @@unique([policyId, frameworkId]) } -// --- Employee Acceptance Tracking --- - model EmployeePolicyAcceptance { id String @id @default(cuid()) employeeId String @@ -910,98 +667,75 @@ model EmployeePolicyAcceptance { acceptedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - - employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) - policy Policy @relation(fields: [policyId], references: [id], onDelete: Cascade) + employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) + policy Policy @relation(fields: [policyId], references: [id], onDelete: Cascade) @@unique([employeeId, policyId]) } model PolicyControl { - id String @id @default(cuid()) + id String @id @default(cuid()) policyId String controlId String - - policy Policy @relation(fields: [policyId], references: [id], onDelete: Cascade) - control Control @relation(fields: [controlId], references: [id], onDelete: Cascade) + control Control @relation(fields: [controlId], references: [id], onDelete: Cascade) + policy Policy @relation(fields: [policyId], references: [id], onDelete: Cascade) @@unique([policyId, controlId]) } model ControlRequirement { - id String @id @default(cuid()) - name String @default("") - type RequirementType - description String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - frequency Frequency? - - controlId String - control Control @relation(fields: [controlId], references: [id], onDelete: Cascade) - - policyId String? - policy Policy? @relation(fields: [policyId], references: [id], onDelete: SetNull) - - evidenceId String? - evidence Evidence? @relation(fields: [evidenceId], references: [id], onDelete: SetNull) - + id String @id @default(cuid()) + controlId String + type RequirementType + description String? + policyId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + name String @default("") + evidenceId String? + frequency Frequency? + control Control @relation(fields: [controlId], references: [id], onDelete: Cascade) + evidence Evidence? @relation(fields: [evidenceId], references: [id]) + policy Policy? @relation(fields: [policyId], references: [id]) OrganizationControlRequirement OrganizationControlRequirement[] @@index([controlId]) } model OrganizationControlRequirement { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - type RequirementType - description String? - - published Boolean @default(false) - content Json? - fileUrl String? - - organizationControlId String - organizationControl OrganizationControl @relation(fields: [organizationControlId], references: [id], onDelete: Cascade) - - controlRequirementId String - controlRequirement ControlRequirement @relation(fields: [controlRequirementId], references: [id], onDelete: Cascade) - - organizationPolicyId String? - organizationPolicy OrganizationPolicy? @relation(fields: [organizationPolicyId], references: [id], onDelete: Cascade) - + id String @id @default(cuid()) + organizationControlId String + controlRequirementId String + content Json? + createdAt DateTime @default(now()) + description String? + fileUrl String? + published Boolean @default(false) + type RequirementType + updatedAt DateTime @updatedAt + organizationPolicyId String? organizationEvidenceId String? + controlRequirement ControlRequirement @relation(fields: [controlRequirementId], references: [id], onDelete: Cascade) + organizationControl OrganizationControl @relation(fields: [organizationControlId], references: [id], onDelete: Cascade) organizationEvidence OrganizationEvidence? @relation(fields: [organizationEvidenceId], references: [id], onDelete: Cascade) + organizationPolicy OrganizationPolicy? @relation(fields: [organizationPolicyId], references: [id], onDelete: Cascade) @@unique([organizationControlId, controlRequirementId]) } -enum PolicyStatus { - draft - published - archived -} - model OrganizationPolicy { - id String @id @default(cuid()) - organizationId String - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - - status PolicyStatus @default(draft) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - lastPublishedAt DateTime? - - frequency Frequency? - - policyId String - policy Policy @relation(fields: [policyId], references: [id], onDelete: Cascade) - + id String @id @default(cuid()) + organizationId String + status PolicyStatus @default(draft) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + policyId String content Json[] + frequency Frequency? + lastPublishedAt DateTime? OrganizationControlRequirement OrganizationControlRequirement[] + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + policy Policy @relation(fields: [policyId], references: [id], onDelete: Cascade) @@unique([organizationId, policyId]) @@index([organizationId]) @@ -1009,96 +743,77 @@ model OrganizationPolicy { } model OrganizationCategory { - id String @id @default(cuid()) - name String - description String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - frameworkId String - framework Framework @relation(fields: [frameworkId], references: [id], onDelete: Cascade) - + id String @id @unique @default(cuid()) + name String + description String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + organizationId String + frameworkId String + framework Framework @relation(fields: [frameworkId], references: [id], onDelete: Cascade) + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) organizationControl OrganizationControl[] - organizationId String - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - - @@unique([id]) @@index([organizationId]) } -// Evidence metadata model Evidence { - id String @id @default(cuid()) - name String - description String? - frequency Frequency? - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - + id String @id @unique @default(cuid()) + name String + description String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + frequency Frequency? ControlRequirement ControlRequirement[] OrganizationEvidence OrganizationEvidence[] - - @@unique([id]) } -// Organization evidence, specific to an organization model OrganizationEvidence { - id String @id @default(cuid()) - name String - description String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - published Boolean @default(false) - lastPublishedAt DateTime? - - frequency Frequency? - department Departments? @default(none) - - fileUrls String[] - additionalUrls String[] - - organizationId String - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - + id String @id @unique @default(cuid()) + name String + description String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + published Boolean @default(false) + organizationId String + additionalUrls String[] evidenceId String - evidence Evidence @relation(fields: [evidenceId], references: [id], onDelete: Cascade) + fileUrls String[] + frequency Frequency? + lastPublishedAt DateTime? + assigneeId String? + department Departments? @default(none) + isNotRelevant Boolean @default(false) OrganizationControlRequirement OrganizationControlRequirement[] + assignee User? @relation(fields: [assigneeId], references: [id]) + evidence Evidence @relation(fields: [evidenceId], references: [id], onDelete: Cascade) + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - assigneeId String? - assignee User? @relation(fields: [assigneeId], references: [id], onDelete: SetNull) - - @@unique([id]) @@index([organizationId]) @@index([assigneeId]) } -// Employee Portal - model PortalUser { id String @id name String - email String + email String @unique emailVerified Boolean image String? createdAt DateTime updatedAt DateTime - sessions PortalSession[] - accounts PortalAccount[] - employees Employee[] organizationId String? + employees Employee[] + accounts PortalAccount[] + sessions PortalSession[] organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade) - @@unique([email]) @@map("portal_user") } model PortalSession { id String @id expiresAt DateTime - token String + token String @unique createdAt DateTime updatedAt DateTime ipAddress String? @@ -1106,7 +821,6 @@ model PortalSession { userId String user PortalUser @relation(fields: [userId], references: [id], onDelete: Cascade) - @@unique([token]) @@map("portal_session") } @@ -1115,7 +829,6 @@ model PortalAccount { accountId String providerId String userId String - user PortalUser @relation(fields: [userId], references: [id], onDelete: Cascade) accessToken String? refreshToken String? idToken String? @@ -1125,6 +838,7 @@ model PortalAccount { password String? createdAt DateTime updatedAt DateTime + user PortalUser @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("portal_account") } @@ -1139,3 +853,154 @@ model PortalVerification { @@map("portal_verification") } + +enum Tier { + free + pro +} + +enum Role { + member + admin +} + +enum Departments { + none + admin + gov + hr + it + itsm + qms +} + +enum RequirementType { + policy + file + link + procedure + evidence + training +} + +enum Frequency { + monthly + quarterly + yearly +} + +enum FrameworkStatus { + not_started + in_progress + compliant + non_compliant +} + +enum ComplianceStatus { + not_started + in_progress + compliant + non_compliant +} + +enum ArtifactType { + policy + evidence + procedure + training +} + +enum RiskLevel { + low + medium + high +} + +enum RiskCategory { + customer + governance + operations + other + people + regulatory + reporting + resilience + technology + vendor_management +} + +enum TreatmentType { + accept + avoid + mitigate + transfer +} + +enum RiskStatus { + open + pending + closed + archived +} + +enum RiskTaskStatus { + open + pending + closed +} + +enum RiskAttachmentType { + file + url +} + +enum VendorCategory { + cloud + infrastructure + software_as_a_service + finance + marketing + sales + hr + other +} + +enum VendorStatus { + not_assessed + in_progress + assessed +} + +enum VendorInherentRisk { + low + medium + high + unknown +} + +enum VendorResidualRisk { + low + medium + high + unknown +} + +enum MembershipRole { + owner + admin + member + viewer +} + +enum EmployeeTaskStatus { + assigned + in_progress + completed + overdue +} + +enum PolicyStatus { + draft + published + archived +}