From 005d2763c1161c14b6cc69349f6757d4c25f1b71 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Tue, 18 Mar 2025 11:53:10 -0400 Subject: [PATCH] Refactor evidence overview: implement loading state, replace useEvidenceDashboard hook with direct data fetching, and enhance error handling. Introduce new data structure for evidence dashboard and remove obsolete action file. --- .../overview/actions/getEvidenceDashboard.ts | 243 ------------------ .../AssigneeChart/AssigneeBarChart.tsx | 3 +- .../AssigneeChart/AssigneeChartRow.tsx | 8 +- .../DepartmentChart/DepartmentBarChart.tsx | 3 +- .../overview/components/EvidenceOverview.tsx | 45 ++-- .../FrameworkChart/FrameworkBarChart.tsx | 5 +- .../overview/data/getEvidenceDashboard.ts | 218 ++++++++++++++++ .../overview/hooks/useAssigneeData.ts | 2 +- .../overview/hooks/useEvidenceDashboard.ts | 57 ---- .../overview/hooks/useEvidenceData.ts | 2 +- .../overview/hooks/useFrameworkData.ts | 2 +- .../(dashboard)/evidence/overview/loading.tsx | 5 + .../(dashboard)/evidence/overview/page.tsx | 26 +- apps/app/src/lib/errors.ts | 17 ++ yarn.lock | 6 +- 15 files changed, 294 insertions(+), 348 deletions(-) delete mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/actions/getEvidenceDashboard.ts create mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/data/getEvidenceDashboard.ts delete mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/hooks/useEvidenceDashboard.ts create mode 100644 apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/loading.tsx create mode 100644 apps/app/src/lib/errors.ts diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/actions/getEvidenceDashboard.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/actions/getEvidenceDashboard.ts deleted file mode 100644 index aaaa1619c9..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/actions/getEvidenceDashboard.ts +++ /dev/null @@ -1,243 +0,0 @@ -"use server"; - -import { authActionClient } from "@/actions/safe-action"; -import { db } from "@bubba/db"; -import type { Departments, OrganizationEvidence } from "@prisma/client"; - -export type EvidenceStatus = "empty" | "draft" | "needsReview" | "upToDate"; - -export interface EvidenceWithStatus - extends Omit { - status: EvidenceStatus; - assigneeEmail?: string; -} - -export interface EvidenceDashboardData { - byDepartment: Record; - byAssignee: Record; - byFramework: Record; - unassigned: EvidenceWithStatus[]; - statusCounts: { - totalCount: number; - emptyCount: number; - draftCount: number; - needsReviewCount: number; - upToDateCount: number; - }; -} - -export const getEvidenceDashboard = authActionClient - .metadata({ - name: "getEvidenceDashboard", - track: { - event: "get-evidence-dashboard", - channel: "server", - }, - }) - .action(async ({ ctx }) => { - const { user } = ctx; - - if (!user.organizationId) { - return { - success: false, - error: "Not authorized - no organization found", - }; - } - - try { - const evidence = await db.organizationEvidence.findMany({ - where: { - organizationId: user.organizationId, - }, - include: { - assignee: true, - }, - }); - - if (!evidence) { - return { - success: false, - error: "Evidence not found", - }; - } - - // Calculate status for each evidence item - const now = new Date(); - let emptyCount = 0; - let draftCount = 0; - let needsReviewCount = 0; - let upToDateCount = 0; - const totalCount = evidence.length; - - const evidenceWithStatus: EvidenceWithStatus[] = evidence.map((item) => { - // Check if task has files or links - const hasContent = - item.fileUrls.length > 0 || item.additionalUrls.length > 0; - - // Check if task is published - const isPublished = item.published; - - // Check if task needs review (published and next review date is in the past) - let nextReviewDate = null; - - if (item.lastPublishedAt && item.frequency) { - // Calculate next review date based on last published date and frequency - const lastPublished = new Date(item.lastPublishedAt); - nextReviewDate = new Date(lastPublished); - - switch (item.frequency) { - case "monthly": - nextReviewDate.setMonth(nextReviewDate.getMonth() + 1); - break; - case "quarterly": - nextReviewDate.setMonth(nextReviewDate.getMonth() + 3); - break; - case "yearly": - nextReviewDate.setFullYear(nextReviewDate.getFullYear() + 1); - break; - } - } - - const isPastDue = nextReviewDate && nextReviewDate < now; - - let status: EvidenceStatus; - - if (!hasContent && !isPublished) { - // No files or links - status = "empty"; - emptyCount++; - } else if (!isPublished) { - // Has content but not published - status = "draft"; - draftCount++; - } else if (isPastDue) { - // Published but needs review - status = "needsReview"; - needsReviewCount++; - } else { - // Published and up to date - status = "upToDate"; - upToDateCount++; - } - - // Extract assignee email if available - const assigneeEmail = item.assignee?.email || undefined; - - // Create a new object without the assignee property - const { assignee, ...evidenceWithoutAssignee } = item; - - return { - ...evidenceWithoutAssignee, - status, - assigneeEmail, - }; - }); - - // Initialize with all department values from the enum - const departmentValues: Departments[] = [ - "none", - "admin", - "gov", - "hr", - "it", - "itsm", - "qms", - ]; - - // Initialize the byDepartment object with empty arrays for all departments - const initialByDepartment = departmentValues.reduce< - Record - >( - (acc, dept) => { - acc[dept] = []; - return acc; - }, - {} as Record - ); - - // Group evidence by department - const byDepartment = evidenceWithStatus.reduce< - Record - >((acc, curr) => { - // Use the department from the evidence, or default to 'none' if it's null/undefined - const department = curr.department || "none"; - - // Add the evidence to the appropriate department array - acc[department].push(curr); - return acc; - }, initialByDepartment); - - // Collect unassigned evidence - const unassigned = evidenceWithStatus.filter((item) => !item.assigneeId); - - // Group evidence by assignee - const byAssignee = evidenceWithStatus.reduce< - Record - >((acc, curr) => { - if (!curr.assigneeId || !curr.assigneeEmail) { - return acc; - } - - const email = curr.assigneeEmail; - if (!acc[email]) { - acc[email] = []; - } - acc[email].push(curr); - return acc; - }, {}); - - // Sample the first assignee's data - const firstAssigneeEmail = Object.keys(byAssignee)[0]; - if (firstAssigneeEmail) { - const firstAssigneeItems = byAssignee[firstAssigneeEmail]; - // Count items by status for the first assignee - const statusCounts = { - empty: 0, - draft: 0, - needsReview: 0, - upToDate: 0, - }; - - for (const item of firstAssigneeItems) { - statusCounts[item.status]++; - } - } - - const byFramework = evidenceWithStatus.reduce< - Record - >((acc, curr) => { - const frameworkId = curr.frameworkId; - if (!frameworkId) { - return acc; - } - - if (!acc[frameworkId]) { - acc[frameworkId] = []; - } - acc[frameworkId].push(curr); - return acc; - }, {}); - - return { - success: true, - data: { - byDepartment, - byAssignee, - byFramework, - unassigned, - statusCounts: { - totalCount, - emptyCount, - draftCount, - needsReviewCount, - upToDateCount, - }, - }, - }; - } catch (error) { - return { - success: false, - error: "Failed to fetch evidence", - }; - } - }); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/components/AssigneeChart/AssigneeBarChart.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/components/AssigneeChart/AssigneeBarChart.tsx index 312714f530..2ffca97a9d 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/components/AssigneeChart/AssigneeBarChart.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/components/AssigneeChart/AssigneeBarChart.tsx @@ -1,11 +1,10 @@ "use client"; -import React from "react"; -import type { EvidenceWithStatus } from "../../actions/getEvidenceDashboard"; import { ChevronDown, ChevronUp } from "lucide-react"; import { useScrollPosition } from "../../hooks/useScrollPosition"; import { useAssigneeData } from "../../hooks/useAssigneeData"; import { AssigneeChartRow } from "./AssigneeChartRow"; +import type { EvidenceWithStatus } from "../../data/getEvidenceDashboard"; interface AssigneeBarChartProps { byAssignee?: Record; diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/components/AssigneeChart/AssigneeChartRow.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/components/AssigneeChart/AssigneeChartRow.tsx index 4cbc280ff3..2f9737bf06 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/components/AssigneeChart/AssigneeChartRow.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/components/AssigneeChart/AssigneeChartRow.tsx @@ -1,16 +1,14 @@ "use client"; -import React from "react"; -import { HorizontalBarChart, type ChartDataItem } from "../HorizontalBarChart"; import { useI18n } from "@/locales/client"; -import type { AssigneeData } from "../../hooks/useAssigneeData"; import { - STATUS_PRIORITY, STATUS_COLORS, STATUS_HEX_COLORS, - type StatusType, + STATUS_PRIORITY, } from "../../constants/evidence-status"; +import type { AssigneeData } from "../../hooks/useAssigneeData"; import { useStatusTranslation } from "../../hooks/useStatusTranslation"; +import { HorizontalBarChart, type ChartDataItem } from "../HorizontalBarChart"; interface AssigneeChartRowProps { assignee: AssigneeData; diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/components/DepartmentChart/DepartmentBarChart.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/components/DepartmentChart/DepartmentBarChart.tsx index c91ed7b7bf..17e38197de 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/components/DepartmentChart/DepartmentBarChart.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/components/DepartmentChart/DepartmentBarChart.tsx @@ -1,8 +1,7 @@ "use client"; -import React from "react"; import type { Departments } from "@prisma/client"; -import type { EvidenceWithStatus } from "../../actions/getEvidenceDashboard"; +import type { EvidenceWithStatus } from "../../data/getEvidenceDashboard"; import { ChevronDown, ChevronUp } from "lucide-react"; import { useScrollPosition } from "../../hooks/useScrollPosition"; import { useEvidenceData } from "../../hooks/useEvidenceData"; diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/components/EvidenceOverview.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/components/EvidenceOverview.tsx index 4703c7a1b7..34d8c76d6d 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/components/EvidenceOverview.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/components/EvidenceOverview.tsx @@ -1,35 +1,27 @@ "use client"; -import React from "react"; -import { useEvidenceDashboard } from "../hooks/useEvidenceDashboard"; -import { DepartmentBarChart } from "./DepartmentChart/DepartmentBarChart"; -import { AssigneeBarChart } from "./AssigneeChart/AssigneeBarChart"; -import { FrameworkBarChart } from "./FrameworkChart/FrameworkBarChart"; -import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card"; import { useI18n } from "@/locales/client"; -import { - EvidenceOverviewSkeleton, - EvidenceErrorState, - EvidenceEmptyState, -} from "./EvidenceUIStates"; +import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card"; import { EvidenceSummaryCards } from "../../list/components/EvidenceSummaryCards"; +import type { EvidenceDashboardData } from "../data/getEvidenceDashboard"; +import { AssigneeBarChart } from "./AssigneeChart/AssigneeBarChart"; +import { DepartmentBarChart } from "./DepartmentChart/DepartmentBarChart"; +import { EvidenceEmptyState } from "./EvidenceUIStates"; +import { FrameworkBarChart } from "./FrameworkChart/FrameworkBarChart"; -export const EvidenceOverview = () => { - const { data, isLoading, error } = useEvidenceDashboard(); +export const EvidenceOverview = ({ + evidence, +}: { + evidence: EvidenceDashboardData | null; +}) => { const t = useI18n(); - if (isLoading) { - return ; - } - - if (error) { - return ; - } - - if (!data) { + if (!evidence) { return ; } + const { byDepartment, byAssignee, byFramework, unassigned } = evidence; + return (
{/* Evidence summary statistics cards */} @@ -42,7 +34,7 @@ export const EvidenceOverview = () => { {t("evidence.dashboard.by_department")} - + @@ -51,10 +43,7 @@ export const EvidenceOverview = () => { {t("evidence.dashboard.by_assignee")} - + @@ -63,7 +52,7 @@ export const EvidenceOverview = () => { {t("evidence.dashboard.by_framework")} - +
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/components/FrameworkChart/FrameworkBarChart.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/components/FrameworkChart/FrameworkBarChart.tsx index 5162459603..1bf8c90eca 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/components/FrameworkChart/FrameworkBarChart.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/components/FrameworkChart/FrameworkBarChart.tsx @@ -1,10 +1,9 @@ "use client"; -import React from "react"; -import type { EvidenceWithStatus } from "../../actions/getEvidenceDashboard"; import { ChevronDown, ChevronUp } from "lucide-react"; -import { useScrollPosition } from "../../hooks/useScrollPosition"; +import type { EvidenceWithStatus } from "../../data/getEvidenceDashboard"; import { useFrameworkData } from "../../hooks/useFrameworkData"; +import { useScrollPosition } from "../../hooks/useScrollPosition"; import { FrameworkChartRow } from "./FrameworkChartRow"; interface FrameworkBarChartProps { diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/data/getEvidenceDashboard.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/data/getEvidenceDashboard.ts new file mode 100644 index 0000000000..e7f6885bd1 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/data/getEvidenceDashboard.ts @@ -0,0 +1,218 @@ +import type { Departments, OrganizationEvidence } from "@bubba/db"; +import { db } from "@bubba/db"; +import { cache } from "react"; + +export type EvidenceStatus = "empty" | "draft" | "needsReview" | "upToDate"; + +export interface EvidenceWithStatus + extends Omit { + status: EvidenceStatus; + assigneeEmail?: string; +} + +export interface EvidenceDashboardData { + byDepartment: Record; + byAssignee: Record; + byFramework: Record; + unassigned: EvidenceWithStatus[]; + statusCounts: { + totalCount: number; + emptyCount: number; + draftCount: number; + needsReviewCount: number; + upToDateCount: number; + }; +} + +/** + * Get the evidence dashboard data for a given organization + * @param organizationId - The ID of the organization to get the evidence for + * @returns The evidence dashboard data for the given organization + */ +export const getEvidenceDashboard = cache( + async (organizationId: string): Promise => { + const evidence = await db.organizationEvidence.findMany({ + where: { + organizationId, + }, + include: { + assignee: true, + }, + }); + + if (!evidence) { + return null; + } + + // Calculate status for each evidence item + const now = new Date(); + let emptyCount = 0; + let draftCount = 0; + let needsReviewCount = 0; + let upToDateCount = 0; + const totalCount = evidence.length; + + const evidenceWithStatus: EvidenceWithStatus[] = evidence.map((item) => { + // Check if task has files or links + const hasContent = + item.fileUrls.length > 0 || item.additionalUrls.length > 0; + + // Check if task is published + const isPublished = item.published; + + // Check if task needs review (published and next review date is in the past) + let nextReviewDate = null; + + if (item.lastPublishedAt && item.frequency) { + // Calculate next review date based on last published date and frequency + const lastPublished = new Date(item.lastPublishedAt); + nextReviewDate = new Date(lastPublished); + + switch (item.frequency) { + case "monthly": + nextReviewDate.setMonth(nextReviewDate.getMonth() + 1); + break; + case "quarterly": + nextReviewDate.setMonth(nextReviewDate.getMonth() + 3); + break; + case "yearly": + nextReviewDate.setFullYear(nextReviewDate.getFullYear() + 1); + break; + } + } + + const isPastDue = nextReviewDate && nextReviewDate < now; + + let status: EvidenceStatus; + + if (!hasContent && !isPublished) { + // No files or links + status = "empty"; + emptyCount++; + } else if (!isPublished) { + // Has content but not published + status = "draft"; + draftCount++; + } else if (isPastDue) { + // Published but needs review + status = "needsReview"; + needsReviewCount++; + } else { + // Published and up to date + status = "upToDate"; + upToDateCount++; + } + + // Extract assignee email if available + const assigneeEmail = item.assignee?.email || undefined; + + // Create a new object without the assignee property + const { assignee, ...evidenceWithoutAssignee } = item; + + return { + ...evidenceWithoutAssignee, + status, + assigneeEmail, + }; + }); + + // Initialize with all department values from the enum + const departmentValues: Departments[] = [ + "none", + "admin", + "gov", + "hr", + "it", + "itsm", + "qms", + ]; + + // Initialize the byDepartment object with empty arrays for all departments + const initialByDepartment = departmentValues.reduce< + Record + >( + (acc, dept) => { + acc[dept] = []; + return acc; + }, + {} as Record + ); + + // Group evidence by department + const byDepartment = evidenceWithStatus.reduce< + Record + >((acc, curr) => { + // Use the department from the evidence, or default to 'none' if it's null/undefined + const department = curr.department || "none"; + + // Add the evidence to the appropriate department array + acc[department].push(curr); + return acc; + }, initialByDepartment); + + // Collect unassigned evidence + const unassigned = evidenceWithStatus.filter((item) => !item.assigneeId); + + // Group evidence by assignee + const byAssignee = evidenceWithStatus.reduce< + Record + >((acc, curr) => { + if (!curr.assigneeId || !curr.assigneeEmail) { + return acc; + } + + const email = curr.assigneeEmail; + if (!acc[email]) { + acc[email] = []; + } + acc[email].push(curr); + return acc; + }, {}); + + // Sample the first assignee's data + const firstAssigneeEmail = Object.keys(byAssignee)[0]; + if (firstAssigneeEmail) { + const firstAssigneeItems = byAssignee[firstAssigneeEmail]; + // Count items by status for the first assignee + const statusCounts = { + empty: 0, + draft: 0, + needsReview: 0, + upToDate: 0, + }; + + for (const item of firstAssigneeItems) { + statusCounts[item.status]++; + } + } + + const byFramework = evidenceWithStatus.reduce< + Record + >((acc, curr) => { + const frameworkId = curr.frameworkId; + if (!frameworkId) { + return acc; + } + + if (!acc[frameworkId]) { + acc[frameworkId] = []; + } + acc[frameworkId].push(curr); + return acc; + }, {}); + + return { + byDepartment, + byAssignee, + byFramework, + unassigned, + statusCounts: { + totalCount, + emptyCount, + draftCount, + needsReviewCount, + upToDateCount, + }, + }; + } +); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/hooks/useAssigneeData.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/hooks/useAssigneeData.ts index cfea2c0aff..63594de0f5 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/hooks/useAssigneeData.ts +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/hooks/useAssigneeData.ts @@ -1,6 +1,6 @@ import { useI18n } from "@/locales/client"; -import type { EvidenceWithStatus } from "../actions/getEvidenceDashboard"; import type { StatusType } from "../constants/evidence-status"; +import type { EvidenceWithStatus } from "../data/getEvidenceDashboard"; /** * Interface for the processed assignee data diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/hooks/useEvidenceDashboard.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/hooks/useEvidenceDashboard.ts deleted file mode 100644 index 7c114ecf38..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/hooks/useEvidenceDashboard.ts +++ /dev/null @@ -1,57 +0,0 @@ -"use client"; - -import useSWR from "swr"; -import { getEvidenceDashboard } from "../actions/getEvidenceDashboard"; - -async function fetchEvidenceDashboard() { - try { - const result = await getEvidenceDashboard(); - - if (!result) { - throw new Error("No result received"); - } - - if (result.serverError) { - throw new Error(result.serverError); - } - - // Debug: Log the data received from the server action - console.log("Evidence Dashboard Data:", result.data?.data); - - // Check if the data contains status information - if (result.data?.data?.byAssignee) { - const firstAssignee = Object.values(result.data.data.byAssignee)[0]; - if (firstAssignee && firstAssignee.length > 0) { - console.log("First assignee's first item:", firstAssignee[0]); - } - } - - return result.data?.data; - } catch (error) { - console.error("Error fetching evidence dashboard:", error); - throw error; - } -} - -export function useEvidenceDashboard() { - const { data, error, isLoading, mutate } = useSWR( - "evidence-dashboard", - fetchEvidenceDashboard, - { - revalidateOnFocus: false, - revalidateOnReconnect: false, - }, - ); - - // Debug: Log the data in the hook - if (data) { - console.log("useEvidenceDashboard hook data:", data); - } - - return { - data, - error, - isLoading, - mutate, - }; -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/hooks/useEvidenceData.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/hooks/useEvidenceData.ts index a6045abcfe..744a6a8adf 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/hooks/useEvidenceData.ts +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/hooks/useEvidenceData.ts @@ -1,8 +1,8 @@ import { useI18n } from "@/locales/client"; import type { Departments } from "@prisma/client"; -import type { EvidenceWithStatus } from "../actions/getEvidenceDashboard"; import { DEPARTMENT_NAMES } from "../constants/departments"; import type { StatusType } from "../constants/evidence-status"; +import type { EvidenceWithStatus } from "../data/getEvidenceDashboard"; /** * Interface for the processed department data diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/hooks/useFrameworkData.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/hooks/useFrameworkData.ts index 2899968644..308e993288 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/hooks/useFrameworkData.ts +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/hooks/useFrameworkData.ts @@ -1,4 +1,4 @@ -import type { EvidenceWithStatus } from "../actions/getEvidenceDashboard"; +import type { EvidenceWithStatus } from "../data/getEvidenceDashboard"; import type { StatusType } from "../constants/evidence-status"; /** diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/loading.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/loading.tsx new file mode 100644 index 0000000000..86c68be725 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/loading.tsx @@ -0,0 +1,5 @@ +import { EvidenceOverviewSkeleton } from "./components/EvidenceUIStates"; + +export default function Loading() { + return ; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/page.tsx index 2e0268082c..27cab8b7d9 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/overview/page.tsx @@ -1,5 +1,27 @@ +import { auth } from "@/auth"; +import { redirect } from "next/navigation"; import { EvidenceOverview } from "./components/EvidenceOverview"; +import { getEvidenceDashboard } from "./data/getEvidenceDashboard"; +import { EvidenceErrorState } from "./components/EvidenceUIStates"; -export default function EvidenceOverviewPage() { - return ; +export default async function EvidenceOverviewPage() { + const session = await auth(); + + if (!session) { + return redirect("/"); + } + + if (!session.user.organizationId) { + return redirect("/"); + } + + try { + const evidence = await getEvidenceDashboard(session.user.organizationId); + + return ; + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : "An unexpected error occurred"; + return ; + } } diff --git a/apps/app/src/lib/errors.ts b/apps/app/src/lib/errors.ts new file mode 100644 index 0000000000..1fc188d2e5 --- /dev/null +++ b/apps/app/src/lib/errors.ts @@ -0,0 +1,17 @@ +export class AppError extends Error { + constructor( + public code: string, + message?: string, + public status: number = 400 + ) { + super(message || code); + this.name = "AppError"; + } +} + +export const appErrors = { + UNAUTHORIZED: "UNAUTHORIZED", + NOT_FOUND: "NOT_FOUND", + VALIDATION_ERROR: "VALIDATION_ERROR", + UNEXPECTED_ERROR: "UNEXPECTED_ERROR", +} as const; diff --git a/yarn.lock b/yarn.lock index 83d49432b6..b4b8249544 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2681,8 +2681,8 @@ resolved "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-6.5.0.tgz" integrity sha512-ta1EhwAmIWnFjw0bTVdRe3rw5h40YDZ59i2Ko2FGVt4EDvAt4Po5aAN8eCj2Eq46wKJccEDwQ3qBXUQTKUbM8Q== dependencies: - postgres-array "3.0.3" "@prisma/driver-adapter-utils" "6.5.0" + postgres-array "3.0.3" "@prisma/client@>=2.26.0 || >=3 || >=4 || >=5 || >=6", "@prisma/client@>=4.16.1", "@prisma/client@^6.5.0": version "6.5.0" @@ -10973,14 +10973,14 @@ pg@>=8.0, pg@^8.11.3, pg@^8.14.1: version "8.14.1" resolved "https://registry.npmjs.org/pg/-/pg-8.14.1.tgz" integrity sha512-0TdbqfjwIun9Fm/r89oB7RFQ0bLgduAhiIqIXOsyKoiC/L54DbuAAzIEN/9Op0f1Po9X7iCPXGoa/Ah+2aI8Xw== + optionalDependencies: + pg-cloudflare "^1.1.1" dependencies: pg-connection-string "^2.7.0" pg-pool "^3.8.0" pg-protocol "^1.8.0" pg-types "^2.1.0" pgpass "1.x" - optionalDependencies: - pg-cloudflare "^1.1.1" pg-cloudflare@^1.1.1: version "1.1.1"