feat(ui): add ClearanceSummaryCard with OFA clearance chips and GRCA eligibility badge
This commit is contained in:
126
client/src/components/ClearanceSummaryCard.jsx
Normal file
126
client/src/components/ClearanceSummaryCard.jsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ShieldCheck, ShieldAlert, ShieldX, Clock, AlertTriangle, Plus } from 'lucide-react'
|
||||
import axios from 'axios'
|
||||
|
||||
const STATUS_CONFIG = {
|
||||
pass: { icon: ShieldCheck, color: 'var(--success)', label: 'Clear', bg: 'rgba(52,199,89,0.1)' },
|
||||
expiring_soon: { icon: Clock, color: 'var(--warning)', label: 'Expiring Soon', bg: 'rgba(255,159,10,0.1)' },
|
||||
expired: { icon: ShieldX, color: 'var(--danger)', label: 'Expired', bg: 'rgba(255,59,48,0.1)' },
|
||||
missing: { icon: ShieldAlert, color: 'var(--text-muted)', label: 'Missing', bg: 'var(--bg-primary)' },
|
||||
}
|
||||
|
||||
const GROUP_LABELS = { hip: 'Hips', elbow: 'Elbows', heart: 'Heart', eye: 'Eyes' }
|
||||
|
||||
function ClearanceChip({ group, status, record }) {
|
||||
const cfg = STATUS_CONFIG[status] || STATUS_CONFIG.missing
|
||||
const Icon = cfg.icon
|
||||
const tip = record
|
||||
? `OFA #${record.ofa_number || '-'} - ${record.ofa_result || record.result || ''}`
|
||||
: 'No record on file'
|
||||
return (
|
||||
<div
|
||||
title={tip}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: '0.4rem',
|
||||
padding: '0.45rem 0.75rem',
|
||||
background: cfg.bg,
|
||||
border: `1px solid ${cfg.color}44`,
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
flex: '1 1 calc(50% - 0.5rem)',
|
||||
minWidth: '140px',
|
||||
}}
|
||||
>
|
||||
<Icon size={15} color={cfg.color} />
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: '0.7rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
||||
{GROUP_LABELS[group]}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.82rem', fontWeight: 500, color: cfg.color }}>
|
||||
{cfg.label}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ClearanceSummaryCard({ dogId, onAddRecord }) {
|
||||
const [data, setData] = useState(null)
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
axios.get(`/api/health/dog/${dogId}/clearance-summary`)
|
||||
.then(r => setData(r.data))
|
||||
.catch(() => setError(true))
|
||||
}, [dogId])
|
||||
|
||||
if (error || !data) return null
|
||||
|
||||
const { summary, grca_eligible, age_eligible, chic_number } = data
|
||||
const hasMissing = Object.values(summary).some(s => s.status === 'missing')
|
||||
const hasExpiring = Object.values(summary).some(s => s.status === 'expiring_soon')
|
||||
|
||||
return (
|
||||
<div className="card" style={{ marginBottom: '1.5rem' }}>
|
||||
{/* Header row */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
||||
<h2 style={{ fontSize: '1rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em', margin: 0 }}>
|
||||
OFA Clearances
|
||||
</h2>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
{grca_eligible && (
|
||||
<span style={{
|
||||
fontSize: '0.7rem', fontWeight: 600, padding: '0.2rem 0.6rem',
|
||||
background: 'rgba(52,199,89,0.15)', color: 'var(--success)',
|
||||
borderRadius: '999px', border: '1px solid rgba(52,199,89,0.3)'
|
||||
}}>GRCA Eligible</span>
|
||||
)}
|
||||
{!age_eligible && (
|
||||
<span style={{
|
||||
fontSize: '0.7rem', fontWeight: 600, padding: '0.2rem 0.6rem',
|
||||
background: 'rgba(255,159,10,0.15)', color: 'var(--warning)',
|
||||
borderRadius: '999px', border: '1px solid rgba(255,159,10,0.3)'
|
||||
}}>Under 24mo</span>
|
||||
)}
|
||||
{chic_number && (
|
||||
<span style={{
|
||||
fontSize: '0.7rem', fontWeight: 600, padding: '0.2rem 0.6rem',
|
||||
background: 'rgba(99,102,241,0.15)', color: '#818cf8',
|
||||
borderRadius: '999px', border: '1px solid rgba(99,102,241,0.3)'
|
||||
}}>CHIC #{chic_number}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Clearance chips */}
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem', marginBottom: '0.75rem' }}>
|
||||
{Object.entries(summary).map(([group, { status, record }]) => (
|
||||
<ClearanceChip key={group} group={group} status={status} record={record} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Expiry warning */}
|
||||
{hasExpiring && (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: '0.5rem',
|
||||
padding: '0.5rem 0.75rem', borderRadius: 'var(--radius-sm)',
|
||||
background: 'rgba(255,159,10,0.08)', border: '1px solid rgba(255,159,10,0.25)',
|
||||
fontSize: '0.8rem', color: 'var(--warning)', marginBottom: '0.5rem'
|
||||
}}>
|
||||
<AlertTriangle size={14} />
|
||||
One or more clearances expire within 90 days. Schedule re-testing.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CTA */}
|
||||
{(hasMissing || onAddRecord) && (
|
||||
<button
|
||||
className="btn btn-ghost"
|
||||
onClick={onAddRecord}
|
||||
style={{ fontSize: '0.8rem', padding: '0.35rem 0.75rem', marginTop: '0.25rem', display: 'flex', alignItems: 'center', gap: '0.3rem' }}
|
||||
>
|
||||
<Plus size={14} /> Add Health Record
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user