diff --git a/src/components/CSVColumnMapper/CSVColumnMapper.stories.tsx b/src/components/CSVColumnMapper/CSVColumnMapper.stories.tsx index 27651e9f..ed28bcfc 100644 --- a/src/components/CSVColumnMapper/CSVColumnMapper.stories.tsx +++ b/src/components/CSVColumnMapper/CSVColumnMapper.stories.tsx @@ -247,10 +247,6 @@ export const CustomLabels: Story = { includeAll: 'Map All Columns', ignoreUncompleted: 'Skip Unmapped', import: 'Start Import', - ignore: 'Skip', - include: 'Map', - incomingSample: 'Sample Data', - fieldType: 'Map To', ensureAccurateData: 'Data Validation', ensureAccurateDataDescription: 'Matching records will be updated automatically.', diff --git a/src/components/CheckrIntegration/CheckrIntegration.stories.tsx b/src/components/CheckrIntegration/CheckrIntegration.stories.tsx index 80f7d5cf..15382343 100644 --- a/src/components/CheckrIntegration/CheckrIntegration.stories.tsx +++ b/src/components/CheckrIntegration/CheckrIntegration.stories.tsx @@ -5,15 +5,6 @@ import { type BackgroundCheckReport, } from './CheckrIntegration'; -const meta: Meta = { - title: 'Components/CheckrIntegration', - component: CheckrIntegration, - tags: ['autodocs'], -}; - -export default meta; -type Story = StoryObj; - const samplePackages = [ { id: 'basic', @@ -91,6 +82,98 @@ const sampleReports: BackgroundCheckReport[] = [ }, ]; +const meta: Meta = { + title: 'Components/CheckrIntegration', + component: CheckrIntegration, + tags: ['autodocs'], + argTypes: { + connected: { + control: 'boolean', + description: 'Whether Checkr is connected', + table: { + defaultValue: { summary: 'false' }, + }, + }, + account: { + control: 'object', + description: 'Account information (name, plan)', + }, + reports: { + control: 'object', + description: 'Array of background check reports', + }, + packages: { + control: 'object', + description: 'Available background check packages', + }, + loading: { + control: 'boolean', + description: 'Loading state', + table: { + defaultValue: { summary: 'false' }, + }, + }, + error: { + control: 'text', + description: 'Error message to display', + }, + onConnect: { + action: 'connect', + description: 'Callback when Connect button is clicked', + }, + onDisconnect: { + action: 'disconnect', + description: 'Callback when Disconnect button is clicked', + }, + onInviteCandidate: { + action: 'inviteCandidate', + description: 'Callback when a candidate is invited', + }, + onViewReport: { + action: 'viewReport', + description: 'Callback when a single report is viewed', + }, + onViewSelected: { + action: 'viewSelected', + description: 'Callback when View Details is clicked for selected reports', + }, + onExportSelected: { + action: 'exportSelected', + description: 'Callback when Export Selected is clicked', + }, + onRefresh: { + action: 'refresh', + description: 'Callback when Refresh button is clicked', + }, + labels: { + control: 'object', + description: 'Custom labels for UI text', + }, + className: { + control: 'text', + description: 'Additional CSS classes', + }, + }, + args: { + packages: samplePackages, + }, +}; + +export default meta; +type Story = StoryObj; + +// Playground story that respects all Storybook controls +export const Playground: Story = { + args: { + connected: true, + account: { name: 'BlueHive Inc.', plan: 'Enterprise' }, + reports: sampleReports, + packages: samplePackages, + loading: false, + error: '', + }, +}; + // Wrapper for Default story with interactive state function CheckrIntegrationWrapper() { const [connected, setConnected] = useState(true); @@ -117,13 +200,18 @@ function CheckrIntegrationWrapper() { }, ]); }} - onViewReport={(report) => window.open(report.reportUrl, '_blank')} + onViewReport={(report) => { + console.log('View report:', report); + if (report.reportUrl) window.open(report.reportUrl, '_blank'); + }} + onViewSelected={(reports) => console.log('View selected:', reports)} + onExportSelected={(reports) => console.log('Export selected:', reports)} onRefresh={() => console.log('Refresh')} /> ); } -export const Default: Story = { +export const InteractiveDemo: Story = { render: () => , }; diff --git a/src/components/CheckrIntegration/CheckrIntegration.tsx b/src/components/CheckrIntegration/CheckrIntegration.tsx index e99e1503..f9a813f4 100644 --- a/src/components/CheckrIntegration/CheckrIntegration.tsx +++ b/src/components/CheckrIntegration/CheckrIntegration.tsx @@ -2,6 +2,17 @@ import * as React from 'react'; import { cn } from '../../utils'; +import { Button } from '../Button'; +import { Input } from '../Input'; +import { Select, type SelectOption } from '../Select'; +import { + Modal, + ModalHeader, + ModalTitle, + ModalClose, + ModalBody, + ModalFooter, +} from '../Modal'; export interface BackgroundCheckCandidate { /** Candidate ID */ @@ -56,6 +67,10 @@ export interface CheckrIntegrationProps { ) => void; /** Callback to view a report */ onViewReport?: (report: BackgroundCheckReport) => void; + /** Callback to view selected reports */ + onViewSelected?: (reports: BackgroundCheckReport[]) => void; + /** Callback to export selected reports */ + onExportSelected?: (reports: BackgroundCheckReport[]) => void; /** Callback to refresh reports */ onRefresh?: () => void; /** Loading state */ @@ -86,6 +101,9 @@ export interface CheckrIntegrationProps { package?: string; submit?: string; cancel?: string; + exportSelected?: string; + viewDetails?: string; + noReportsSelected?: string; }; } @@ -98,6 +116,8 @@ export function CheckrIntegration({ onDisconnect, onInviteCandidate, onViewReport, + onViewSelected, + onExportSelected, onRefresh, loading = false, error, @@ -125,6 +145,9 @@ export function CheckrIntegration({ package: packageLabel = 'Package', submit = 'Send Invitation', cancel = 'Cancel', + exportSelected = 'Export Selected', + viewDetails = 'View Details', + noReportsSelected = 'No reports selected', } = labels; const [showInviteModal, setShowInviteModal] = React.useState(false); @@ -134,6 +157,15 @@ export function CheckrIntegration({ const [selectedPackage, setSelectedPackage] = React.useState( packages[0]?.id || '' ); + const [selectedReports, setSelectedReports] = React.useState>( + new Set() + ); + + // Reset selected reports when the available reports change + // or when the integration is disconnected to avoid stale selections. + React.useEffect(() => { + setSelectedReports(new Set()); + }, [reports, connected]); const statusLabels: Record = { pending, @@ -149,20 +181,54 @@ export function CheckrIntegration({ adverse_action: adverseAction, }; - const statusColors: Record = { - pending: 'bg-yellow-100 text-yellow-800', - running: 'bg-blue-100 text-blue-800', - complete: 'bg-green-100 text-green-800', - failed: 'bg-red-100 text-red-800', - expired: 'bg-gray-100 text-gray-800', + // Status badge styles using design system tokens + const statusStyles: Record = { + pending: 'border-warning text-warning bg-warning/10', + running: 'border-warning text-warning bg-warning/10', + complete: 'border-success text-success bg-success/10', + failed: 'border-destructive text-destructive bg-destructive/10', + expired: 'border-muted-foreground text-muted-foreground bg-muted', }; - const resultColors: Record = { - clear: 'text-green-600', - consider: 'text-yellow-600', - adverse_action: 'text-red-600', + // Result text colors using design system tokens + const resultStyles: Record = { + clear: 'text-success', + consider: 'text-warning', + adverse_action: 'text-destructive', }; + // Status dot colors for summary + const statusDotColors: Record = { + pending: 'bg-warning', + running: 'bg-warning', + complete: 'bg-success', + failed: 'bg-destructive', + expired: 'bg-muted-foreground', + }; + + // Calculate status counts + const statusCounts = React.useMemo(() => { + const counts: Record = { + pending: 0, + running: 0, + complete: 0, + failed: 0, + expired: 0, + }; + reports.forEach((report) => { + if (counts[report.status] !== undefined) { + counts[report.status]++; + } + }); + return counts; + }, [reports]); + + // Convert packages to SelectOption format + const packageOptions: SelectOption[] = packages.map((pkg) => ({ + value: pkg.id, + label: pkg.name, + })); + const handleInviteSubmit = (e: React.FormEvent) => { e.preventDefault(); if (candidateName && candidateEmail && selectedPackage) { @@ -181,6 +247,32 @@ export function CheckrIntegration({ } }; + const handleToggleReport = (reportId: string) => { + setSelectedReports((prev) => { + const next = new Set(prev); + if (next.has(reportId)) { + next.delete(reportId); + } else { + next.add(reportId); + } + return next; + }); + }; + + const handleViewSelected = () => { + const selected = reports.filter((r) => selectedReports.has(r.id)); + if (onViewSelected) { + onViewSelected(selected); + } else if (selected.length === 1 && onViewReport) { + onViewReport(selected[0]); + } + }; + + const handleExportSelected = () => { + const selected = reports.filter((r) => selectedReports.has(r.id)); + onExportSelected?.(selected); + }; + const formatDate = (date?: Date | string) => { if (!date) return ''; const d = typeof date === 'string' ? new Date(date) : date; @@ -192,11 +284,23 @@ export function CheckrIntegration({ {/* Header */}
-
- +
+ + +
-

Checkr

+

Checkr

{connected && account?.name && (

{account.name} @@ -209,29 +313,45 @@ export function CheckrIntegration({

{connected ? ( - + ) : ( - + )}
{/* Error State */} {error && ( -
- +
+ + + {error}
)} @@ -239,30 +359,69 @@ export function CheckrIntegration({ {/* Connected State */} {connected && ( <> + {/* Status Summary */} + {reports.length > 0 && ( +
+ {Object.entries(statusCounts) + .filter(([, count]) => count > 0) + .map(([status, count]) => ( +
+ + + {count} {statusLabels[status]?.toLowerCase()} + +
+ ))} +
+ )} + {/* Actions */}
- - + +
- {/* Reports List */} -
-
-

{viewReports}

+ {/* Reports Card */} +
+
+

+ {viewReports} +

{loading ? ( @@ -270,31 +429,67 @@ export function CheckrIntegration({
) : reports.length > 0 ? ( -
- {reports.map((report) => ( -
-
-
-

{report.candidate.name}

-

- {report.candidate.email} -

- {report.packageName && ( -

- {report.packageName} + <> +

+ {reports.map((report) => ( +
+
+ {/* Selection Checkbox */} + + + {/* Candidate Info */} +
+

+ {report.candidate.name}

- )} +

+ {report.candidate.email} +

+ {report.packageName && ( +

+ {report.packageName} +

+ )} +
-
-
+ + {/* Status & Date */}
{statusLabels[report.status] || report.status} @@ -303,33 +498,62 @@ export function CheckrIntegration({

{resultLabels[report.result] || report.result}

)} -

+

{formatDate(report.completedAt || report.createdAt)}

- {report.reportUrl && ( - - )}
+ ))} +
+ + {/* Footer */} +
+ + {selectedReports.size > 0 + ? `${selectedReports.size} report${selectedReports.size > 1 ? 's' : ''} selected` + : noReportsSelected} + +
+ +
- ))} -
+
+ ) : (
- + + +

{noReports}

)} @@ -339,114 +563,92 @@ export function CheckrIntegration({ {/* Not Connected State */} {!connected && !error && ( -
- +
+ + +

Connect your Checkr account to run background checks on candidates

- +
)} {/* Invite Modal */} - {showInviteModal && ( -
-
setShowInviteModal(false)} - onKeyDown={(e) => e.key === 'Enter' && setShowInviteModal(false)} - /> -
-

{inviteCandidate}

- -
-
-
- - setCandidateName(e.target.value)} - className="w-full rounded-lg border border-gray-300 px-3 py-2" - required - /> -
- -
- - setCandidateEmail(e.target.value)} - className="w-full rounded-lg border border-gray-300 px-3 py-2" - required - /> -
- -
- - setCandidatePhone(e.target.value)} - className="w-full rounded-lg border border-gray-300 px-3 py-2" - /> -
- -
- - -
-
- -
- - -
-
-
-
- )} + + + {inviteCandidate} + + +
+ +
+ setCandidateName(e.target.value)} + required + /> + setCandidateEmail(e.target.value)} + required + /> + setCandidatePhone(e.target.value)} + /> +