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
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement | null>(null);
const [dragOver, setDragOver] = useState(false);
const [fileError, setFileError] = useState<string | null>(null);

const setField = <K extends keyof AttachFormValues>(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<HTMLInputElement>) => {
acceptFile(event.target.files?.[0]);
};

const handleBrowse = () => inputRef.current?.click();

const handleDrop = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
setDragOver(false);
acceptFile(event.dataTransfer.files?.[0]);
};

return (
<form noValidate onSubmit={(event) => event.preventDefault()}>
<div className="mb-4 grid gap-4 md:grid-cols-2">
<div>
<LabelRow htmlFor="bg-attach-vendor" required>
Vendor
</LabelRow>
<Select
value={values.vendor}
onValueChange={(next) => setField('vendor', next ?? '')}
>
<SelectTrigger id="bg-attach-vendor">
<SelectValue placeholder="Select a vendor" />
</SelectTrigger>
<SelectContent>
{VENDOR_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<LabelRow htmlFor="bg-attach-date" hint="As shown on the report">
Report date
</LabelRow>
<div className="font-mono">
<Input
id="bg-attach-date"
type="date"
value={values.reportDate}
onChange={(event) => setField('reportDate', event.target.value)}
/>
</div>
</div>
</div>
<input
ref={inputRef}
type="file"
accept="application/pdf"
className="sr-only"
onChange={handleFileChange}
aria-label="Background check PDF"
/>
<div
onDragOver={(event) => {
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'
}`}
>
<div className="mb-1.5 flex justify-center text-muted-foreground">
<Upload size={20} />
</div>
<Text size="sm">
{values.file ? (
<>Selected: {values.file.name}</>
) : (
<>
Drop the PDF here, or{' '}
<button
type="button"
onClick={handleBrowse}
className="text-primary underline underline-offset-2 hover:opacity-80"
>
browse files
</button>
</>
)}
</Text>
<Text size="xs" variant="muted">
PDF · up to 25 MB · stored encrypted in your evidence vault
</Text>
{fileError && (
<p className="mt-2 text-xs text-destructive">{fileError}</p>
)}
</div>
<FormFooterRow
info={
<FormFooterInfo>
We extract status and key fields automatically. You&apos;ll get to confirm before
saving.
</FormFooterInfo>
}
>
{onBack && (
<Button type="button" variant="outline" size="lg" onClick={onBack} disabled={submitting}>
Back
</Button>
)}
<Button
type="button"
size="lg"
loading={submitting}
disabled={!canSubmit || submitting}
onClick={onSubmit}
>
Attach report
</Button>
</FormFooterRow>
</form>
);
}
Original file line number Diff line number Diff line change
@@ -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 (
<Stack gap="sm">
<Text weight="medium">Audit timeline</Text>
<Table variant="bordered">
<TableHeader>
<TableRow>
<TableHead>Event</TableHead>
<TableHead>Time</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sortedEvents.slice(0, 5).map((event, index) => (
<TableRow key={eventKey(event, index)}>
<TableCell>
<div className="flex min-w-0 items-center gap-2">
<Badge variant="secondary">{eventCategory(event)}</Badge>
<Text size="sm">{eventLabel(event)}</Text>
</div>
</TableCell>
<TableCell>
<Text size="sm" variant="muted">
{eventTime(event) ?? '-'}
</Text>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Stack>
);
}

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<string, unknown> | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
return value as Record<string, unknown>;
}

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, ' ');
}
Loading
Loading