Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -50,7 +50,7 @@ interface MemberRowProps {
canEdit: boolean;
isCurrentUserOwner: boolean;
customRoles?: CustomRoleOption[];
taskCompletion?: { completed: number; total: number };
taskCompletion?: TaskCompletion;
hasDeviceAgentDevice?: boolean;
}

Expand Down Expand Up @@ -254,16 +254,28 @@ export function MemberRow({
{/* TASKS */}
<TableCell>
{taskCompletion ? (
<div className="w-[170px]">
<div className="w-[220px]">
<div className="h-2 w-full overflow-hidden rounded-full bg-muted">
<div
className="h-full bg-primary transition-all"
style={{ width: `${taskProgressPercent}%` }}
/>
</div>
<Text size="xs" variant="muted">
{taskCompletion.completed}/{taskCompletion.total} complete
</Text>
<div className="mt-1 flex flex-wrap gap-x-2 gap-y-0.5">
<Text size="xs" variant="muted">
Policies {taskCompletion.policies.completed}/{taskCompletion.policies.total}
</Text>
{taskCompletion.training.total > 0 && (
<Text size="xs" variant="muted">
Training {taskCompletion.training.completed}/{taskCompletion.training.total}
</Text>
)}
{taskCompletion.hipaa && (
<Text size="xs" variant="muted">
HIPAA {taskCompletion.hipaa.completed}/{taskCompletion.hipaa.total}
</Text>
)}
</div>
</div>
) : (
<Text size="sm" variant="muted">
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -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<string, { completed: number; total: number }> = {};
const taskCompletionMap: Record<string, TaskCompletion> = {};

const employeeMembers = await filterComplianceMembers(members, organizationId);

Expand All @@ -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: {
Expand All @@ -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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIPAA completion unreachable when training step disabled

Medium Severity

The trainingCompletions query is gated by securityTrainingStepEnabled, but hipaaCompleted is derived from that same trainingCompletions array. When securityTrainingStepEnabled is false, trainingCompletions is empty, so hipaaCompleted will always be 0 — yet totalHipaaTraining is still 1 when hasHipaaFramework is true. This makes HIPAA tasks permanently incomplete in the progress bar. The dashboard in EmployeesOverview.tsx correctly fetches HIPAA completions independently of that flag.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8ac2416. Configure here.


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 },
}),
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -52,7 +52,7 @@ interface TeamMembersClientProps {
isAuditor: boolean;
isCurrentUserOwner: boolean;
employeeSyncData: EmployeeSyncConnectionsData;
taskCompletionMap: Record<string, { completed: number; total: number }>;
taskCompletionMap: Record<string, TaskCompletion>;
memberIdsWithDeviceAgent: string[];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading