diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/(home)/components/FrameworkProgress.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/(home)/components/FrameworkProgress.tsx index 60f23383e7..e806f473bd 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/(home)/components/FrameworkProgress.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/(home)/components/FrameworkProgress.tsx @@ -11,6 +11,8 @@ import { Button } from "@bubba/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card"; import { FileStack } from "lucide-react"; import Link from "next/link"; +import { useMediaQuery } from "@bubba/ui/hooks"; +import type { ReactNode } from "react"; interface Props { frameworks: (OrganizationFramework & { @@ -29,13 +31,15 @@ export function FrameworkProgress({ frameworks }: Props) { isLoading, } = useComplianceScores({ frameworks }); + const isMobile = useMediaQuery("(max-width: 640px)"); + const CircleProgress = ({ percentage, label, href, }: { percentage: number; - label: string; + label: ReactNode; href: string; }) => ( {percentage}% -
- {label} +
+ {label}
); @@ -156,12 +160,22 @@ export function FrameworkProgress({ frameworks }: Props) { /> + Evidence + Evidence Tasks + + } href="/evidence/list" /> + Tests + Cloud Tests + + } href="/tests" />
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 224321b079..53ac22c59e 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 @@ -8,71 +8,71 @@ import type { EvidenceDetailsProps } from "../types"; import { ReviewSection } from "./ReviewSection"; export function EvidenceDetails({ id }: EvidenceDetailsProps) { - const { data, isLoading, error, mutate } = useOrganizationEvidence({ id }); + const { data, isLoading, mutate } = useOrganizationEvidence({ id }); - if (isLoading) { - return ( -
- - - -
- ); - } + if (isLoading) { + return ( +
+ + + +
+ ); + } - if (!data?.data) return null; + if (!data?.data) return null; - const evidence = data.data; + const evidence = data.data; - const handleMutate = async () => { - await mutate(); - }; + const handleMutate = async () => { + await mutate(); + }; - return ( -
- {/* Alert with evidence info and status */} - - - -
- {evidence.evidence.name} Evidence -
- {evidence.published ? ( -
- - Published -
- ) : ( -
- - Draft -
- )} -
-
-
- - {evidence.description || "No description provided."} + return ( +
+ {/* Alert with evidence info and status */} + + + +
+ {evidence.evidence.name} Evidence +
+ {evidence.published ? ( +
+ + Published +
+ ) : ( +
+ + Draft +
+ )} +
+
+
+ + {evidence.description || "No description provided."} - {evidence.isNotRelevant && ( -
- This evidence has been marked as not relevant and will not be - included in compliance reports. -
- )} -
-
+ {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/ReviewSection.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/components/ReviewSection.tsx index 83ef3a4a79..11a77d24b5 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/components/ReviewSection.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/components/ReviewSection.tsx @@ -16,152 +16,152 @@ import { DepartmentSection } from "./DepartmentSection"; import { FrequencySection } from "./FrequencySection"; interface ReviewSectionProps { - evidence: OrganizationEvidence; - evidenceId: string; - lastPublishedAt: Date | null; - frequency: Frequency | null; - department: string | null; - currentAssigneeId: string | null | undefined; - onSuccess: () => Promise; - id: string; + evidence: OrganizationEvidence; + evidenceId: string; + lastPublishedAt: Date | null; + frequency: Frequency | null; + department: string | null; + currentAssigneeId: string | null | undefined; + onSuccess: () => Promise; + id: string; } export function ReviewSection({ - evidenceId, - lastPublishedAt, - frequency, - department, - currentAssigneeId, - onSuccess, - id, - evidence, + evidenceId, + lastPublishedAt, + frequency, + department, + currentAssigneeId, + onSuccess, + id, + evidence, }: ReviewSectionProps) { - const { mutate } = useOrganizationEvidence({ id }); - const reviewInfo = calculateNextReview(lastPublishedAt, frequency); + 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."); - }, - }); + 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."); + }, + }); - const { execute: publishAction, isExecuting } = useAction(publishEvidence, { - onSuccess: () => { - toast.success("Evidence published successfully"); - mutate(); - }, - onError: () => { - toast.error("Failed to publish evidence, please try again."); - }, - }); + const { execute: publishAction, isExecuting } = useAction(publishEvidence, { + onSuccess: () => { + toast.success("Evidence published successfully"); + mutate(); + }, + onError: () => { + toast.error("Failed to publish evidence, please try again."); + }, + }); - return ( - - - -
-

Evidence Overview

-

- Manage review frequency, department assignment, and track upcoming - review dates -

-
- -
-
- -
-
-
- -

- DEPARTMENT -

-
- -
+ return ( + + + +
+

Evidence Overview

+

+ Manage review frequency, department assignment, and track upcoming + review dates +

+
+ +
+
+ +
+
+
+ +

+ DEPARTMENT +

+
+ +
-
-
- -

- FREQUENCY -

-
- -
+
+
+ +

+ FREQUENCY +

+
+ +
-
-
- -

- NEXT REVIEW -

-
- {!reviewInfo ? ( -

ASAP

- ) : ( -
- {reviewInfo.daysUntil} days ( - {format(reviewInfo.nextReviewDate, "MM/dd/yyyy")}) -
- )} -
-
-
- -

- ASSIGNEE -

-
- -
-
- {!evidence.published && ( - - )} -
-
- ); +
+
+ +

+ NEXT REVIEW +

+
+ {!reviewInfo ? ( +

ASAP

+ ) : ( +
+ {reviewInfo.daysUntil} days ( + {format(reviewInfo.nextReviewDate, "MM/dd/yyyy")}) +
+ )} +
+
+
+ +

+ ASSIGNEE +

+
+ +
+
+ {!evidence.published && ( + + )} +
+
+ ); } 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 bf7707ab42..8da64000c7 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 @@ -4,51 +4,51 @@ import { useI18n } from "@/locales/client"; import { useEvidenceTable } from "../hooks/useEvidenceTableContext"; import { DataTable } from "./data-table/EvidenceListTable"; import { - ActiveFilterBadges, - FilterDropdown, - PaginationControls, - SearchInput, + ActiveFilterBadges, + FilterDropdown, + PaginationControls, + SearchInput, } from "./EvidenceFilters"; import { SkeletonTable } from "./SkeletonTable"; import { EvidenceSummaryCards } from "./EvidenceSummaryCards"; export function EvidenceList() { - const t = useI18n(); - const { evidenceTasks = [], isLoading, error } = useEvidenceTable(); - - if (error) return
Error: {error.message}
; - - return ( -
-
- - -
-
- -
- - - - -
-
- - {isLoading ? ( - - ) : ( - <> - {evidenceTasks.length === 0 ? ( -
- No evidence tasks found. Try adjusting your filters. -
- ) : ( - - )} - - - - )} -
- ); + const t = useI18n(); + const { evidenceTasks = [], isLoading, error } = useEvidenceTable(); + + if (error) return
Error: {error.message}
; + + return ( +
+
+ + +
+
+ +
+ + + + +
+
+ + {isLoading ? ( + + ) : ( + <> + {evidenceTasks.length === 0 ? ( +
+ No evidence tasks found. Try adjusting your filters. +
+ ) : ( + + )} + + + + )} +
+ ); } diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/components/SkeletonTable.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/components/SkeletonTable.tsx index 1961ba46b5..6ee50126af 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/components/SkeletonTable.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/components/SkeletonTable.tsx @@ -1,52 +1,68 @@ import { Skeleton } from "@bubba/ui/skeleton"; import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, } from "@bubba/ui/table"; +import { cn } from "@bubba/ui/cn"; const SKELETON_ROWS = [ - "skeleton-1", - "skeleton-2", - "skeleton-3", - "skeleton-4", - "skeleton-5", + "skeleton-1", + "skeleton-2", + "skeleton-3", + "skeleton-4", + "skeleton-5", ] as const; export const SkeletonTable = () => { - return ( - - - - - - - - - - - - - - - - {SKELETON_ROWS.map((key) => ( - - - - - - - - - - - - ))} - -
- ); + return ( + + + + + + + + + + + + + + + + + + + + + + {SKELETON_ROWS.map((key) => ( + + +
+ + +
+
+ + + + + + + + + + + + +
+ ))} +
+
+ ); }; diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/components/data-table/EvidenceListTable.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/components/data-table/EvidenceListTable.tsx index b72f4f9f94..54d89a145b 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/components/data-table/EvidenceListTable.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/components/data-table/EvidenceListTable.tsx @@ -1,93 +1,100 @@ "use client"; import { - type Column, - flexRender, - getCoreRowModel, - useReactTable, - getSortedRowModel, - type SortingState, + type Column, + flexRender, + getCoreRowModel, + useReactTable, + getSortedRowModel, + type SortingState, } from "@tanstack/react-table"; import { Table, TableBody, TableCell, TableRow } from "@bubba/ui/table"; import { columns } from "./columns"; import { DataTableHeader } from "./data-table-header"; import type { EvidenceTaskRow } from "./types"; -import { useRouter } from "next/navigation"; +import { cn } from "@bubba/ui/cn"; import { useState } from "react"; export function DataTable({ data }: { data: EvidenceTaskRow[] }) { - const router = useRouter(); - const [sorting, setSorting] = useState([ - { - id: "name", - desc: false, - }, - ]); + const [sorting, setSorting] = useState([ + { + id: "name", + desc: false, + }, + ]); - const table = useReactTable({ - data, - columns, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - enableColumnResizing: true, - columnResizeMode: "onChange", - state: { - sorting, - }, - onSortingChange: setSorting, - }); + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + enableColumnResizing: true, + columnResizeMode: "onChange", + state: { + sorting, + }, + onSortingChange: setSorting, + }); - return ( -
-
- - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - router.push(`/evidence/${row.original.id}`)} - > - {row.getVisibleCells().map((cell) => ( - -
- {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} -
-
- - ))} - - )) - ) : ( - - - No evidence tasks found. - - - )} - -
-
-
- ); + return ( +
+
+ + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + +
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} +
+
+ + ))} + + )) + ) : ( + + + No evidence tasks found. + + + )} + +
+
+
+ ); } 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 83a8fe9a01..5e2fdddd2a 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 @@ -3,181 +3,198 @@ import type { ColumnDef } from "@tanstack/react-table"; import { CheckCircle2, XCircle, Building, AlertTriangle } from "lucide-react"; import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, } from "@bubba/ui/tooltip"; import { Avatar, AvatarFallback, AvatarImage } from "@bubba/ui/avatar"; import type { EvidenceTaskRow } from "./types"; import { calculateNextReview } from "@/lib/utils/calculate-next-review"; import { format } from "date-fns"; import { StatusPolicies, type StatusType } from "@/components/status-policies"; +import Link from "next/link"; +import { Button } from "@bubba/ui/button"; export const columns: ColumnDef[] = [ - { - id: "name", - accessorKey: "name", - header: "Name", - enableResizing: true, - enableSorting: true, - size: 100, - minSize: 200, - cell: ({ row }) => ( - - - -
{row.original.name}
-
- {row.original.description} -
-
- ), - }, - { - id: "status", - accessorKey: "published", - header: "Status", - enableResizing: true, - enableSorting: true, - size: 150, - minSize: 120, - cell: ({ row }) => { - const isPublished = row.original.published; - const label = isPublished ? "Published" : "Draft"; + { + id: "name", + accessorKey: "name", + header: "Name", + enableResizing: true, + enableSorting: true, + size: 100, + minSize: 200, + cell: ({ row }) => ( +
+ +
+ +
+
+ ), + }, + { + id: "status", + accessorKey: "published", + header: "Status", + enableResizing: true, + enableSorting: true, + size: 150, + minSize: 120, + cell: ({ row }) => { + const isPublished = row.original.published; - return ( -
- -
- ); - }, - }, - { - id: "department", - accessorKey: "department", - header: "Department", - size: 150, - enableResizing: true, - minSize: 130, - enableSorting: true, - cell: ({ row }) => { - const department = row.original.department; - if (!department || department === "none") - return
None
; + return ( +
+ +
+ ); + }, + }, + { + id: "department", + accessorKey: "department", + header: "Department", + size: 150, + enableResizing: true, + minSize: 130, + enableSorting: true, + cell: ({ row }) => { + const department = row.original.department; + if (!department || department === "none") + return ( +
+ None +
+ ); - return ( -
- - - {department.replace(/_/g, " ").toUpperCase()} - -
- ); - }, - }, - { - id: "frequency", - accessorKey: "frequency", - header: "Frequency", - size: 150, - enableResizing: true, - minSize: 130, - enableSorting: true, - cell: ({ row }) => { - const frequency = row.original.frequency; - if (!frequency) return null; + return ( +
+ + + {department.replace(/_/g, " ").toUpperCase()} + +
+ ); + }, + }, + { + id: "frequency", + accessorKey: "frequency", + header: "Frequency", + size: 150, + enableResizing: true, + minSize: 130, + enableSorting: true, + cell: ({ row }) => { + const frequency = row.original.frequency; + if (!frequency) return null; - return
{frequency}
; - }, - }, - { - id: "nextReviewDate", - accessorKey: "nextReviewDate", - header: "Next Review Date", - size: 150, - enableResizing: true, - minSize: 180, - enableSorting: true, - cell: ({ row }) => { - if (row.original.lastPublishedAt === null) { - return
ASAP
; - } + return
{frequency}
; + }, + }, + { + id: "nextReviewDate", + accessorKey: "nextReviewDate", + header: "Next Review Date", + size: 150, + enableResizing: true, + minSize: 180, + enableSorting: true, + cell: ({ row }) => { + if (row.original.lastPublishedAt === null) { + return ( +
+ ASAP +
+ ); + } - const reviewInfo = calculateNextReview( - row.original.lastPublishedAt, - row.original.frequency, - ); + const reviewInfo = calculateNextReview( + row.original.lastPublishedAt, + row.original.frequency, + ); - if (!reviewInfo) return null; + if (!reviewInfo) return null; - return ( -
- {reviewInfo.daysUntil} days ( - {format(reviewInfo.nextReviewDate, "MM/dd/yyyy")}) -
- ); - }, - }, - { - id: "assignee", - accessorKey: "assignee", - header: "Assignee", - enableResizing: true, - enableSorting: true, - size: 150, - minSize: 150, - cell: ({ row }) => { - const assignee = row.original.assignee; + return ( + + ); + }, + }, + { + id: "assignee", + accessorKey: "assignee", + header: "Assignee", + enableResizing: true, + enableSorting: true, + size: 150, + minSize: 150, + cell: ({ row }) => { + const assignee = row.original.assignee; - if (!assignee) { - return
Unassigned
; - } + if (!assignee) { + return ( +
+ Unassigned +
+ ); + } - return ( -
- - - - {assignee.name ? assignee.name.charAt(0) : "?"} - - - {assignee.name} -
- ); - }, - }, - { - id: "relevance", - accessorKey: "isNotRelevant", - header: "Relevance", - enableResizing: true, - enableSorting: true, - size: 150, - minSize: 120, - cell: ({ row }) => { - const isNotRelevant = row.original.isNotRelevant; + return ( +
+ + + + {assignee.name ? assignee.name.charAt(0) : "?"} + + + {assignee.name} +
+ ); + }, + }, + { + 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 -
- ); - } + if (!isNotRelevant) { + return ( +
+ + Relevant +
+ ); + } - return ( -
- - Not Relevant -
- ); - }, - }, + return ( +
+ + Not Relevant +
+ ); + }, + }, ]; diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/components/data-table/data-table-header.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/components/data-table/data-table-header.tsx index e02f6cd071..f4f1d0852e 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/components/data-table/data-table-header.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/components/data-table/data-table-header.tsx @@ -8,56 +8,65 @@ import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react"; import { cn } from "@bubba/ui/cn"; interface DataTableHeaderProps { - table: Table; + table: Table; } export function DataTableHeader({ table }: DataTableHeaderProps) { - return ( - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder ? null : ( -
- {flexRender( - header.column.columnDef.header, - header.getContext(), - )} - {{ - asc: , - desc: , - }[header.column.getIsSorted() as string] ?? - (header.column.getCanSort() && ( - - ))} -
- )} -
- - ))} - - ))} - - ); + return ( + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder ? null : ( +
+ {flexRender( + header.column.columnDef.header, + header.getContext(), + )} + {{ + asc: , + desc: , + }[header.column.getIsSorted() as string] ?? + (header.column.getCanSort() && ( + + ))} +
+ )} +
+ + ))} + + ))} + + ); } diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/components/EditableDepartment.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/components/EditableDepartment.tsx index 07ac5575b5..6e163f1f1f 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/components/EditableDepartment.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/components/EditableDepartment.tsx @@ -15,6 +15,7 @@ import { useAction } from "next-safe-action/hooks"; import { updateEmployeeDepartment } from "../actions/update-department"; import { Pencil, Check, X } from "lucide-react"; import type { Departments } from "@bubba/db"; +import { cn } from "@bubba/ui/cn"; const DEPARTMENTS = [ { value: "admin", label: "Admin" }, @@ -62,7 +63,7 @@ export function EditableDepartment({ if (!isEditing) { return ( -
+
-

+

{DEPARTMENTS.find((d) => d.value === currentDepartment)?.label || currentDepartment}

@@ -83,14 +84,16 @@ export function EditableDepartment({ } return ( -
+
-
+
-
+
+ +
+ + ); + + // Created key content for reuse in both Dialog and Sheet + const renderCreatedKeyContent = () => ( + <> +
+
+

+ {t("settings.api_keys.api_key")} +

+
+
+
+
+ {createdApiKey} +
+
+ +
+
+

+ {t("settings.api_keys.save_warning")} +

+
+
+
+ +
+ + ); + + // Render different UI components for mobile vs desktop + if (isMobile) { + return ( + + +
+ {createdApiKey ? ( + <> + + + {t("settings.api_keys.created_title")} + + + {t("settings.api_keys.created_description")} + + + {renderCreatedKeyContent()} + + ) : ( + <> + + {t("settings.api_keys.create_title")} + + {t("settings.api_keys.create_description")} + + + {renderFormContent()} + + )} +
+
+
+ ); + } + return ( - + {createdApiKey ? ( <> @@ -107,45 +262,7 @@ export function CreateApiKeyDialog({ {t("settings.api_keys.created_description")} -
-
-

- {t("settings.api_keys.api_key")} -

-
-
-
-
- - {createdApiKey} - -
-
- -
-
-

- {t("settings.api_keys.save_warning")} -

-
-
- - - + {renderCreatedKeyContent()} ) : ( <> @@ -155,77 +272,7 @@ export function CreateApiKeyDialog({ {t("settings.api_keys.create_description")} -
-
- - setName(e.target.value)} - placeholder={t("settings.api_keys.name_placeholder")} - required - className="w-full" - /> -
-
- - -
- - - - -
+ {renderFormContent()} )}
diff --git a/apps/app/src/components/tables/people/data-table-header.tsx b/apps/app/src/components/tables/people/data-table-header.tsx index d6d8b11622..41f0b8d345 100644 --- a/apps/app/src/components/tables/people/data-table-header.tsx +++ b/apps/app/src/components/tables/people/data-table-header.tsx @@ -7,6 +7,7 @@ import { TableHead, TableHeader, TableRow } from "@bubba/ui/table"; import { ArrowDown, ArrowUp } from "lucide-react"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useCallback } from "react"; +import { cn } from "@bubba/ui/cn"; type Props = { table?: { @@ -59,27 +60,29 @@ export function DataTableHeader({ table, loading }: Props) { return ( - {isVisible("email") && ( - + {isVisible("name") && ( + )} - {isVisible("name") && ( + {isVisible("email") && ( )} @@ -103,7 +106,7 @@ export function DataTableHeader({ table, loading }: Props) { )} {isVisible("status") && ( - + - + ), + cell: ({ row }) => { + const name = row.original.name; + const email = row.original.email; + const isActive = row.original.isActive; + const status = getEmployeeStatusFromBoolean(isActive); + + return ( +
+ +
+ {email} +
+
+ +
+
+ ); + }, }, { - id: "name", - accessorKey: "name", + id: "email", + accessorKey: "email", header: ({ column }) => ( - - - + ), + cell: ({ row }) => { + const email = row.original.email; + return ( +
{email}
+ ); + }, }, { id: "department", accessorKey: "department", header: ({ column }) => ( - - - + ), cell: ({ row }) => { const department = row.original.department; return ( -
+
{department}
); @@ -116,7 +124,11 @@ function getColumns(): ColumnDef[] { const isActive = row.original.isActive; const status = getEmployeeStatusFromBoolean(isActive); - return ; + return ( +
+ +
+ ); }, enableSorting: true, enableHiding: true, @@ -130,13 +142,8 @@ export function DataTable({ pageCount, currentPage, }: DataTableProps) { + const columns = getColumns(); const router = useRouter(); - const clientColumns = getColumns(); - const columns = clientColumns.map((col) => ({ - ...col, - header: columnHeaders[col.id as keyof typeof columnHeaders], - accessorFn: (row: PersonType) => row[col.id as keyof PersonType], - })); const table = useReactTable({ data, @@ -147,55 +154,49 @@ export function DataTable({ }); return ( -
- - - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - { - const person = row.original; - router.push(`/people/${person.id}`); - }} - > - {row.getVisibleCells().map((cell) => { - let cellClassName = ""; - - if (cell.column.id === "name") { - cellClassName = "w-[30%]"; - } else if (cell.column.id === "email") { - cellClassName = "w-[30%] hidden md:table-cell"; - } else if (cell.column.id === "department") { - cellClassName = "w-[20%] uppercase"; - } else if (cell.column.id === "status") { - cellClassName = "w-[20%] text-center"; - } - - return ( - +
+
+
+ + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + {flexRender( cell.column.columnDef.cell, cell.getContext(), )} - ); - })} + ))} + + )) + ) : ( + + + No results. + - )) - ) : ( - - - No results. - - - )} - -
+ )} + + +
); diff --git a/apps/app/src/components/vendors/charts/category-chart.tsx b/apps/app/src/components/vendors/charts/category-chart.tsx index f1288b341a..5abf48505e 100644 --- a/apps/app/src/components/vendors/charts/category-chart.tsx +++ b/apps/app/src/components/vendors/charts/category-chart.tsx @@ -4,47 +4,43 @@ import React, { type CSSProperties } from "react"; import { scaleBand, scaleLinear, max, format } from "d3"; import { ClientTooltip } from "@bubba/ui/chart-tooltip"; -const VENDOR_CATEGORY_COLORS = { - cloud: "bg-[var(--chart-open)]", - infrastructure: "bg-[var(--chart-pending)]", - software_as_a_service: "bg-[var(--chart-closed)]", - finance: "bg-[var(--chart-open)]", - marketing: "bg-[var(--chart-pending)]", - sales: "bg-[var(--chart-closed)]", - hr: "bg-[var(--chart-open)]", - other: "bg-[var(--chart-pending)]", -}; - -interface VendorCategoryData { +interface CategoryData { name: string; value: number; } interface VendorCategoryChartProps { - data: VendorCategoryData[]; + data: CategoryData[]; showEmptyDepartments?: boolean; } export function VendorCategoryChart({ data, - showEmptyDepartments = true, + showEmptyDepartments = false, }: VendorCategoryChartProps) { + // Filter out departments with zero values if not showing empty departments const filteredData = showEmptyDepartments ? data - : data.filter((dept) => dept.value > 0); + : data.filter((d) => d.value > 0); const sortedData = [...filteredData].sort((a, b) => b.value - a.value); - // Return early with a message if no departments have risks if (sortedData.length === 0) { return (
- No departments with risks found + No categories with risks found
); } - // Define fixed settings for consistent bar sizing + // If all values are 0, add a fake value to make the chart display properly + const allZeros = sortedData.every((d) => d.value === 0); + if (allZeros) { + for (const d of sortedData) { + d.value = 1; // Set a default value for display purposes + } + } + const barHeight = 40; // Fixed height for each bar in pixels const barGap = 16; // Gap between bars in pixels const minChartHeight = 300; // Minimum chart height @@ -66,25 +62,15 @@ export function VendorCategoryChart({ const xScale = scaleLinear().domain([0, maxValue]).range([0, 100]); - const marginLeft = 70; - const marginRight = 20; - const marginBottom = 20; + const marginLeft = 120; // Increase left margin for category names + const marginRight = 40; // Increase right margin slightly + const marginBottom = 30; // Increase bottom margin for tick labels - const getBarKey = (item: VendorCategoryData) => - `bar-${item.name}-${item.value}`; + const getBarKey = (item: CategoryData) => `bar-${item.name}-${item.value}`; const getTickKey = (value: number) => `tick-${value}`; const getGridKey = (value: number, position = 0) => `grid-${value.toString().replace(".", "-")}-${position}`; - const getLabelKey = (item: VendorCategoryData) => `label-${item.name}`; - - const getCategoryColor = (categoryName: string) => { - const normalizedName = categoryName.toLowerCase(); - return ( - VENDOR_CATEGORY_COLORS[ - normalizedName as keyof typeof VENDOR_CATEGORY_COLORS - ] || "bg-gray-400" - ); - }; + const getLabelKey = (item: CategoryData) => `label-${item.name}`; // Generate appropriate tick values based on max value const generateTickValues = () => { @@ -104,7 +90,7 @@ export function VendorCategoryChart({ return (
); })} @@ -195,7 +181,7 @@ export function VendorCategoryChart({ style={{ left: "0", top: `${yScale(entry.name)! + yScale.bandwidth() / 2}%`, - width: `${marginLeft - 2}px`, + width: `${marginLeft - 10}px`, }} className="absolute text-xs font-medium text-muted-foreground -translate-y-1/2 text-right pr-1 truncate" > diff --git a/apps/app/src/components/vendors/charts/status-chart.tsx b/apps/app/src/components/vendors/charts/status-chart.tsx index 984aa5a2e6..b117b318dd 100644 --- a/apps/app/src/components/vendors/charts/status-chart.tsx +++ b/apps/app/src/components/vendors/charts/status-chart.tsx @@ -51,6 +51,14 @@ export function StatusChart({ data }: StatusChartProps) { ); } + // If all values are 0, add a fake value to make the chart display properly + const allZeros = sortedData.every((d) => d.value === 0); + if (allZeros) { + for (const d of sortedData) { + d.value = 1; // Set a default value for display purposes + } + } + const barHeight = 40; // Fixed height for each bar in pixels const barGap = 16; // Gap between bars in pixels const minChartHeight = 300; // Minimum chart height @@ -72,9 +80,9 @@ export function StatusChart({ data }: StatusChartProps) { const xScale = scaleLinear().domain([0, maxValue]).range([0, 100]); - const marginLeft = 70; - const marginRight = 20; - const marginBottom = 20; + const marginLeft = 120; // Increase left margin for labels + const marginRight = 40; // Increase right margin slightly + const marginBottom = 30; // Increase bottom margin for tick labels const getBarKey = (item: StatusData) => `bar-${item.name}-${item.value}`; const getTickKey = (value: number) => `tick-${value}`; @@ -108,7 +116,7 @@ export function StatusChart({ data }: StatusChartProps) { return (
); })} @@ -199,7 +207,7 @@ export function StatusChart({ data }: StatusChartProps) { style={{ left: "0", top: `${yScale(entry.name)! + yScale.bandwidth() / 2}%`, - width: `${marginLeft - 2}px`, + width: `${marginLeft - 10}px`, }} className="absolute text-xs font-medium text-muted-foreground -translate-y-1/2 text-right pr-1 truncate" > diff --git a/apps/app/src/components/vendors/charts/vendors-by-category.tsx b/apps/app/src/components/vendors/charts/vendors-by-category.tsx index 55ee5d30de..ec91c5d3c6 100644 --- a/apps/app/src/components/vendors/charts/vendors-by-category.tsx +++ b/apps/app/src/components/vendors/charts/vendors-by-category.tsx @@ -28,14 +28,14 @@ export async function VendorsByCategory({ organizationId }: Props) { }; }).sort((a, b) => b.value - a.value); - // Separate departments with values > 0 and departments with values = 0 - const categoriesWithValues = data.filter((dept) => dept.value > 0); - const categoriesWithoutValues = data.filter((dept) => dept.value === 0); + // Separate categories with values > 0 and categories with values = 0 + const categoriesWithValues = data.filter((cat) => cat.value > 0); + const categoriesWithoutValues = data.filter((cat) => cat.value === 0); - // Determine which departments to show + // Determine which categories to show let categoriesToShow = [...categoriesWithValues]; - // If we have fewer than 4 departments with values, show up to 2 departments with no values + // If we have fewer than 4 categories with values, show up to 2 categories with no values if (categoriesWithValues.length < 4 && categoriesWithoutValues.length > 0) { categoriesToShow = [ ...categoriesWithValues, @@ -44,11 +44,11 @@ export async function VendorsByCategory({ organizationId }: Props) { } return ( - + {t("dashboard.vendors_by_category")} - + + {t("dashboard.vendor_status")} - + diff --git a/apps/app/src/components/vendors/charts/vendors-overview.tsx b/apps/app/src/components/vendors/charts/vendors-overview.tsx index 57d082dd68..7412e3faee 100644 --- a/apps/app/src/components/vendors/charts/vendors-overview.tsx +++ b/apps/app/src/components/vendors/charts/vendors-overview.tsx @@ -7,9 +7,13 @@ interface VendorOverviewProps { export function VendorOverview({ organizationId }: VendorOverviewProps) { return ( - <> - - - +
+
+ +
+
+ +
+
); }