feat: US-010 - Clean up removed standalone tiles and verify layout
This commit is contained in:
+2
-2
@@ -153,7 +153,7 @@
|
|||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
],
|
],
|
||||||
"priority": 9,
|
"priority": 9,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": "Education is the bottom-most subsection in Patient Pathway. Include A-Levels: Mathematics (A*), Chemistry (B), Politics (C) — Highworth Grammar School, 2009–2011."
|
"notes": "Education is the bottom-most subsection in Patient Pathway. Include A-Levels: Mathematics (A*), Chemistry (B), Politics (C) — Highworth Grammar School, 2009–2011."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -173,7 +173,7 @@
|
|||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
],
|
],
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": "ProjectsTile may remain as a standalone tile or be absorbed — check if it still makes sense as standalone. If so, keep it. The key outcome is that the deleted tiles have no remaining references."
|
"notes": "ProjectsTile may remain as a standalone tile or be absorbed — check if it still makes sense as standalone. If so, keep it. The key outcome is that the deleted tiles have no remaining references."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -957,3 +957,29 @@
|
|||||||
- CoreSkillsTile.tsx still exists but is no longer imported from DashboardLayout — US-010 will clean it up
|
- CoreSkillsTile.tsx still exists but is no longer imported from DashboardLayout — US-010 will clean it up
|
||||||
- The RepeatMedicationsSubsection is a near-copy of CoreSkillsTile internals minus the Card wrapper — both files exist until cleanup in US-010
|
- The RepeatMedicationsSubsection is a near-copy of CoreSkillsTile internals minus the Card wrapper — both files exist until cleanup in US-010
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-02-14 - US-009
|
||||||
|
- Moved Education content from standalone EducationTile into Patient Pathway ParentSection as the bottom-most subsection
|
||||||
|
- Created `src/components/EducationSubsection.tsx` — standalone component with purple dot CardHeader "EDUCATION", renders 3 entries (Mary Seacole, MPharm, A-Levels) with hover, click-to-detail panel, inline details
|
||||||
|
- Deleted `src/components/tiles/EducationTile.tsx` — content fully moved to EducationSubsection
|
||||||
|
- Updated DashboardLayout: replaced EducationTile import/render with EducationSubsection inside Patient Pathway
|
||||||
|
- Files changed: src/components/DashboardLayout.tsx (modified), src/components/EducationSubsection.tsx (new), src/components/tiles/EducationTile.tsx (deleted)
|
||||||
|
- **Learnings for future iterations:**
|
||||||
|
- Git detected the move as a rename (91% similarity) — the EducationSubsection is nearly identical to EducationTile minus the Card wrapper
|
||||||
|
- Same pattern as WorkExperienceSubsection and RepeatMedicationsSubsection: standalone file, `marginTop: 24px` for spacing from preceding section, uses CardHeader for subsection header
|
||||||
|
- All Patient Pathway subsections now in place: CareerConstellation → LastConsultation → WorkExperience + RepeatMedications (two-column) → Education
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-02-14 - US-010
|
||||||
|
- Deleted 3 orphaned tile files: CareerActivityTile.tsx, CoreSkillsTile.tsx, LatestResultsTile.tsx (LastConsultationTile and EducationTile were already deleted in US-007/US-009)
|
||||||
|
- Removed unused `.activity-grid` CSS class from index.css (was only used by deleted CareerActivityTile)
|
||||||
|
- Updated 2 stale comments referencing deleted tiles (index.css, SkillsAllDetail.tsx)
|
||||||
|
- Verified no broken imports remain — grep found zero import references to any deleted tile
|
||||||
|
- Verified dashboard grid: PatientSummaryTile (full width) + ProjectsTile (half width) + Patient Pathway ParentSection (full width)
|
||||||
|
- Browser verification: no visual gaps, all content renders correctly
|
||||||
|
- Files changed: src/components/tiles/CareerActivityTile.tsx (deleted), src/components/tiles/CoreSkillsTile.tsx (deleted), src/components/tiles/LatestResultsTile.tsx (deleted), src/index.css, src/components/detail/SkillsAllDetail.tsx
|
||||||
|
- **Learnings for future iterations:**
|
||||||
|
- ProjectsTile remains as a standalone tile — it makes sense outside Patient Pathway since projects are cross-cutting, not career-timeline-specific
|
||||||
|
- The `.activity-grid` CSS was orphaned when CareerActivityTile was removed — always check for CSS classes that were only used by deleted components
|
||||||
|
- Only 2 tile files remain in src/components/tiles/: PatientSummaryTile.tsx and ProjectsTile.tsx
|
||||||
|
---
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export function SkillsAllDetail({ category }: SkillsAllDetailProps) {
|
|||||||
key={group.id}
|
key={group.id}
|
||||||
ref={(el) => { categoryRefs.current[group.id] = el }}
|
ref={(el) => { categoryRefs.current[group.id] = el }}
|
||||||
>
|
>
|
||||||
{/* Category header — matches CoreSkillsTile divider style */}
|
{/* Category header — divider style */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|||||||
@@ -1,302 +0,0 @@
|
|||||||
import React, { useState, useCallback } from 'react'
|
|
||||||
import { Card, CardHeader } from '../Card'
|
|
||||||
import { documents } from '@/data/documents'
|
|
||||||
import { consultations } from '@/data/consultations'
|
|
||||||
import { skills } from '@/data/skills'
|
|
||||||
import { useDetailPanel } from '@/contexts/DetailPanelContext'
|
|
||||||
import CareerConstellation from '../CareerConstellation'
|
|
||||||
|
|
||||||
type ActivityType = 'role' | 'project' | 'cert' | 'edu'
|
|
||||||
|
|
||||||
interface ActivityEntry {
|
|
||||||
id: string
|
|
||||||
type: ActivityType
|
|
||||||
title: string
|
|
||||||
meta: string
|
|
||||||
date: string
|
|
||||||
sortYear: number
|
|
||||||
/** ID of the corresponding consultation in consultations.ts (role entries only) */
|
|
||||||
consultationId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build timeline from multiple data sources
|
|
||||||
* Matches the concept HTML entries exactly
|
|
||||||
*/
|
|
||||||
function buildTimeline(): ActivityEntry[] {
|
|
||||||
const entries: ActivityEntry[] = []
|
|
||||||
|
|
||||||
// Roles from consultations (matching CV_v4.md)
|
|
||||||
entries.push({
|
|
||||||
id: 'interim-head-2025',
|
|
||||||
type: 'role',
|
|
||||||
title: 'Interim Head, Population Health & Data Analysis',
|
|
||||||
meta: 'NHS Norfolk & Waveney ICB',
|
|
||||||
date: 'May – Nov 2025',
|
|
||||||
sortYear: 2025,
|
|
||||||
consultationId: 'interim-head-2025',
|
|
||||||
})
|
|
||||||
|
|
||||||
entries.push({
|
|
||||||
id: 'deputy-head-2024',
|
|
||||||
type: 'role',
|
|
||||||
title: 'Deputy Head, Population Health & Data Analysis',
|
|
||||||
meta: 'NHS Norfolk & Waveney ICB',
|
|
||||||
date: 'Jul 2024 – Present',
|
|
||||||
sortYear: 2024,
|
|
||||||
consultationId: 'deputy-head-2024',
|
|
||||||
})
|
|
||||||
|
|
||||||
entries.push({
|
|
||||||
id: 'high-cost-drugs-2022',
|
|
||||||
type: 'role',
|
|
||||||
title: 'High-Cost Drugs & Interface Pharmacist',
|
|
||||||
meta: 'NHS Norfolk & Waveney ICB',
|
|
||||||
date: 'May 2022 – Jul 2024',
|
|
||||||
sortYear: 2022,
|
|
||||||
consultationId: 'high-cost-drugs-2022',
|
|
||||||
})
|
|
||||||
|
|
||||||
entries.push({
|
|
||||||
id: 'pharmacy-manager-2017',
|
|
||||||
type: 'role',
|
|
||||||
title: 'Pharmacy Manager',
|
|
||||||
meta: 'Tesco PLC',
|
|
||||||
date: 'Nov 2017 – May 2022',
|
|
||||||
sortYear: 2017,
|
|
||||||
consultationId: 'pharmacy-manager-2017',
|
|
||||||
})
|
|
||||||
|
|
||||||
// Certifications (matching CV_v4.md)
|
|
||||||
entries.push({
|
|
||||||
id: 'doc-gphc',
|
|
||||||
type: 'cert',
|
|
||||||
title: 'GPhC Registration',
|
|
||||||
meta: 'General Pharmaceutical Council',
|
|
||||||
date: 'August 2016',
|
|
||||||
sortYear: 2016,
|
|
||||||
})
|
|
||||||
|
|
||||||
entries.push({
|
|
||||||
id: 'cert-mary-seacole',
|
|
||||||
type: 'cert',
|
|
||||||
title: 'NHS Leadership Academy — Mary Seacole Programme',
|
|
||||||
meta: 'NHS leadership qualification',
|
|
||||||
date: '2018',
|
|
||||||
sortYear: 2018,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Education (matching CV_v4.md)
|
|
||||||
const mpharm = documents.find((d) => d.id === 'doc-mpharm')
|
|
||||||
if (mpharm) {
|
|
||||||
entries.push({
|
|
||||||
id: mpharm.id,
|
|
||||||
type: 'edu',
|
|
||||||
title: 'MPharm (Hons) — 2:1',
|
|
||||||
meta: 'University of East Anglia',
|
|
||||||
date: '2011 – 2015',
|
|
||||||
sortYear: 2011,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
entries.push({
|
|
||||||
id: 'edu-alevels',
|
|
||||||
type: 'edu',
|
|
||||||
title: 'A-Levels: Mathematics (A*), Chemistry (B), Politics (C)',
|
|
||||||
meta: 'Highworth Grammar School',
|
|
||||||
date: '2009 – 2011',
|
|
||||||
sortYear: 2009,
|
|
||||||
})
|
|
||||||
|
|
||||||
return entries.sort((a, b) => {
|
|
||||||
if (b.sortYear !== a.sortYear) return b.sortYear - a.sortYear
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const dotColorMap: Record<ActivityType, string> = {
|
|
||||||
role: '#0D6E6E',
|
|
||||||
project: '#D97706',
|
|
||||||
cert: '#059669',
|
|
||||||
edu: '#7C3AED',
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ActivityItemProps {
|
|
||||||
entry: ActivityEntry
|
|
||||||
onItemClick: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const ActivityItem: React.FC<ActivityItemProps> = ({ entry, onItemClick }) => {
|
|
||||||
const [isHovered, setIsHovered] = useState(false)
|
|
||||||
const dotColor = dotColorMap[entry.type]
|
|
||||||
const isClickable = entry.type === 'role' && entry.consultationId
|
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
|
||||||
(e: React.KeyboardEvent) => {
|
|
||||||
if (!isClickable) return
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault()
|
|
||||||
onItemClick()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[isClickable, onItemClick],
|
|
||||||
)
|
|
||||||
|
|
||||||
// Get consultation data for preview text
|
|
||||||
const consultation = isClickable
|
|
||||||
? consultations.find((c) => c.id === entry.consultationId)
|
|
||||||
: null
|
|
||||||
|
|
||||||
// Get preview text (first 1-2 lines from examination)
|
|
||||||
const previewText =
|
|
||||||
consultation && consultation.examination.length > 0
|
|
||||||
? consultation.examination[0]
|
|
||||||
: null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role={isClickable ? 'button' : undefined}
|
|
||||||
tabIndex={isClickable ? 0 : undefined}
|
|
||||||
onClick={isClickable ? onItemClick : undefined}
|
|
||||||
onKeyDown={isClickable ? handleKeyDown : undefined}
|
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
background: 'var(--bg-dashboard)',
|
|
||||||
borderRadius: 'var(--radius-sm)',
|
|
||||||
border: '1px solid var(--border-light)',
|
|
||||||
fontSize: '12px',
|
|
||||||
transition: 'all 0.15s ease-out',
|
|
||||||
cursor: isClickable ? 'pointer' : 'default',
|
|
||||||
transform: isHovered && isClickable ? 'translateY(-1px)' : 'none',
|
|
||||||
boxShadow: isHovered && isClickable
|
|
||||||
? '0 2px 8px rgba(26,43,42,0.08)'
|
|
||||||
: '0 1px 2px rgba(26,43,42,0.05)',
|
|
||||||
borderColor: isHovered && isClickable ? 'var(--accent-border)' : 'var(--border-light)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Item header row */}
|
|
||||||
<div style={{ display: 'flex', gap: '10px', padding: '10px 12px' }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: '8px',
|
|
||||||
height: '8px',
|
|
||||||
borderRadius: '50%',
|
|
||||||
background: dotColor,
|
|
||||||
flexShrink: 0,
|
|
||||||
marginTop: '2px',
|
|
||||||
}}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontWeight: 600,
|
|
||||||
color: 'var(--text-primary)',
|
|
||||||
lineHeight: 1.3,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{entry.title}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: '11px',
|
|
||||||
color: 'var(--text-secondary)',
|
|
||||||
marginTop: '2px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{entry.meta}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: '10px',
|
|
||||||
fontFamily: 'var(--font-mono)',
|
|
||||||
color: 'var(--text-tertiary)',
|
|
||||||
marginTop: '3px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{entry.date}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Hover preview text for roles */}
|
|
||||||
{isHovered && previewText && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: '11px',
|
|
||||||
color: 'var(--text-secondary)',
|
|
||||||
marginTop: '6px',
|
|
||||||
lineHeight: 1.4,
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
display: '-webkit-box',
|
|
||||||
WebkitLineClamp: 2,
|
|
||||||
WebkitBoxOrient: 'vertical',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{previewText}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CareerActivityTile: React.FC = () => {
|
|
||||||
const timeline = buildTimeline()
|
|
||||||
const { openPanel } = useDetailPanel()
|
|
||||||
|
|
||||||
const handleRoleClick = useCallback(
|
|
||||||
(roleId: string) => {
|
|
||||||
const consultation = consultations.find((c) => c.id === roleId)
|
|
||||||
if (consultation) {
|
|
||||||
openPanel({ type: 'career-role', consultation })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[openPanel],
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleSkillClick = useCallback(
|
|
||||||
(skillId: string) => {
|
|
||||||
const skill = skills.find((s) => s.id === skillId)
|
|
||||||
if (skill) {
|
|
||||||
openPanel({ type: 'skill', skill })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[openPanel],
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleItemClick = useCallback(
|
|
||||||
(entry: ActivityEntry) => {
|
|
||||||
if (entry.type === 'role' && entry.consultationId) {
|
|
||||||
handleRoleClick(entry.consultationId)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[handleRoleClick],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card full tileId="career-activity">
|
|
||||||
<CardHeader dotColor="teal" title="CAREER ACTIVITY" rightText="Full timeline" />
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '20px' }}>
|
|
||||||
<CareerConstellation
|
|
||||||
onRoleClick={handleRoleClick}
|
|
||||||
onSkillClick={handleSkillClick}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="activity-grid">
|
|
||||||
{timeline.map((entry) => (
|
|
||||||
<ActivityItem
|
|
||||||
key={entry.id}
|
|
||||||
entry={entry}
|
|
||||||
onItemClick={() => handleItemClick(entry)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import type { LucideIcon } from 'lucide-react'
|
|
||||||
import {
|
|
||||||
BarChart3, Code2, Database, PieChart, FileCode2,
|
|
||||||
Sheet, GitBranch, Workflow, Pill, Users, FileCheck,
|
|
||||||
TrendingUp, Route, ShieldAlert, Banknote, Handshake,
|
|
||||||
MessageSquare, UserPlus, RefreshCw, Calculator, Presentation,
|
|
||||||
ChevronRight,
|
|
||||||
} from 'lucide-react'
|
|
||||||
import { Card, CardHeader } from '../Card'
|
|
||||||
import { skills } from '@/data/skills'
|
|
||||||
import { useDetailPanel } from '@/contexts/DetailPanelContext'
|
|
||||||
import type { SkillMedication, SkillCategory } from '@/types/pmr'
|
|
||||||
|
|
||||||
const iconMap: Record<string, LucideIcon> = {
|
|
||||||
BarChart3,
|
|
||||||
Code2,
|
|
||||||
Database,
|
|
||||||
PieChart,
|
|
||||||
FileCode2,
|
|
||||||
Sheet,
|
|
||||||
GitBranch,
|
|
||||||
Workflow,
|
|
||||||
Pill,
|
|
||||||
Users,
|
|
||||||
FileCheck,
|
|
||||||
TrendingUp,
|
|
||||||
Route,
|
|
||||||
ShieldAlert,
|
|
||||||
Banknote,
|
|
||||||
Handshake,
|
|
||||||
MessageSquare,
|
|
||||||
UserPlus,
|
|
||||||
RefreshCw,
|
|
||||||
Calculator,
|
|
||||||
Presentation,
|
|
||||||
}
|
|
||||||
|
|
||||||
const SKILLS_PER_CATEGORY = 4
|
|
||||||
|
|
||||||
const categoryConfig: { id: SkillCategory; label: string }[] = [
|
|
||||||
{ id: 'Technical', label: 'Technical' },
|
|
||||||
{ id: 'Domain', label: 'Healthcare Domain' },
|
|
||||||
{ id: 'Leadership', label: 'Strategic & Leadership' },
|
|
||||||
]
|
|
||||||
|
|
||||||
interface SkillRowProps {
|
|
||||||
skill: SkillMedication
|
|
||||||
onClick: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
function SkillRow({ skill, onClick }: SkillRowProps) {
|
|
||||||
const IconComponent = iconMap[skill.icon]
|
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault()
|
|
||||||
onClick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={onClick}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
aria-label={`${skill.name}: ${skill.frequency}, ${skill.yearsOfExperience} years experience. Click for details.`}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '10px',
|
|
||||||
padding: '8px 10px',
|
|
||||||
minHeight: '44px',
|
|
||||||
background: 'var(--bg-dashboard)',
|
|
||||||
borderRadius: 'var(--radius-sm)',
|
|
||||||
border: '1px solid var(--border-light)',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'border-color 0.15s, box-shadow 0.15s',
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.currentTarget.style.borderColor = 'var(--accent-border)'
|
|
||||||
e.currentTarget.style.boxShadow = 'var(--shadow-md)'
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.borderColor = 'var(--border-light)'
|
|
||||||
e.currentTarget.style.boxShadow = 'none'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Icon */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: '26px',
|
|
||||||
height: '26px',
|
|
||||||
borderRadius: '6px',
|
|
||||||
background: 'var(--accent-light)',
|
|
||||||
color: 'var(--accent)',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{IconComponent && <IconComponent size={13} />}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Text */}
|
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: '12.5px',
|
|
||||||
fontWeight: 600,
|
|
||||||
color: 'var(--text-primary)',
|
|
||||||
lineHeight: 1.3,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{skill.name}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: '10.5px',
|
|
||||||
color: 'var(--text-tertiary)',
|
|
||||||
fontFamily: '"Geist Mono", monospace',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{skill.frequency} · {skill.yearsOfExperience} yrs
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Status badge */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: '10px',
|
|
||||||
fontWeight: 500,
|
|
||||||
padding: '2px 7px',
|
|
||||||
borderRadius: '20px',
|
|
||||||
background: 'var(--success-light)',
|
|
||||||
color: 'var(--success)',
|
|
||||||
border: '1px solid var(--success-border)',
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{skill.status}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Chevron */}
|
|
||||||
<ChevronRight
|
|
||||||
size={14}
|
|
||||||
style={{ color: 'var(--text-tertiary)', flexShrink: 0 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CategorySectionProps {
|
|
||||||
label: string
|
|
||||||
categoryId: SkillCategory
|
|
||||||
skills: SkillMedication[]
|
|
||||||
onSkillClick: (skill: SkillMedication) => void
|
|
||||||
onViewAll: (category: SkillCategory) => void
|
|
||||||
isFirst: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
function CategorySection({
|
|
||||||
label,
|
|
||||||
categoryId,
|
|
||||||
skills: categorySkills,
|
|
||||||
onSkillClick,
|
|
||||||
onViewAll,
|
|
||||||
isFirst,
|
|
||||||
}: CategorySectionProps) {
|
|
||||||
const visibleSkills = categorySkills.slice(0, SKILLS_PER_CATEGORY)
|
|
||||||
const remainingCount = categorySkills.length - SKILLS_PER_CATEGORY
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ marginTop: isFirst ? 0 : '16px' }}>
|
|
||||||
{/* Category header — sidebar section divider style */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '8px',
|
|
||||||
marginBottom: '10px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: '10px',
|
|
||||||
fontWeight: 600,
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.06em',
|
|
||||||
color: 'var(--text-tertiary)',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
height: '1px',
|
|
||||||
background: 'var(--border-light)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: '10px',
|
|
||||||
color: 'var(--text-tertiary)',
|
|
||||||
fontFamily: '"Geist Mono", monospace',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{categorySkills.length} items
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Skill rows */}
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
|
||||||
{visibleSkills.map((skill) => (
|
|
||||||
<SkillRow
|
|
||||||
key={skill.id}
|
|
||||||
skill={skill}
|
|
||||||
onClick={() => onSkillClick(skill)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* View all button */}
|
|
||||||
{remainingCount > 0 && (
|
|
||||||
<button
|
|
||||||
onClick={() => onViewAll(categoryId)}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '4px',
|
|
||||||
marginTop: '8px',
|
|
||||||
padding: '4px 0',
|
|
||||||
minHeight: '44px',
|
|
||||||
background: 'none',
|
|
||||||
border: 'none',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontSize: '11px',
|
|
||||||
fontWeight: 500,
|
|
||||||
color: 'var(--accent)',
|
|
||||||
fontFamily: 'inherit',
|
|
||||||
transition: 'color 0.15s',
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.currentTarget.style.color = 'var(--accent-hover)'
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.color = 'var(--accent)'
|
|
||||||
}}
|
|
||||||
aria-label={`View all ${categorySkills.length} ${label} skills`}
|
|
||||||
>
|
|
||||||
View all ({categorySkills.length})
|
|
||||||
<ChevronRight size={12} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CoreSkillsTile() {
|
|
||||||
const { openPanel } = useDetailPanel()
|
|
||||||
|
|
||||||
// Group skills by category, sorted by proficiency descending
|
|
||||||
const groupedSkills = categoryConfig.map(({ id, label }) => ({
|
|
||||||
id,
|
|
||||||
label,
|
|
||||||
skills: skills
|
|
||||||
.filter((s) => s.category === id)
|
|
||||||
.sort((a, b) => b.proficiency - a.proficiency),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const handleSkillClick = (skill: SkillMedication) => {
|
|
||||||
openPanel({ type: 'skill', skill })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleViewAll = (category: SkillCategory) => {
|
|
||||||
openPanel({ type: 'skills-all', category })
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card full tileId="core-skills">
|
|
||||||
<CardHeader
|
|
||||||
dotColor="amber"
|
|
||||||
title="REPEAT MEDICATIONS"
|
|
||||||
rightText="Active prescriptions"
|
|
||||||
/>
|
|
||||||
{groupedSkills.map((group, index) => (
|
|
||||||
<CategorySection
|
|
||||||
key={group.id}
|
|
||||||
label={group.label}
|
|
||||||
categoryId={group.id}
|
|
||||||
skills={group.skills}
|
|
||||||
onSkillClick={handleSkillClick}
|
|
||||||
onViewAll={handleViewAll}
|
|
||||||
isFirst={index === 0}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { Card, CardHeader } from '../Card'
|
|
||||||
import { kpis } from '@/data/kpis'
|
|
||||||
import type { KPI } from '@/types/pmr'
|
|
||||||
import { useDetailPanel } from '@/contexts/DetailPanelContext'
|
|
||||||
|
|
||||||
const colorMap: Record<KPI['colorVariant'], string> = {
|
|
||||||
green: '#059669',
|
|
||||||
amber: '#D97706',
|
|
||||||
teal: '#0D6E6E',
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MetricCardProps {
|
|
||||||
kpi: KPI
|
|
||||||
}
|
|
||||||
|
|
||||||
function MetricCard({ kpi }: MetricCardProps) {
|
|
||||||
const { openPanel } = useDetailPanel()
|
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
openPanel({ type: 'kpi', kpi })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault()
|
|
||||||
openPanel({ type: 'kpi', kpi })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttonStyles: React.CSSProperties = {
|
|
||||||
width: '100%',
|
|
||||||
textAlign: 'left',
|
|
||||||
padding: '16px',
|
|
||||||
background: 'var(--surface)',
|
|
||||||
border: '1px solid var(--border-light)',
|
|
||||||
borderRadius: 'var(--radius-sm)',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'border-color 150ms ease-out, box-shadow 150ms ease-out',
|
|
||||||
}
|
|
||||||
|
|
||||||
const valueStyles: React.CSSProperties = {
|
|
||||||
fontSize: '28px',
|
|
||||||
fontWeight: 700,
|
|
||||||
letterSpacing: '-0.02em',
|
|
||||||
lineHeight: 1.2,
|
|
||||||
color: colorMap[kpi.colorVariant],
|
|
||||||
}
|
|
||||||
|
|
||||||
const labelStyles: React.CSSProperties = {
|
|
||||||
fontSize: '12px',
|
|
||||||
fontWeight: 500,
|
|
||||||
color: 'var(--text-primary)',
|
|
||||||
marginTop: '4px',
|
|
||||||
}
|
|
||||||
|
|
||||||
const subStyles: React.CSSProperties = {
|
|
||||||
fontSize: '10px',
|
|
||||||
color: 'var(--text-tertiary)',
|
|
||||||
fontFamily: 'var(--font-geist-mono)',
|
|
||||||
marginTop: '2px',
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={handleClick}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
style={buttonStyles}
|
|
||||||
aria-label={`${kpi.label}: ${kpi.value}. Click to view details.`}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.currentTarget.style.borderColor = 'var(--accent-border)'
|
|
||||||
e.currentTarget.style.boxShadow = 'var(--shadow-md)'
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.borderColor = 'var(--border-light)'
|
|
||||||
e.currentTarget.style.boxShadow = 'none'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={valueStyles}>{kpi.value}</div>
|
|
||||||
<div style={labelStyles}>{kpi.label}</div>
|
|
||||||
<div style={subStyles}>{kpi.sub}</div>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LatestResultsTile() {
|
|
||||||
const gridStyles: React.CSSProperties = {
|
|
||||||
display: 'grid',
|
|
||||||
gridTemplateColumns: '1fr 1fr',
|
|
||||||
gap: '12px',
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card tileId="latest-results">
|
|
||||||
<CardHeader dotColor="teal" title="LATEST RESULTS" rightText="Updated May 2025" />
|
|
||||||
<div style={gridStyles}>
|
|
||||||
{kpis.map((kpi) => (
|
|
||||||
<MetricCard key={kpi.id} kpi={kpi} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -286,20 +286,6 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Activity grid responsive — mobile-first (used in CareerActivityTile) */
|
|
||||||
.activity-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tablet and up: 2 columns */
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.activity-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pathway two-column layout — mobile-first (used in Patient Pathway) */
|
/* Pathway two-column layout — mobile-first (used in Patient Pathway) */
|
||||||
.pathway-columns {
|
.pathway-columns {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
Reference in New Issue
Block a user