import { FINDING_DELTA, FINDING_STATUS, type FindingDelta, type FindingGroupRow, type FindingStatus, } from "@/types"; type FindingGroupDeltaState = Pick< FindingGroupRow, | "newCount" | "changedCount" | "newFailCount" | "newFailMutedCount" | "newPassCount" | "newPassMutedCount" | "newManualCount" | "newManualMutedCount" | "changedFailCount" | "changedFailMutedCount" | "changedPassCount" | "changedPassMutedCount" | "changedManualCount" | "changedManualMutedCount" >; type FindingGroupDelta = Exclude; type FindingGroupStatus = FindingStatus; const FINDING_GROUP_STATUSES = Object.values(FINDING_STATUS); export function isFindingGroupMuted( group: Pick< FindingGroupRow, "muted" | "mutedCount" | "resourcesFail" | "resourcesTotal" >, ): boolean { if (typeof group.muted === "boolean") { return group.muted; } const mutedCount = group.mutedCount ?? 0; if (mutedCount === 0) { return false; } return ( mutedCount === group.resourcesFail || mutedCount === group.resourcesTotal ); } export function getFindingGroupImpactedCounts( group: Pick< FindingGroupRow, | "resourcesTotal" | "resourcesFail" | "passCount" | "failCount" | "manualCount" | "passMutedCount" | "failMutedCount" | "manualMutedCount" | "muted" | "mutedCount" >, ): { impacted: number; total: number } { if (group.resourcesTotal > 0) { return { impacted: group.resourcesFail, total: group.resourcesTotal, }; } const total = (group.passCount ?? 0) + (group.failCount ?? 0) + (group.manualCount ?? 0); if (!isFindingGroupMuted(group)) { return { impacted: group.failCount ?? 0, total, }; } return { impacted: group.failCount ?? 0, total: total + (group.passMutedCount ?? 0) + (group.failMutedCount ?? 0) + (group.manualMutedCount ?? 0), }; } export function canDrillDownFindingGroup( group: Pick, ): boolean { return group.resourcesTotal > 0; } function getNewDeltaTotal(group: FindingGroupDeltaState): number { const breakdownTotal = (group.newFailCount ?? 0) + (group.newFailMutedCount ?? 0) + (group.newPassCount ?? 0) + (group.newPassMutedCount ?? 0) + (group.newManualCount ?? 0) + (group.newManualMutedCount ?? 0); return breakdownTotal > 0 ? breakdownTotal : group.newCount; } function getChangedDeltaTotal(group: FindingGroupDeltaState): number { const breakdownTotal = (group.changedFailCount ?? 0) + (group.changedFailMutedCount ?? 0) + (group.changedPassCount ?? 0) + (group.changedPassMutedCount ?? 0) + (group.changedManualCount ?? 0) + (group.changedManualMutedCount ?? 0); return breakdownTotal > 0 ? breakdownTotal : group.changedCount; } export function getFindingGroupDelta( group: FindingGroupDeltaState, ): FindingGroupDelta { if (getNewDeltaTotal(group) > 0) { return FINDING_DELTA.NEW; } if (getChangedDeltaTotal(group) > 0) { return FINDING_DELTA.CHANGED; } return FINDING_DELTA.NONE; } type FindingGroupFiltersRecord = Record; function parseStatusFilterValue( rawValue: string | string[] | undefined, ): FindingGroupStatus[] { if (!rawValue) { return []; } const joined = Array.isArray(rawValue) ? rawValue.join(",") : rawValue; return joined .split(",") .map((status) => status.trim().toUpperCase()) .filter((status): status is FindingGroupStatus => (FINDING_GROUP_STATUSES as readonly string[]).includes(status), ); } /** * Returns the set of statuses the user has explicitly narrowed the findings * view to, or null when no status filter is active (→ all statuses should be * considered). Supports both `filter[status]` (single value) and * `filter[status__in]` (comma-separated values). */ export function getActiveStatusFilter( filters: FindingGroupFiltersRecord, ): Set | null { const direct = parseStatusFilterValue(filters["filter[status]"]); if (direct.length > 0) { return new Set(direct); } const multi = parseStatusFilterValue(filters["filter[status__in]"]); if (multi.length > 0) { return new Set(multi); } return null; } function hasAnyDeltaBreakdown(group: FindingGroupDeltaState): boolean { return ( (group.newFailCount ?? 0) > 0 || (group.newFailMutedCount ?? 0) > 0 || (group.newPassCount ?? 0) > 0 || (group.newPassMutedCount ?? 0) > 0 || (group.newManualCount ?? 0) > 0 || (group.newManualMutedCount ?? 0) > 0 || (group.changedFailCount ?? 0) > 0 || (group.changedFailMutedCount ?? 0) > 0 || (group.changedPassCount ?? 0) > 0 || (group.changedPassMutedCount ?? 0) > 0 || (group.changedManualCount ?? 0) > 0 || (group.changedManualMutedCount ?? 0) > 0 ); } function getNewDeltaForStatuses( group: FindingGroupDeltaState, statuses: Set, ): number { let total = 0; if (statuses.has(FINDING_STATUS.FAIL)) { total += (group.newFailCount ?? 0) + (group.newFailMutedCount ?? 0); } if (statuses.has(FINDING_STATUS.PASS)) { total += (group.newPassCount ?? 0) + (group.newPassMutedCount ?? 0); } if (statuses.has(FINDING_STATUS.MANUAL)) { total += (group.newManualCount ?? 0) + (group.newManualMutedCount ?? 0); } return total; } function getChangedDeltaForStatuses( group: FindingGroupDeltaState, statuses: Set, ): number { let total = 0; if (statuses.has(FINDING_STATUS.FAIL)) { total += (group.changedFailCount ?? 0) + (group.changedFailMutedCount ?? 0); } if (statuses.has(FINDING_STATUS.PASS)) { total += (group.changedPassCount ?? 0) + (group.changedPassMutedCount ?? 0); } if (statuses.has(FINDING_STATUS.MANUAL)) { total += (group.changedManualCount ?? 0) + (group.changedManualMutedCount ?? 0); } return total; } /** * Filter-aware variant of {@link getFindingGroupDelta}. When a status filter * is active, only delta counters belonging to the filtered statuses contribute * to the indicator. When no status filter is active, or when the API response * lacks breakdown counters (legacy shape), this falls back to the aggregate * delta so rows still surface deltas correctly. */ export function getFilteredFindingGroupDelta( group: FindingGroupDeltaState, filters: FindingGroupFiltersRecord, ): FindingGroupDelta { const activeStatuses = getActiveStatusFilter(filters); if (!activeStatuses || !hasAnyDeltaBreakdown(group)) { return getFindingGroupDelta(group); } if (getNewDeltaForStatuses(group, activeStatuses) > 0) { return FINDING_DELTA.NEW; } if (getChangedDeltaForStatuses(group, activeStatuses) > 0) { return FINDING_DELTA.CHANGED; } return FINDING_DELTA.NONE; }