From a5ebe7778dee104affff0480772e5f980e8402c6 Mon Sep 17 00:00:00 2001 From: Tofik Hasanov Date: Wed, 13 May 2026 18:04:28 -0400 Subject: [PATCH 1/6] feat(background-check): redesign overview and report to surface real verification methodology Replaces generic benefit bullets ("Required for Compliance", "Social media verifications") with a marketing-forward methodology block that names what we actually do: biometric ID + liveness, HR-confirmed employment, structured reference questionnaires, AI-augmented public-source research, and FCRA-style adjudication. The completed report now opens with a methodology banner and each section shows its verification method as a subtitle. Renames "Social and media research" to "Public-source research" so LinkedIn stops reading as the whole product. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/BackgroundCheckMethodology.tsx | 143 ++++++++++++++++++ .../components/BackgroundCheckReport.tsx | 44 ++++-- .../BackgroundCheckStatusView.test.tsx | 4 +- .../components/BackgroundCheckWizardParts.tsx | 34 ++--- .../EmployeeBackgroundCheck.test.tsx | 14 +- 5 files changed, 196 insertions(+), 43 deletions(-) create mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckMethodology.tsx diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckMethodology.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckMethodology.tsx new file mode 100644 index 0000000000..8b5a0accfd --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckMethodology.tsx @@ -0,0 +1,143 @@ +'use client'; + +import { Badge, Stack, Text } from '@trycompai/design-system'; +import { + CheckmarkFilled, + Document, + Locked, + MagicWand, + Security, + UserMultiple, +} from '@trycompai/design-system/icons'; +import type { ComponentType } from 'react'; + +type IconComponent = ComponentType<{ size?: number }>; + +const TRUST_SIGNALS: Array<{ icon: IconComponent; label: string }> = [ + { icon: Security, label: 'Biometric identity verification' }, + { icon: UserMultiple, label: 'Human-verified employment & references' }, + { icon: Locked, label: 'FCRA-compliant adjudication' }, + { icon: MagicWand, label: 'AI-augmented public-source research' }, +]; + +const CHECKS_INCLUDED: Array<{ title: string; description: string }> = [ + { + title: 'Identity & liveness', + description: + 'Candidate uploads a government ID and records a live video. The face on the ID, the live capture, and the candidate’s public profile photo are matched against each other.', + }, + { + title: 'Employment confirmation', + description: + 'Each past employer’s HR contact receives a secure verification link to confirm role and dates of employment directly — not just inferred from a profile.', + }, + { + title: 'Reference questionnaires', + description: + 'Each professional reference receives a structured form. We record relationship confirmation, recommendation, and free-text response per reference.', + }, + { + title: 'Right-to-work documentation', + description: + 'Work authorization is extracted from the candidate’s government identity document — passport, national ID, or work visa.', + }, + { + title: 'Cross-referenced public research', + description: + 'LinkedIn and public-source profiles are aggregated by AI, then validated against the candidate’s submitted employment history and identity.', + }, +]; + +export function MethodologyTrustSignals() { + return ( +
+ {TRUST_SIGNALS.map(({ icon: Icon, label }) => ( +
+ + + + + {label} + +
+ ))} +
+ ); +} + +export function MethodologyIncluded() { + return ( + + + What’s included in every check + +
+ {CHECKS_INCLUDED.map((check) => ( +
+ + + + + + {check.title} + + + {check.description} + + +
+ ))} +
+
+ ); +} + +export function MethodologyComplianceNote() { + return ( +
+
+ + + + + + FCRA-style adverse-action workflow + + + Reports follow Fair Credit Reporting Act adverse-action + conventions. Candidates can review and dispute findings before any + decision is final. + + +
+
+ ); +} + +/** Compact methodology banner for the top of the completed report. */ +export function MethodologyReportBanner() { + return ( +
+ + + How this check was performed + + Every report combines biometric identity verification with direct + employer and reference confirmation, plus AI-augmented public-source + research — all under an FCRA-style adjudication workflow. + + +
+ {TRUST_SIGNALS.map(({ label }) => ( + + {label} + + ))} +
+
+
+ ); +} diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckReport.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckReport.tsx index 9e49806f7d..bb22014b52 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckReport.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckReport.tsx @@ -12,26 +12,35 @@ import { Text, } from '@trycompai/design-system'; import { BackgroundCheckExternalReport } from './BackgroundCheckExternalReport'; +import { MethodologyReportBanner } from './BackgroundCheckMethodology'; import { BackgroundCheckShareableSummary, hasShareableSummary, } from './BackgroundCheckShareableSummary'; -const REPORT_SECTIONS = [ +const REPORT_SECTIONS: Array<{ + title: string; + method: string; + paths: string[][]; +}> = [ { - title: 'Identity verification', + title: 'Identity & liveness', + method: 'Government ID + live video, face-matched', paths: [['identityVerification'], ['report', 'identity'], ['report', 'report', 'identity']], }, { title: 'Employment verification', + method: 'HR-confirmed by email per employer', paths: [['employment'], ['report', 'employment'], ['report', 'report', 'employment']], }, { title: 'References', + method: 'Structured questionnaire per reference', paths: [['references'], ['report', 'references'], ['report', 'report', 'references']], }, { - title: 'Social and media research', + title: 'Public-source research', + method: 'LinkedIn + public web, cross-referenced', paths: [ ['linkedinAnalysis'], ['latestResearchRun'], @@ -56,18 +65,20 @@ export function BackgroundCheckReport({ return ( - Report + Verification report {syncedAt && ( Snapshot synced {new Date(syncedAt).toLocaleString()} )} +
{REPORT_SECTIONS.map((section) => ( ))} @@ -84,9 +95,11 @@ export function BackgroundCheckReport({ function ReportSection({ title, + method, value, }: { title: string; + method: string; value: unknown; }) { const status = readStatus(value); @@ -95,12 +108,17 @@ function ReportSection({ return (
-
- {title} - {status && shouldShowStatus(status) && ( - {formatLabel(status)} - )} -
+ +
+ {title} + {status && shouldShowStatus(status) && ( + {formatLabel(status)} + )} +
+ + {method} + +
{description}
@@ -165,18 +183,18 @@ function getPath(root: unknown, path: string[]): unknown { function describeValue(value: unknown): string { const values = toArray(value); if (values.length > 0) { - return `${values.length} item${values.length === 1 ? '' : 's'} recorded`; + return `${values.length} verified entr${values.length === 1 ? 'y' : 'ies'} on file`; } const record = toRecord(value); - if (!record) return 'Not included in the synced report snapshot'; + if (!record) return 'Awaiting candidate submission for this section.'; for (const key of ['summary', 'notes', 'message', 'decision', 'result', 'outcome']) { const text = readString(record[key]); if (text) return truncateSummary(text); } - return `${Object.keys(record).length} report fields captured`; + return `${Object.keys(record).length} verification fields recorded`; } function truncateSummary(value: string): string { diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckStatusView.test.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckStatusView.test.tsx index 2a7af56523..eb994e117b 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckStatusView.test.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckStatusView.test.tsx @@ -109,7 +109,7 @@ describe('BackgroundCheckStatusView', () => { expect(screen.getByText('Complete with flags')).toBeInTheDocument(); expect(screen.queryByRole('button', { name: /copy candidate link/i })).not.toBeInTheDocument(); expect(screen.queryByText(/Adjudication: Draft/i)).not.toBeInTheDocument(); - expect(screen.getByText('Identity verification')).toBeInTheDocument(); + expect(screen.getByText('Identity & liveness')).toBeInTheDocument(); expect(screen.getByText('Shareable summary')).toBeInTheDocument(); expect(screen.getByText('The background check is completed with flags.')).toBeInTheDocument(); expect( @@ -119,7 +119,7 @@ describe('BackgroundCheckStatusView', () => { expect(screen.getByText('Name mismatch needs review')).toBeInTheDocument(); expect(screen.getByText('Public source data can change.')).toBeInTheDocument(); expect(screen.getByText('Employment verification')).toBeInTheDocument(); - expect(screen.getByText('Social and media research')).toBeInTheDocument(); + expect(screen.getByText('Public-source research')).toBeInTheDocument(); expect(screen.queryByText(/Right-to-work/i)).not.toBeInTheDocument(); expect(screen.queryByText(/Right to work/i)).not.toBeInTheDocument(); expect(screen.queryByText('Not Found')).not.toBeInTheDocument(); diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckWizardParts.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckWizardParts.tsx index 09b02c66df..2f4e232f48 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckWizardParts.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckWizardParts.tsx @@ -1,17 +1,13 @@ 'use client'; import { Button, HStack, Stack, Text } from '@trycompai/design-system'; -import { CheckmarkFilled, Launch } from '@trycompai/design-system/icons'; +import { Launch } from '@trycompai/design-system/icons'; import Link from 'next/link'; - -const BENEFITS = [ - 'Required for Compliance', - 'Full audited report / background check', - 'Identity verification', - 'Previous employer verification + checks', - 'Reference checks', - 'Social media verifications', -]; +import { + MethodologyComplianceNote, + MethodologyIncluded, + MethodologyTrustSignals, +} from './BackgroundCheckMethodology'; export function OverviewStep({ canRequest, @@ -85,23 +81,17 @@ export function BackgroundCheckSummary() {
- Employee Background Check + Background-checked the right way
- Streamline employee background checks with Comp AI. + A serious check, designed for compliance hiring — biometric, + human-verified, and FCRA-aligned.
-
- {BENEFITS.map((benefit) => ( -
- - - - {benefit} -
- ))} -
+ + +
); } diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeBackgroundCheck.test.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeBackgroundCheck.test.tsx index 4e1d530962..1020b4b4e6 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeBackgroundCheck.test.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeBackgroundCheck.test.tsx @@ -120,16 +120,18 @@ describe('EmployeeBackgroundCheck', () => { }); }); - it('renders overview benefits before the form', () => { + it('renders methodology overview before the form', () => { renderSection({ initialBillingStatus: { hasPaymentMethod: false, setupAt: null }, }); - expect(screen.getByText('Employee Background Check')).toBeInTheDocument(); - expect(screen.getByText('Required for Compliance')).toBeInTheDocument(); - expect(screen.getByText('Full audited report / background check')).toBeInTheDocument(); + expect(screen.getByText('Background-checked the right way')).toBeInTheDocument(); + expect(screen.getByText('Biometric identity verification')).toBeInTheDocument(); + expect(screen.getByText('FCRA-compliant adjudication')).toBeInTheDocument(); expect( - screen.getByText('Streamline employee background checks with Comp AI.'), + screen.getByText( + 'A serious check, designed for compliance hiring — biometric, human-verified, and FCRA-aligned.', + ), ).toBeInTheDocument(); expect(screen.getByText('$79 / month')).toBeInTheDocument(); expect(screen.queryByText(/charged \$49/i)).not.toBeInTheDocument(); @@ -143,7 +145,7 @@ describe('EmployeeBackgroundCheck', () => { it('skips the overview when a payment method is already saved', () => { renderSection(); - expect(screen.getByText('Employee Background Check')).toBeInTheDocument(); + expect(screen.getByText('Background-checked the right way')).toBeInTheDocument(); expect(screen.getByLabelText('Personal email')).toBeInTheDocument(); expect(screen.getByText('2 background checks remaining this period.')).toBeInTheDocument(); expect(screen.queryByRole('button', { name: /back/i })).not.toBeInTheDocument(); From 4c81b38bafe7d7daeeea8e5145b6b060f066ff73 Mon Sep 17 00:00:00 2001 From: Tofik Hasanov Date: Wed, 13 May 2026 18:51:39 -0400 Subject: [PATCH 2/6] feat(background-check): replace marketing overview with V1 two-paths task surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the V1 design from `design_handoff_background_check/SPEC.md`: the Background Check tab's empty state is now a unified task-oriented page where Order / Attach / Exempt are equal first-class decisions, instead of a marketing-style benefits grid followed by a separate form and upload UI. Page anatomy (top to bottom): status strip with credits + plan link → path picker (3 selectable cards) → contextual body (Order / Attach / Exempt form per selection, form state preserved across switches) → collapsible "What's verified in this check" scope panel. The path picker is a true radiogroup (role=radio, ArrowLeft/Right navigation, Space/Enter to select). Out-of-credits disables Send invite; missing PDF disables Attach report; missing reason disables Confirm exemption. Wiring: - Order → POST /v1/people/:id/background-check (existing endpoint) - Attach → POST /v1/people/:id/background-check/custom (existing) - Exempt → PATCH /v1/people/:id with backgroundCheckExempt + reason + justification (latter two are new; backend ignores until DTO is extended — flagged in PR description) Removed: BackgroundCheckMethodology, BackgroundCheckWizardParts (Overview / BackgroundCheckSummary / BillingCallout), BackgroundCheckDetailsForm, CustomBackgroundCheckUpload, PaymentMethodUpdateDialog, react-hook-form glue for the wizard, sessionStorage pending-request stash, and the 402 "out of credits → redirect to billing" flow (V1 just disables the button and exposes "Choose a plan →" in the status strip). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/BackgroundCheckAttachForm.tsx | 197 ++++++++ .../BackgroundCheckAuditTimeline.tsx | 107 +++++ .../components/BackgroundCheckDetailsForm.tsx | 122 ----- .../components/BackgroundCheckExemptForm.tsx | 141 ++++++ .../components/BackgroundCheckFormHelpers.tsx | 67 +++ .../components/BackgroundCheckMethodology.tsx | 143 ------ .../components/BackgroundCheckNotices.tsx | 59 +++ .../components/BackgroundCheckOrderForm.tsx | 118 +++++ .../components/BackgroundCheckPathCard.tsx | 101 +++++ .../components/BackgroundCheckReport.tsx | 138 ++---- .../components/BackgroundCheckScopePanel.tsx | 123 +++++ .../components/BackgroundCheckStatusStrip.tsx | 101 +++++ .../components/BackgroundCheckV1Page.tsx | 189 ++++++++ .../components/BackgroundCheckWizardParts.tsx | 137 ------ .../CustomBackgroundCheckUpload.test.tsx | 84 ---- .../CustomBackgroundCheckUpload.tsx | 141 ------ .../EmployeeBackgroundCheck.test.tsx | 369 +++++---------- .../components/EmployeeBackgroundCheck.tsx | 423 ++++++------------ .../components/PaymentMethodUpdateDialog.tsx | 71 --- .../components/backgroundCheckForm.ts | 90 ---- .../components/backgroundCheckUtils.ts | 49 ++ 21 files changed, 1541 insertions(+), 1429 deletions(-) create mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckAttachForm.tsx create mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckAuditTimeline.tsx delete mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckDetailsForm.tsx create mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckExemptForm.tsx create mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckFormHelpers.tsx delete mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckMethodology.tsx create mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckNotices.tsx create mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckOrderForm.tsx create mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckPathCard.tsx create mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckScopePanel.tsx create mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckStatusStrip.tsx create mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckV1Page.tsx delete mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckWizardParts.tsx delete mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/CustomBackgroundCheckUpload.test.tsx delete mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/CustomBackgroundCheckUpload.tsx delete mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/PaymentMethodUpdateDialog.tsx delete mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/backgroundCheckForm.ts create mode 100644 apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/backgroundCheckUtils.ts diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckAttachForm.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckAttachForm.tsx new file mode 100644 index 0000000000..cc7a4fb3f3 --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckAttachForm.tsx @@ -0,0 +1,197 @@ +'use client'; + +import { + Button, + Input, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Text, +} from '@trycompai/design-system'; +import { Upload } from '@trycompai/design-system/icons'; +import { useRef, useState, type ChangeEvent, type DragEvent } from 'react'; +import { + FormFooterInfo, + FormFooterRow, + LabelRow, +} from './BackgroundCheckFormHelpers'; + +export const VENDOR_OPTIONS = [ + { value: 'checkr', label: 'Checkr' }, + { value: 'sterling', label: 'Sterling' }, + { value: 'hireright', label: 'HireRight' }, + { value: 'goodhire', label: 'Goodhire' }, + { value: 'other', label: 'Other' }, +] as const; + +export interface AttachFormValues { + vendor: string; + reportDate: string; + file: File | null; +} + +interface AttachFormProps { + values: AttachFormValues; + onChange: (next: AttachFormValues) => void; + onSubmit: () => void; + onBack?: () => void; + submitting: boolean; + canSubmit: boolean; +} + +const MAX_FILE_BYTES = 25 * 1024 * 1024; + +export function BackgroundCheckAttachForm({ + values, + onChange, + onSubmit, + onBack, + submitting, + canSubmit, +}: AttachFormProps) { + const inputRef = useRef(null); + const [dragOver, setDragOver] = useState(false); + const [fileError, setFileError] = useState(null); + + const setField = (key: K, value: AttachFormValues[K]) => { + onChange({ ...values, [key]: value }); + }; + + const acceptFile = (file: File | undefined) => { + if (!file) return; + if (file.size > MAX_FILE_BYTES) { + setFileError('File exceeds 25 MB limit.'); + return; + } + if (file.type && file.type !== 'application/pdf') { + setFileError('Only PDF files are accepted.'); + return; + } + setFileError(null); + setField('file', file); + }; + + const handleFileChange = (event: ChangeEvent) => { + acceptFile(event.target.files?.[0]); + }; + + const handleBrowse = () => inputRef.current?.click(); + + const handleDrop = (event: DragEvent) => { + event.preventDefault(); + setDragOver(false); + acceptFile(event.dataTransfer.files?.[0]); + }; + + return ( +
event.preventDefault()}> +
+
+ + Vendor + + +
+
+ + Report date + +
+ setField('reportDate', event.target.value)} + /> +
+
+
+ +
{ + event.preventDefault(); + setDragOver(true); + }} + onDragLeave={() => setDragOver(false)} + onDrop={handleDrop} + className={`mb-4 rounded-[var(--radius)] border-[1.5px] border-dashed px-4 py-7 text-center transition-colors ${ + dragOver + ? 'border-primary bg-[oklch(0.985_0.012_167)]' + : 'border-border bg-muted' + }`} + > +
+ +
+ + {values.file ? ( + <>Selected: {values.file.name} + ) : ( + <> + Drop the PDF here, or{' '} + + + )} + + + PDF · up to 25 MB · stored encrypted in your evidence vault + + {fileError && ( +

{fileError}

+ )} +
+ + We extract status and key fields automatically. You'll get to confirm before + saving. + + } + > + {onBack && ( + + )} + + +
+ ); +} diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckAuditTimeline.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckAuditTimeline.tsx new file mode 100644 index 0000000000..b7cacc99f3 --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckAuditTimeline.tsx @@ -0,0 +1,107 @@ +'use client'; + +import { + Badge, + Stack, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + Text, +} from '@trycompai/design-system'; + +export function BackgroundCheckAuditTimeline({ events }: { events: unknown[] }) { + const sortedEvents = [...events].sort((a, b) => eventTimestamp(b) - eventTimestamp(a)); + + return ( + + Audit timeline + + + + Event + Time + + + + {sortedEvents.slice(0, 5).map((event, index) => ( + + +
+ {eventCategory(event)} + {eventLabel(event)} +
+
+ + + {eventTime(event) ?? '-'} + + +
+ ))} +
+
+
+ ); +} + +function eventLabel(event: unknown): string { + const record = toRecord(event); + if (!record) return 'Report event'; + const label = + readString(record.eventType) ?? + readString(record.type) ?? + readString(record.message) ?? + 'Report event'; + return formatEventLabel(label); +} + +function eventCategory(event: unknown): string { + const record = toRecord(event); + if (!record) return 'Report'; + const label = readString(record.eventType) ?? readString(record.type) ?? ''; + return formatLabel(label.split('.')[0] || 'report'); +} + +function eventTime(event: unknown): string | null { + const timestamp = eventTimestamp(event); + return timestamp > 0 ? new Date(timestamp).toLocaleString() : null; +} + +function eventTimestamp(event: unknown): number { + const record = toRecord(event); + if (!record) return 0; + const value = record.createdAt ?? record.timestamp ?? record.processedAt; + if (typeof value === 'number') return value; + if (typeof value === 'string') { + const numeric = Number(value); + if (!Number.isNaN(numeric) && value.trim() !== '') return numeric; + const timestamp = new Date(value).getTime(); + return Number.isNaN(timestamp) ? 0 : timestamp; + } + return 0; +} + +function eventKey(event: unknown, index: number): string { + const record = toRecord(event); + return readString(record?.id) ?? readString(record?.eventId) ?? `event-${index}`; +} + +function toRecord(value: unknown): Record | null { + if (!value || typeof value !== 'object' || Array.isArray(value)) return null; + return value as Record; +} + +function readString(value: unknown): string | null { + return typeof value === 'string' && value.trim() ? value : null; +} + +function formatLabel(value: string): string { + return value.replace(/_/g, ' ').replace(/\b\w/g, (letter) => letter.toUpperCase()); +} + +function formatEventLabel(value: string): string { + return formatLabel(value.split('.').slice(1).join('.') || value).replace(/\./g, ' '); +} diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckDetailsForm.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckDetailsForm.tsx deleted file mode 100644 index 088a93040b..0000000000 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/BackgroundCheckDetailsForm.tsx +++ /dev/null @@ -1,122 +0,0 @@ -'use client'; - -import { Button, Grid, Input, Label, Stack, Text, Textarea } from '@trycompai/design-system'; -import Link from 'next/link'; -import type { UseFormReturn } from 'react-hook-form'; -import { BackgroundCheckSummary, BillingCallout } from './BackgroundCheckWizardParts'; -import type { BackgroundCheckFormValues } from './backgroundCheckForm'; - -export function BackgroundCheckDetailsForm({ - canRequest, - form, - isOpeningBilling, - isRequesting, - billingSetupComplete, - backgroundChecksRemaining, - billingHref, - canGoBack, - onBack, - onSubmit, -}: { - canRequest: boolean; - form: UseFormReturn; - isOpeningBilling: boolean; - isRequesting: boolean; - billingSetupComplete: boolean; - backgroundChecksRemaining: number | null; - billingHref: string; - canGoBack: boolean; - onBack: () => void; - onSubmit: (values: BackgroundCheckFormValues) => Promise; -}) { - return ( -
- - -
- {billingSetupComplete && ( - - )} - - - - - {form.formState.errors.employeeName && ( - - {form.formState.errors.employeeName.message} - - )} - - - - - {form.formState.errors.employeeEmail && ( - - {form.formState.errors.employeeEmail.message} - - )} - - - - -