feat: Display biomarkers by category in collapsible sections on the dashboard, including new UI styles and backend category_id in the API response.

This commit is contained in:
2025-12-19 21:12:02 +05:30
parent d08f63f50c
commit 10e6d2c58a
3 changed files with 145 additions and 4 deletions

View File

@@ -43,6 +43,7 @@ pub struct ReferenceRuleResponse {
#[derive(Serialize)] #[derive(Serialize)]
pub struct BiomarkerListItem { pub struct BiomarkerListItem {
pub id: i32, pub id: i32,
pub category_id: i32,
pub name: String, pub name: String,
pub test_category: String, pub test_category: String,
pub unit: String, pub unit: String,
@@ -62,6 +63,7 @@ pub async fn list_biomarkers(
.into_iter() .into_iter()
.map(|b| BiomarkerListItem { .map(|b| BiomarkerListItem {
id: b.id, id: b.id,
category_id: b.category_id,
name: b.name, name: b.name,
test_category: b.test_category, test_category: b.test_category,
unit: b.unit, unit: b.unit,

View File

@@ -386,4 +386,38 @@ select.input {
.btn-danger:hover { .btn-danger:hover {
background-color: color-mix(in srgb, var(--indicator-critical) 10%, transparent); background-color: color-mix(in srgb, var(--indicator-critical) 10%, transparent);
}
/* Collapsible header */
.collapsible-header:hover {
background-color: var(--bg-secondary) !important;
}
/* Biomarker chips */
.biomarker-chip {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
padding: var(--space-xs) var(--space-sm);
background-color: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-md);
font-size: 13px;
cursor: default;
transition: background-color 0.15s;
}
.biomarker-chip:hover {
background-color: color-mix(in srgb, var(--accent) 10%, var(--bg-secondary));
border-color: var(--accent);
}
.biomarker-name {
font-weight: 500;
color: var(--text-primary);
}
.biomarker-unit {
color: var(--text-secondary);
font-size: 11px;
} }

View File

@@ -8,6 +8,21 @@ interface User {
role: string role: string
} }
interface Category {
id: number
name: string
description: string | null
}
interface Biomarker {
id: number
category_id: number
name: string
test_category: string
unit: string
methodology: string | null
}
export function Dashboard() { export function Dashboard() {
const navigate = useNavigate() const navigate = useNavigate()
const [user, setUser] = useState<User | null>(null) const [user, setUser] = useState<User | null>(null)
@@ -15,6 +30,9 @@ export function Dashboard() {
const [theme, setTheme] = useState(() => const [theme, setTheme] = useState(() =>
document.documentElement.getAttribute('data-theme') || 'light' document.documentElement.getAttribute('data-theme') || 'light'
) )
const [categories, setCategories] = useState<Category[]>([])
const [biomarkers, setBiomarkers] = useState<Biomarker[]>([])
const [expandedCategories, setExpandedCategories] = useState<Set<number>>(new Set())
useEffect(() => { useEffect(() => {
// First get auth info, then fetch full user profile for name // First get auth info, then fetch full user profile for name
@@ -36,6 +54,15 @@ export function Dashboard() {
}) })
.catch(() => navigate('/login')) .catch(() => navigate('/login'))
.finally(() => setLoading(false)) .finally(() => setLoading(false))
// Fetch categories and biomarkers
Promise.all([
fetch('/api/categories', { credentials: 'include' }).then(r => r.json()),
fetch('/api/biomarkers', { credentials: 'include' }).then(r => r.json()),
]).then(([cats, bms]) => {
setCategories(cats)
setBiomarkers(bms)
})
}, [navigate]) }, [navigate])
const handleLogout = async () => { const handleLogout = async () => {
@@ -53,6 +80,22 @@ export function Dashboard() {
setTheme(newTheme) setTheme(newTheme)
} }
const toggleCategory = (categoryId: number) => {
setExpandedCategories(prev => {
const next = new Set(prev)
if (next.has(categoryId)) {
next.delete(categoryId)
} else {
next.add(categoryId)
}
return next
})
}
const getBiomarkersForCategory = (categoryId: number) => {
return biomarkers.filter(b => b.category_id === categoryId)
}
if (loading) { if (loading) {
return <div style={{ padding: 'var(--space-lg)' }}>Loading...</div> return <div style={{ padding: 'var(--space-lg)' }}>Loading...</div>
} }
@@ -86,8 +129,8 @@ export function Dashboard() {
</div> </div>
</header> </header>
<main> <main>
{/* User Welcome Card */}
<div className="card" style={{ marginBottom: 'var(--space-lg)' }}> <div className="card" style={{ marginBottom: 'var(--space-lg)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div> <div>
@@ -102,6 +145,7 @@ export function Dashboard() {
</div> </div>
</div> </div>
{/* Admin Section */}
{user.role === 'admin' && ( {user.role === 'admin' && (
<div className="card" style={{ marginBottom: 'var(--space-lg)' }}> <div className="card" style={{ marginBottom: 'var(--space-lg)' }}>
<h3 style={{ marginBottom: 'var(--space-sm)' }}>Admin</h3> <h3 style={{ marginBottom: 'var(--space-sm)' }}>Admin</h3>
@@ -114,9 +158,70 @@ export function Dashboard() {
</div> </div>
)} )}
<p className="text-secondary text-sm"> {/* Biomarker Categories */}
Dashboard coming soon... <h2 style={{ marginBottom: 'var(--space-md)' }}>Biomarkers</h2>
</p> <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-sm)' }}>
{categories.map(category => {
const categoryBiomarkers = getBiomarkersForCategory(category.id)
const isExpanded = expandedCategories.has(category.id)
return (
<div key={category.id} className="card" style={{ padding: 0 }}>
{/* Category Header - Clickable */}
<button
className="collapsible-header"
onClick={() => toggleCategory(category.id)}
style={{
width: '100%',
padding: 'var(--space-md)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
background: 'none',
border: 'none',
cursor: 'pointer',
textAlign: 'left',
color: 'var(--text-primary)',
}}
>
<div>
<span style={{ fontSize: '16px', fontWeight: 600 }}>{category.name}</span>
<span className="text-secondary text-sm" style={{ marginLeft: 'var(--space-sm)' }}>
({categoryBiomarkers.length} biomarkers)
</span>
</div>
<span style={{ fontSize: '18px', transition: 'transform 0.2s', transform: isExpanded ? 'rotate(180deg)' : 'rotate(0)' }}>
</span>
</button>
{/* Biomarkers List - Collapsible */}
{isExpanded && (
<div style={{ borderTop: '1px solid var(--border)', padding: 'var(--space-sm)' }}>
{categoryBiomarkers.length === 0 ? (
<p className="text-secondary text-sm" style={{ padding: 'var(--space-sm)' }}>
No biomarkers in this category
</p>
) : (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--space-xs)' }}>
{categoryBiomarkers.map(biomarker => (
<div
key={biomarker.id}
className="biomarker-chip"
title={`${biomarker.name} (${biomarker.unit})`}
>
<span className="biomarker-name">{biomarker.name}</span>
<span className="biomarker-unit">{biomarker.unit}</span>
</div>
))}
</div>
)}
</div>
)}
</div>
)
})}
</div>
</main> </main>
</div> </div>
) )