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} - - )} - - - - -