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