From 8ac24165fe0f4f61c75b00b6866eb02af0f4f363 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Mon, 6 Apr 2026 17:26:09 -0400 Subject: [PATCH] fix(people): include HIPAA training in people table task counts and show per-category breakdown (#2464) The people table was not accounting for HIPAA training in the task completion totals, so employees appeared fully complete even when HIPAA training was pending. Also adds individual category counts (Policies, Training, HIPAA) to the tasks column for easier at-a-glance tracking, and fixes the overview chart bar to include HIPAA in the completed segment. Co-authored-by: Claude Opus 4.6 (1M context) --- .../people/all/components/MemberRow.tsx | 24 ++++++--- .../people/all/components/TeamMembers.tsx | 52 ++++++++++++++++--- .../all/components/TeamMembersClient.tsx | 4 +- .../components/EmployeeCompletionChart.tsx | 2 +- 4 files changed, 65 insertions(+), 17 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx index 60f188a006..cc041cd4be 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx @@ -39,7 +39,7 @@ import { MultiRoleCombobox } from './MultiRoleCombobox'; import { RemoveDeviceAlert } from './RemoveDeviceAlert'; import { RemoveMemberAlert } from './RemoveMemberAlert'; import type { CustomRoleOption } from './MultiRoleCombobox'; -import type { MemberWithUser } from './TeamMembers'; +import type { MemberWithUser, TaskCompletion } from './TeamMembers'; interface MemberRowProps { member: MemberWithUser; @@ -50,7 +50,7 @@ interface MemberRowProps { canEdit: boolean; isCurrentUserOwner: boolean; customRoles?: CustomRoleOption[]; - taskCompletion?: { completed: number; total: number }; + taskCompletion?: TaskCompletion; hasDeviceAgentDevice?: boolean; } @@ -254,16 +254,28 @@ export function MemberRow({ {/* TASKS */} {taskCompletion ? ( -
+
- - {taskCompletion.completed}/{taskCompletion.total} complete - +
+ + Policies {taskCompletion.policies.completed}/{taskCompletion.policies.total} + + {taskCompletion.training.total > 0 && ( + + Training {taskCompletion.training.completed}/{taskCompletion.training.total} + + )} + {taskCompletion.hipaa && ( + + HIPAA {taskCompletion.hipaa.completed}/{taskCompletion.hipaa.total} + + )} +
) : ( diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx index a607f992ff..8fa34be45d 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembers.tsx @@ -1,4 +1,5 @@ import { filterComplianceMembers } from '@/lib/compliance'; +import { HIPAA_TRAINING_ID } from '@/lib/data/hipaa-training-content'; import { trainingVideos as trainingVideosData } from '@/lib/data/training-videos'; import { serverApi } from '@/lib/server-api-client'; import type { Invitation, Member, User } from '@db'; @@ -15,6 +16,14 @@ export interface TeamMembersData { pendingInvitations: Invitation[]; } +export interface TaskCompletion { + completed: number; + total: number; + policies: { completed: number; total: number }; + training: { completed: number; total: number }; + hipaa?: { completed: number; total: number }; +} + export interface TeamMembersProps { canManageMembers: boolean; canInviteUsers: boolean; @@ -51,7 +60,7 @@ export async function TeamMembers(props: TeamMembersProps) { const employeeSyncData = await getEmployeeSyncConnections(organizationId); // Build task completion map for employees/contractors - const taskCompletionMap: Record = {}; + const taskCompletionMap: Record = {}; const employeeMembers = await filterComplianceMembers(members, organizationId); @@ -69,10 +78,17 @@ export async function TeamMembers(props: TeamMembersProps) { ]; if (employeeMembers.length > 0) { - const org = await db.organization.findUnique({ - where: { id: organizationId }, - select: { securityTrainingStepEnabled: true }, - }); + const [org, hipaaInstance] = await Promise.all([ + db.organization.findUnique({ + where: { id: organizationId }, + select: { securityTrainingStepEnabled: true }, + }), + db.frameworkInstance.findFirst({ + where: { organizationId, framework: { name: 'HIPAA' } }, + select: { id: true }, + }), + ]); + const hasHipaaFramework = !!hipaaInstance; const policies = await db.policy.findMany({ where: { @@ -92,20 +108,40 @@ export async function TeamMembers(props: TeamMembersProps) { const totalPolicies = policies.length; const totalTrainingVideos = org?.securityTrainingStepEnabled ? trainingVideosData.length : 0; - const totalTasks = totalPolicies + totalTrainingVideos; + const totalHipaaTraining = hasHipaaFramework ? 1 : 0; + const totalTasks = totalPolicies + totalTrainingVideos + totalHipaaTraining; for (const employee of employeeMembers) { const policiesCompleted = policies.filter((p) => p.signedBy.includes(employee.id)).length; const trainingsCompleted = org?.securityTrainingStepEnabled ? trainingCompletions.filter( - (tc) => tc.memberId === employee.id && tc.completedAt !== null, + (tc) => + tc.memberId === employee.id && + tc.completedAt !== null && + tc.videoId !== HIPAA_TRAINING_ID, ).length : 0; + const hipaaCompleted = + hasHipaaFramework && + trainingCompletions.some( + (tc) => + tc.memberId === employee.id && + tc.videoId === HIPAA_TRAINING_ID && + tc.completedAt !== null, + ) + ? 1 + : 0; + taskCompletionMap[employee.id] = { - completed: policiesCompleted + trainingsCompleted, + completed: policiesCompleted + trainingsCompleted + hipaaCompleted, total: totalTasks, + policies: { completed: policiesCompleted, total: totalPolicies }, + training: { completed: trainingsCompleted, total: totalTrainingVideos }, + ...(hasHipaaFramework && { + hipaa: { completed: hipaaCompleted, total: 1 }, + }), }; } } diff --git a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx index b6657d5624..f0e6268373 100644 --- a/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/all/components/TeamMembersClient.tsx @@ -39,7 +39,7 @@ import { apiClient } from '@/lib/api-client'; import { buildDisplayItems, filterDisplayItems } from './filter-members'; import { MemberRow } from './MemberRow'; import { PendingInvitationRow } from './PendingInvitationRow'; -import type { MemberWithUser, TeamMembersData } from './TeamMembers'; +import type { MemberWithUser, TaskCompletion, TeamMembersData } from './TeamMembers'; import type { EmployeeSyncConnectionsData } from '../data/queries'; import { useEmployeeSync } from '../hooks/useEmployeeSync'; @@ -52,7 +52,7 @@ interface TeamMembersClientProps { isAuditor: boolean; isCurrentUserOwner: boolean; employeeSyncData: EmployeeSyncConnectionsData; - taskCompletionMap: Record; + taskCompletionMap: Record; memberIdsWithDeviceAgent: string[]; } diff --git a/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeeCompletionChart.tsx b/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeeCompletionChart.tsx index 3ddc0bf2e0..39873551a2 100644 --- a/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeeCompletionChart.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeeCompletionChart.tsx @@ -329,7 +329,7 @@ export function EmployeeCompletionChart({ } function TaskBarChart({ stat }: { stat: EmployeeTaskStats }) { - const totalCompleted = stat.policiesCompleted + stat.trainingsCompleted; + const totalCompleted = stat.policiesCompleted + stat.trainingsCompleted + (stat.hipaaCompleted ? 1 : 0); const totalIncomplete = stat.totalTasks - totalCompleted; const barHeight = 12;