/**
* SidePanel — right-side drawer for document upload and rule testing.
* Slides in over the ReviewScreen without navigation.
*/
import React, { useState, useRef } from 'react'
import { extractDocuments, executeRules, sendReport } from '../api/client'
const STATUS_CONFIG = {
PASS: { label: 'Pass', cls: 'bg-emerald-500/20 text-emerald-300 border-emerald-500/30' },
VIOLATION: { label: 'Violation', cls: 'bg-red-500/20 text-red-300 border-red-500/30' },
SKIPPED: { label: 'Skipped', cls: 'bg-slate-500/20 text-slate-400 border-slate-500/30' },
}
function UploadZone({ label, sublabel, file, onChange, accept = '.pdf' }) {
const inputRef = useRef()
function handleDrop(e) {
e.preventDefault()
const dropped = e.dataTransfer.files[0]
if (dropped) onChange(dropped)
}
return (
inputRef.current?.click()}
onDragOver={(e) => e.preventDefault()}
onDrop={handleDrop}
className={`relative border-2 border-dashed rounded-xl p-4 cursor-pointer transition-all duration-200 text-center
${file
? 'border-emerald-500/50 bg-emerald-950/20'
: 'border-slate-600 hover:border-slate-500 bg-slate-800/30'
}`}
>
onChange(e.target.files[0])}
/>
{label}
{sublabel}
{file ? (
✓ {file.name}
) : (
Click or drag PDF here
)}
)
}
function ResultRow({ result }) {
const cfg = STATUS_CONFIG[result.status] || STATUS_CONFIG.SKIPPED
const [expanded, setExpanded] = useState(result.status === 'VIOLATION')
return (
{expanded && result.deviation_details && (
{result.deviation_details.reason}
{result.action && (
→ Action: {result.action}
)}
)}
)
}
export default function SidePanel({ open, onClose }) {
const [files, setFiles] = useState({ invoice: null, po: null, grn: null })
const [email, setEmail] = useState('')
const [loading, setLoading] = useState(false)
const [loadingMsg, setLoadingMsg] = useState('')
const [results, setResults] = useState(null)
const [payload, setPayload] = useState(null)
const [error, setError] = useState('')
const [sending, setSending] = useState(false)
const [sent, setSent] = useState(false)
function setFile(key) {
return (file) => setFiles((f) => ({ ...f, [key]: file }))
}
async function handleRunAnalysis() {
if (!files.invoice && !files.po && !files.grn) {
setError('Upload at least one document.')
return
}
setError('')
setResults(null)
setLoading(true)
try {
setLoadingMsg('Extracting document data…')
const extractRes = await extractDocuments(files)
const extractedPayload = extractRes.data.payload
setPayload(extractedPayload)
setLoadingMsg('Running rule engine…')
const execRes = await executeRules(extractedPayload)
setResults(execRes.data.results)
} catch (e) {
setError(e.response?.data?.detail || e.message || 'Analysis failed.')
} finally {
setLoading(false)
setLoadingMsg('')
}
}
async function handleSendReport() {
if (!email) { setError('Enter a recipient email.'); return }
if (!results) return
setSending(true)
setError('')
try {
const invoiceNum = payload?.Invoice_table?.invoice_number ?? null
await sendReport(results, email, invoiceNum)
setSent(true)
} catch (e) {
setError(e.response?.data?.detail || e.message || 'Failed to send report.')
} finally {
setSending(false)
}
}
const passed = results?.filter((r) => r.status === 'PASS').length ?? 0
const failed = results?.filter((r) => r.status === 'VIOLATION').length ?? 0
const skipped = results?.filter((r) => r.status === 'SKIPPED').length ?? 0
return (
<>
{/* Backdrop */}
{open && (
)}
{/* Drawer */}
{/* Header */}
Test These Rules
Upload documents to validate against your finalized ruleset
{/* Upload zones */}
Documents
{/* Email */}
setEmail(e.target.value)}
placeholder="ap-team@company.com"
className="w-full bg-slate-800 border border-slate-600 rounded-lg px-3 py-2 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-cyan-500 transition-colors"
/>
{/* Run Analysis */}
{error && (
{error}
)}
{/* Results */}
{results && (
{/* Summary row */}
{/* Overall status */}
{failed === 0 ? '✓ COMPLIANT' : `✗ NON-COMPLIANT — ${failed} violation${failed > 1 ? 's' : ''}`}
{/* Rule rows */}
{results.map((r) => (
))}
{/* Send report */}
)}
>
)
}