Skip to content

Commit 5a69038

Browse files
authored
Merge pull request #2837 from trycompai/main
[comp] Production Deploy
2 parents f374d1b + ce1564b commit 5a69038

21 files changed

Lines changed: 1561 additions & 1307 deletions
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
'use client';
2+
3+
import {
4+
Button,
5+
Input,
6+
Select,
7+
SelectContent,
8+
SelectItem,
9+
SelectTrigger,
10+
SelectValue,
11+
Text,
12+
} from '@trycompai/design-system';
13+
import { Upload } from '@trycompai/design-system/icons';
14+
import { useRef, useState, type ChangeEvent, type DragEvent } from 'react';
15+
import {
16+
FormFooterInfo,
17+
FormFooterRow,
18+
LabelRow,
19+
} from './BackgroundCheckFormHelpers';
20+
21+
export const VENDOR_OPTIONS = [
22+
{ value: 'checkr', label: 'Checkr' },
23+
{ value: 'sterling', label: 'Sterling' },
24+
{ value: 'hireright', label: 'HireRight' },
25+
{ value: 'goodhire', label: 'Goodhire' },
26+
{ value: 'other', label: 'Other' },
27+
] as const;
28+
29+
export interface AttachFormValues {
30+
vendor: string;
31+
reportDate: string;
32+
file: File | null;
33+
}
34+
35+
interface AttachFormProps {
36+
values: AttachFormValues;
37+
onChange: (next: AttachFormValues) => void;
38+
onSubmit: () => void;
39+
onBack?: () => void;
40+
submitting: boolean;
41+
canSubmit: boolean;
42+
}
43+
44+
const MAX_FILE_BYTES = 25 * 1024 * 1024;
45+
46+
export function BackgroundCheckAttachForm({
47+
values,
48+
onChange,
49+
onSubmit,
50+
onBack,
51+
submitting,
52+
canSubmit,
53+
}: AttachFormProps) {
54+
const inputRef = useRef<HTMLInputElement | null>(null);
55+
const [dragOver, setDragOver] = useState(false);
56+
const [fileError, setFileError] = useState<string | null>(null);
57+
58+
const setField = <K extends keyof AttachFormValues>(key: K, value: AttachFormValues[K]) => {
59+
onChange({ ...values, [key]: value });
60+
};
61+
62+
const acceptFile = (file: File | undefined) => {
63+
if (!file) return;
64+
if (file.size > MAX_FILE_BYTES) {
65+
setFileError('File exceeds 25 MB limit.');
66+
return;
67+
}
68+
if (file.type && file.type !== 'application/pdf') {
69+
setFileError('Only PDF files are accepted.');
70+
return;
71+
}
72+
setFileError(null);
73+
setField('file', file);
74+
};
75+
76+
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
77+
acceptFile(event.target.files?.[0]);
78+
};
79+
80+
const handleBrowse = () => inputRef.current?.click();
81+
82+
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
83+
event.preventDefault();
84+
setDragOver(false);
85+
acceptFile(event.dataTransfer.files?.[0]);
86+
};
87+
88+
return (
89+
<form noValidate onSubmit={(event) => event.preventDefault()}>
90+
<div className="mb-4 grid gap-4 md:grid-cols-2">
91+
<div>
92+
<LabelRow htmlFor="bg-attach-vendor" required>
93+
Vendor
94+
</LabelRow>
95+
<Select
96+
value={values.vendor}
97+
onValueChange={(next) => setField('vendor', next ?? '')}
98+
>
99+
<SelectTrigger id="bg-attach-vendor">
100+
<SelectValue placeholder="Select a vendor" />
101+
</SelectTrigger>
102+
<SelectContent>
103+
{VENDOR_OPTIONS.map((option) => (
104+
<SelectItem key={option.value} value={option.value}>
105+
{option.label}
106+
</SelectItem>
107+
))}
108+
</SelectContent>
109+
</Select>
110+
</div>
111+
<div>
112+
<LabelRow htmlFor="bg-attach-date" hint="As shown on the report">
113+
Report date
114+
</LabelRow>
115+
<div className="font-mono">
116+
<Input
117+
id="bg-attach-date"
118+
type="date"
119+
value={values.reportDate}
120+
onChange={(event) => setField('reportDate', event.target.value)}
121+
/>
122+
</div>
123+
</div>
124+
</div>
125+
<input
126+
ref={inputRef}
127+
type="file"
128+
accept="application/pdf"
129+
className="sr-only"
130+
onChange={handleFileChange}
131+
aria-label="Background check PDF"
132+
/>
133+
<div
134+
onDragOver={(event) => {
135+
event.preventDefault();
136+
setDragOver(true);
137+
}}
138+
onDragLeave={() => setDragOver(false)}
139+
onDrop={handleDrop}
140+
className={`mb-4 rounded-[var(--radius)] border-[1.5px] border-dashed px-4 py-7 text-center transition-colors ${
141+
dragOver
142+
? 'border-primary bg-[oklch(0.985_0.012_167)]'
143+
: 'border-border bg-muted'
144+
}`}
145+
>
146+
<div className="mb-1.5 flex justify-center text-muted-foreground">
147+
<Upload size={20} />
148+
</div>
149+
<Text size="sm">
150+
{values.file ? (
151+
<>Selected: {values.file.name}</>
152+
) : (
153+
<>
154+
Drop the PDF here, or{' '}
155+
<button
156+
type="button"
157+
onClick={handleBrowse}
158+
className="text-primary underline underline-offset-2 hover:opacity-80"
159+
>
160+
browse files
161+
</button>
162+
</>
163+
)}
164+
</Text>
165+
<Text size="xs" variant="muted">
166+
PDF · up to 25 MB · stored encrypted in your evidence vault
167+
</Text>
168+
{fileError && (
169+
<p className="mt-2 text-xs text-destructive">{fileError}</p>
170+
)}
171+
</div>
172+
<FormFooterRow
173+
info={
174+
<FormFooterInfo>
175+
We extract status and key fields automatically. You&apos;ll get to confirm before
176+
saving.
177+
</FormFooterInfo>
178+
}
179+
>
180+
{onBack && (
181+
<Button type="button" variant="outline" size="lg" onClick={onBack} disabled={submitting}>
182+
Back
183+
</Button>
184+
)}
185+
<Button
186+
type="button"
187+
size="lg"
188+
loading={submitting}
189+
disabled={!canSubmit || submitting}
190+
onClick={onSubmit}
191+
>
192+
Attach report
193+
</Button>
194+
</FormFooterRow>
195+
</form>
196+
);
197+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
'use client';
2+
3+
import {
4+
Badge,
5+
Stack,
6+
Table,
7+
TableBody,
8+
TableCell,
9+
TableHead,
10+
TableHeader,
11+
TableRow,
12+
Text,
13+
} from '@trycompai/design-system';
14+
15+
export function BackgroundCheckAuditTimeline({ events }: { events: unknown[] }) {
16+
const sortedEvents = [...events].sort((a, b) => eventTimestamp(b) - eventTimestamp(a));
17+
18+
return (
19+
<Stack gap="sm">
20+
<Text weight="medium">Audit timeline</Text>
21+
<Table variant="bordered">
22+
<TableHeader>
23+
<TableRow>
24+
<TableHead>Event</TableHead>
25+
<TableHead>Time</TableHead>
26+
</TableRow>
27+
</TableHeader>
28+
<TableBody>
29+
{sortedEvents.slice(0, 5).map((event, index) => (
30+
<TableRow key={eventKey(event, index)}>
31+
<TableCell>
32+
<div className="flex min-w-0 items-center gap-2">
33+
<Badge variant="secondary">{eventCategory(event)}</Badge>
34+
<Text size="sm">{eventLabel(event)}</Text>
35+
</div>
36+
</TableCell>
37+
<TableCell>
38+
<Text size="sm" variant="muted">
39+
{eventTime(event) ?? '-'}
40+
</Text>
41+
</TableCell>
42+
</TableRow>
43+
))}
44+
</TableBody>
45+
</Table>
46+
</Stack>
47+
);
48+
}
49+
50+
function eventLabel(event: unknown): string {
51+
const record = toRecord(event);
52+
if (!record) return 'Report event';
53+
const label =
54+
readString(record.eventType) ??
55+
readString(record.type) ??
56+
readString(record.message) ??
57+
'Report event';
58+
return formatEventLabel(label);
59+
}
60+
61+
function eventCategory(event: unknown): string {
62+
const record = toRecord(event);
63+
if (!record) return 'Report';
64+
const label = readString(record.eventType) ?? readString(record.type) ?? '';
65+
return formatLabel(label.split('.')[0] || 'report');
66+
}
67+
68+
function eventTime(event: unknown): string | null {
69+
const timestamp = eventTimestamp(event);
70+
return timestamp > 0 ? new Date(timestamp).toLocaleString() : null;
71+
}
72+
73+
function eventTimestamp(event: unknown): number {
74+
const record = toRecord(event);
75+
if (!record) return 0;
76+
const value = record.createdAt ?? record.timestamp ?? record.processedAt;
77+
if (typeof value === 'number') return value;
78+
if (typeof value === 'string') {
79+
const numeric = Number(value);
80+
if (!Number.isNaN(numeric) && value.trim() !== '') return numeric;
81+
const timestamp = new Date(value).getTime();
82+
return Number.isNaN(timestamp) ? 0 : timestamp;
83+
}
84+
return 0;
85+
}
86+
87+
function eventKey(event: unknown, index: number): string {
88+
const record = toRecord(event);
89+
return readString(record?.id) ?? readString(record?.eventId) ?? `event-${index}`;
90+
}
91+
92+
function toRecord(value: unknown): Record<string, unknown> | null {
93+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
94+
return value as Record<string, unknown>;
95+
}
96+
97+
function readString(value: unknown): string | null {
98+
return typeof value === 'string' && value.trim() ? value : null;
99+
}
100+
101+
function formatLabel(value: string): string {
102+
return value.replace(/_/g, ' ').replace(/\b\w/g, (letter) => letter.toUpperCase());
103+
}
104+
105+
function formatEventLabel(value: string): string {
106+
return formatLabel(value.split('.').slice(1).join('.') || value).replace(/\./g, ' ');
107+
}

0 commit comments

Comments
 (0)