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;