Added Mary Seacole back in

This commit is contained in:
2026-02-19 20:27:06 +00:00
parent 72d159484f
commit edc1327987
7 changed files with 263 additions and 146 deletions
+14 -4
View File
@@ -4,8 +4,8 @@ import {
BarChart3, Code2, Database, LayoutDashboard, Bot, FileCode2, BarChart3, Code2, Database, LayoutDashboard, Bot, FileCode2,
Pill, Users, FileCheck, Stethoscope, Pill, Users, FileCheck, Stethoscope,
TrendingUp, Route, BookOpen, Store, TrendingUp, Route, BookOpen, Store,
Presentation, Calculator, Banknote, Handshake, RefreshCw, Crown, Presentation, Calculator, Banknote, Handshake, RefreshCw,
ChevronRight, GitBranch, Workflow, UserPlus, ChevronRight,
} from 'lucide-react' } from 'lucide-react'
import { CardHeader } from './Card' import { CardHeader } from './Card'
import { skills } from '@/data/skills' import { skills } from '@/data/skills'
@@ -17,7 +17,8 @@ const iconMap: Record<string, LucideIcon> = {
BarChart3, Code2, Database, LayoutDashboard, Bot, FileCode2, BarChart3, Code2, Database, LayoutDashboard, Bot, FileCode2,
Pill, Users, FileCheck, Stethoscope, Pill, Users, FileCheck, Stethoscope,
TrendingUp, Route, BookOpen, Store, TrendingUp, Route, BookOpen, Store,
Presentation, Calculator, Banknote, Handshake, RefreshCw, Crown, Presentation, Calculator, Banknote, Handshake, RefreshCw,
GitBranch, Workflow, UserPlus,
} }
@@ -210,6 +211,15 @@ interface RepeatMedicationsSubsectionProps {
focusRelatedIds?: Set<string> | null focusRelatedIds?: Set<string> | null
} }
const frequencyRank = (freq: string): number => {
if (freq.includes('daily')) return freq.startsWith('4') ? 0 : freq.startsWith('3') ? 1 : freq.startsWith('1') ? 3 : 2
if (freq === 'Daily') return 4
if (freq.includes('weekly')) return freq.startsWith('2') ? 5 : freq.startsWith('1') ? 6 : 7
if (freq === 'Weekly') return 7
if (freq === 'Bi-monthly') return 8
return 9 // As needed
}
export function RepeatMedicationsSubsection({ onNodeHighlight, focusRelatedIds }: RepeatMedicationsSubsectionProps) { export function RepeatMedicationsSubsection({ onNodeHighlight, focusRelatedIds }: RepeatMedicationsSubsectionProps) {
const { openPanel } = useDetailPanel() const { openPanel } = useDetailPanel()
const skillsCopy = getSkillsUICopy() const skillsCopy = getSkillsUICopy()
@@ -219,7 +229,7 @@ export function RepeatMedicationsSubsection({ onNodeHighlight, focusRelatedIds }
label, label,
skills: skills skills: skills
.filter((s) => s.category === id) .filter((s) => s.category === id)
.sort((a, b) => b.yearsOfExperience - a.yearsOfExperience), .sort((a, b) => frequencyRank(a.frequency) - frequencyRank(b.frequency)),
})) }))
const handleSkillClick = (skill: SkillMedication) => { const handleSkillClick = (skill: SkillMedication) => {
@@ -4,11 +4,18 @@ import { motion, AnimatePresence } from 'framer-motion'
import { ExpandableCardShell } from './ExpandableCardShell' import { ExpandableCardShell } from './ExpandableCardShell'
import { useDetailPanel } from '@/contexts/DetailPanelContext' import { useDetailPanel } from '@/contexts/DetailPanelContext'
import { timelineEntities, timelineConsultations } from '@/data/timeline' import { timelineEntities, timelineConsultations } from '@/data/timeline'
import { documents } from '@/data/documents'
import { getExperienceEducationUICopy } from '@/lib/profile-content' import { getExperienceEducationUICopy } from '@/lib/profile-content'
import type { TimelineEntity } from '@/types/pmr' import type { TimelineEntity } from '@/types/pmr'
const timelineToDocumentId: Record<string, string> = {
'nhs-mary-seacole-2018': 'doc-mary-seacole',
'uea-mpharm-2011': 'doc-mpharm',
'highworth-alevels-2009': 'doc-alevels',
}
import { hexToRgba, motionSafeTransition } from '@/lib/utils' import { hexToRgba, motionSafeTransition } from '@/lib/utils'
const VISIBLE_COUNT = 4 const VISIBLE_COUNT = 5
interface TimelineInterventionItemProps { interface TimelineInterventionItemProps {
entity: TimelineEntity entity: TimelineEntity
@@ -304,6 +311,12 @@ export function TimelineInterventionsSubsection({ onNodeHighlight, highlightedRo
}, []) }, [])
const handleViewFull = useCallback((entity: TimelineEntity) => { const handleViewFull = useCallback((entity: TimelineEntity) => {
if (entity.kind === 'education') {
const docId = timelineToDocumentId[entity.id]
const doc = docId ? documents.find((d) => d.id === docId) : undefined
if (doc) openPanel({ type: 'education', document: doc })
return
}
const consultation = consultationsById.get(entity.id) const consultation = consultationsById.get(entity.id)
if (!consultation) return if (!consultation) return
openPanel({ type: 'career-role', consultation }) openPanel({ type: 'career-role', consultation })
+14 -4
View File
@@ -4,8 +4,8 @@ import {
BarChart3, Code2, Database, LayoutDashboard, Bot, FileCode2, BarChart3, Code2, Database, LayoutDashboard, Bot, FileCode2,
Pill, Users, FileCheck, Stethoscope, Pill, Users, FileCheck, Stethoscope,
TrendingUp, Route, BookOpen, Store, TrendingUp, Route, BookOpen, Store,
Presentation, Calculator, Banknote, Handshake, RefreshCw, Crown, Presentation, Calculator, Banknote, Handshake, RefreshCw,
ChevronRight, GitBranch, Workflow, UserPlus, ChevronRight,
} from 'lucide-react' } from 'lucide-react'
import { skills } from '@/data/skills' import { skills } from '@/data/skills'
import { useDetailPanel } from '@/contexts/DetailPanelContext' import { useDetailPanel } from '@/contexts/DetailPanelContext'
@@ -16,7 +16,8 @@ const iconMap: Record<string, LucideIcon> = {
BarChart3, Code2, Database, LayoutDashboard, Bot, FileCode2, BarChart3, Code2, Database, LayoutDashboard, Bot, FileCode2,
Pill, Users, FileCheck, Stethoscope, Pill, Users, FileCheck, Stethoscope,
TrendingUp, Route, BookOpen, Store, TrendingUp, Route, BookOpen, Store,
Presentation, Calculator, Banknote, Handshake, RefreshCw, Crown, Presentation, Calculator, Banknote, Handshake, RefreshCw,
GitBranch, Workflow, UserPlus,
} }
interface SkillsAllDetailProps { interface SkillsAllDetailProps {
@@ -35,12 +36,21 @@ export function SkillsAllDetail({ category }: SkillsAllDetailProps) {
} }
}, [category]) }, [category])
const frequencyRank = (freq: string): number => {
if (freq.includes('daily')) return freq.startsWith('4') ? 0 : freq.startsWith('3') ? 1 : freq.startsWith('1') ? 3 : 2
if (freq === 'Daily') return 4
if (freq.includes('weekly')) return freq.startsWith('2') ? 5 : freq.startsWith('1') ? 6 : 7
if (freq === 'Weekly') return 7
if (freq === 'Bi-monthly') return 8
return 9 // As needed
}
const groupedSkills = skillsCopy.categories.map(({ id, label }) => ({ const groupedSkills = skillsCopy.categories.map(({ id, label }) => ({
id, id,
label, label,
skills: skills skills: skills
.filter((s) => s.category === id) .filter((s) => s.category === id)
.sort((a, b) => b.yearsOfExperience - a.yearsOfExperience), .sort((a, b) => frequencyRank(a.frequency) - frequencyRank(b.frequency)),
})) }))
const handleSkillClick = (skill: SkillMedication) => { const handleSkillClick = (skill: SkillMedication) => {
+5 -5
View File
@@ -90,13 +90,13 @@ export const profileContent: DeepReadonly<ProfileContent> = {
title: '£2.6M Savings via Automated Algorithm', title: '£2.6M Savings via Automated Algorithm',
subtitle: '14,000 patients identified in 3 days', subtitle: '14,000 patients identified in 3 days',
keywords: '2.6m savings automated algorithm python switching 14000 patients cost-effective alternatives prescribing analytics', keywords: '2.6m savings automated algorithm python switching 14000 patients cost-effective alternatives prescribing analytics',
kpiId: 'years', kpiId: 'algorithm',
}, },
{ {
title: '1.2M Population Served', title: '20M Prescriptions Decoded Annually',
subtitle: 'Norfolk & Waveney Integrated Care System', subtitle: '90% coverage — free text to structured data',
keywords: '1.2m population served norfolk waveney ics integrated care system primary care secondary care commissioning', keywords: '20m prescriptions decoded parsed free text structured data regex ai pipeline daily quantities adherence population scale',
kpiId: 'population', kpiId: 'prescriptions',
}, },
], ],
}, },
+137 -121
View File
@@ -1,7 +1,7 @@
import type { SkillMedication } from '@/types/pmr' import type { SkillMedication } from '@/types/pmr'
export const skills: SkillMedication[] = [ export const skills: SkillMedication[] = [
// Technical (6 skills) // Technical (8 skills) — sorted by frequency
{ {
id: 'data-analysis', id: 'data-analysis',
name: 'Data Analysis', name: 'Data Analysis',
@@ -19,6 +19,21 @@ export const skills: SkillMedication[] = [
{ year: 2024, description: 'Current: ICS-wide analytics strategy development' }, { year: 2024, description: 'Current: ICS-wide analytics strategy development' },
], ],
}, },
{
id: 'ai-prompt-engineering',
name: 'AI Integration & Automation',
shortName: 'AI Integration',
frequency: '4x daily',
startYear: 2024,
yearsOfExperience: 2,
category: 'Technical',
status: 'Active',
icon: 'Bot',
prescribingHistory: [
{ year: 2024, description: 'Started: LLM-assisted code generation and data analysis workflows' },
{ year: 2025, description: 'Current: Agentic coding, prompt design for clinical data extraction' },
],
},
{ {
id: 'python', id: 'python',
name: 'Python', name: 'Python',
@@ -67,38 +82,6 @@ export const skills: SkillMedication[] = [
{ year: 2025, description: 'Current: Self-serve analytics dashboards, incentive scheme tracking' }, { year: 2025, description: 'Current: Self-serve analytics dashboards, incentive scheme tracking' },
], ],
}, },
{
id: 'ai-prompt-engineering',
name: 'AI Integration & Automation',
shortName: 'AI Integration',
frequency: '4x daily',
startYear: 2024,
yearsOfExperience: 2,
category: 'Technical',
status: 'Active',
icon: 'Bot',
prescribingHistory: [
{ year: 2024, description: 'Started: LLM-assisted code generation and data analysis workflows' },
{ year: 2025, description: 'Current: Agentic coding, prompt design for clinical data extraction' },
],
},
{
id: 'javascript-typescript',
name: 'JavaScript / TypeScript',
shortName: 'JS / TS',
frequency: 'As needed',
startYear: 2020,
yearsOfExperience: 4,
category: 'Technical',
status: 'Active',
icon: 'FileCode2',
prescribingHistory: [
{ year: 2020, description: 'Started: Web development for personal projects' },
{ year: 2022, description: 'Increased: React dashboard components' },
{ year: 2024, description: 'Current: CV/portfolio development, interactive tools' },
],
},
{ {
id: 'algorithm-design', id: 'algorithm-design',
name: 'Algorithm Design', name: 'Algorithm Design',
@@ -132,8 +115,24 @@ export const skills: SkillMedication[] = [
{ year: 2025, description: 'Current: ICS-wide data infrastructure' }, { year: 2025, description: 'Current: ICS-wide data infrastructure' },
], ],
}, },
{
id: 'javascript-typescript',
name: 'JavaScript / TypeScript',
shortName: 'JS / TS',
frequency: 'As needed',
startYear: 2020,
yearsOfExperience: 4,
category: 'Technical',
status: 'Active',
icon: 'FileCode2',
prescribingHistory: [
{ year: 2020, description: 'Started: Web development for personal projects' },
{ year: 2022, description: 'Increased: React dashboard components' },
{ year: 2024, description: 'Current: CV/portfolio development, interactive tools' },
],
},
// Clinical (8 skills) // Clinical (8 skills) — sorted by frequency
{ {
id: 'medicines-optimisation', id: 'medicines-optimisation',
name: 'Medicines Optimisation', name: 'Medicines Optimisation',
@@ -151,52 +150,6 @@ export const skills: SkillMedication[] = [
{ year: 2025, description: 'Current: £14.6M efficiency programme delivery' }, { year: 2025, description: 'Current: £14.6M efficiency programme delivery' },
], ],
}, },
{
id: 'population-health',
name: 'Population Health',
shortName: 'Pop. Health',
frequency: 'Daily',
startYear: 2024,
yearsOfExperience: 2,
category: 'Clinical',
status: 'Active',
icon: 'Users',
prescribingHistory: [
{ year: 2024, description: 'Started: 1.2M population coverage, ICS-wide analytics' },
{ year: 2025, description: 'Current: Health inequality analysis, population-level interventions' },
],
},
{
id: 'nice-ta',
name: 'NICE TA Implementation',
shortName: 'NICE TA',
frequency: '12x weekly',
startYear: 2022,
yearsOfExperience: 4,
category: 'Clinical',
status: 'Active',
icon: 'FileCheck',
prescribingHistory: [
{ year: 2022, description: 'Started: High-cost drug pathway development' },
{ year: 2023, description: 'Increased: Multi-specialty pathway authoring' },
{ year: 2024, description: 'Current: Tirzepatide (TA1026) commissioning' },
],
},
{
id: 'health-system-prescribing',
name: 'Health System Prescribing Mgmt',
shortName: 'Prescribing Mgmt',
frequency: 'Daily',
startYear: 2024,
yearsOfExperience: 2,
category: 'Clinical',
status: 'Active',
icon: 'Stethoscope',
prescribingHistory: [
{ year: 2024, description: 'Started: dm+d medicines data table, OME calculations, prescribing infrastructure' },
{ year: 2025, description: 'Current: Patient-level SQL analytics, self-serve prescribing model' },
],
},
{ {
id: 'health-economics', id: 'health-economics',
name: 'Health Economics', name: 'Health Economics',
@@ -213,6 +166,36 @@ export const skills: SkillMedication[] = [
{ year: 2025, description: 'Current: Efficiency programme prioritisation' }, { year: 2025, description: 'Current: Efficiency programme prioritisation' },
], ],
}, },
{
id: 'population-health',
name: 'Population Health',
shortName: 'Pop. Health',
frequency: 'Daily',
startYear: 2024,
yearsOfExperience: 2,
category: 'Clinical',
status: 'Active',
icon: 'Users',
prescribingHistory: [
{ year: 2024, description: 'Started: 1.2M population coverage, ICS-wide analytics' },
{ year: 2025, description: 'Current: Health inequality analysis, population-level interventions' },
],
},
{
id: 'health-system-prescribing',
name: 'Health System Prescribing Mgmt',
shortName: 'Prescribing Mgmt',
frequency: 'Daily',
startYear: 2024,
yearsOfExperience: 2,
category: 'Clinical',
status: 'Active',
icon: 'Stethoscope',
prescribingHistory: [
{ year: 2024, description: 'Started: dm+d medicines data table, OME calculations, prescribing infrastructure' },
{ year: 2025, description: 'Current: Patient-level SQL analytics, self-serve prescribing model' },
],
},
{ {
id: 'clinical-pathways', id: 'clinical-pathways',
name: 'Clinical Pathways', name: 'Clinical Pathways',
@@ -229,6 +212,22 @@ export const skills: SkillMedication[] = [
{ year: 2024, description: 'Current: System-wide pathway governance' }, { year: 2024, description: 'Current: System-wide pathway governance' },
], ],
}, },
{
id: 'nice-ta',
name: 'NICE TA Implementation',
shortName: 'NICE TA',
frequency: '12x weekly',
startYear: 2022,
yearsOfExperience: 4,
category: 'Clinical',
status: 'Active',
icon: 'FileCheck',
prescribingHistory: [
{ year: 2022, description: 'Started: High-cost drug pathway development' },
{ year: 2023, description: 'Increased: Multi-specialty pathway authoring' },
{ year: 2024, description: 'Current: Tirzepatide (TA1026) commissioning' },
],
},
{ {
id: 'formulary-commissioning', id: 'formulary-commissioning',
name: 'Formulary & Commissioning', name: 'Formulary & Commissioning',
@@ -251,7 +250,7 @@ export const skills: SkillMedication[] = [
shortName: 'Community Pharm.', shortName: 'Community Pharm.',
frequency: 'As needed', frequency: 'As needed',
startYear: 2015, startYear: 2015,
yearsOfExperience: 11, yearsOfExperience: 7,
category: 'Clinical', category: 'Clinical',
status: 'Active', status: 'Active',
icon: 'Store', icon: 'Store',
@@ -262,36 +261,37 @@ export const skills: SkillMedication[] = [
], ],
}, },
// Strategic (6 skills) // Strategic (6 skills) — sorted by frequency
{ {
id: 'executive-comms', id: 'stakeholder-engagement',
name: 'Executive Communication', name: 'Stakeholder Engagement',
shortName: 'Exec. Comms', shortName: 'Stakeholders',
frequency: 'Bi-monthly', frequency: 'Daily',
startYear: 2024,
yearsOfExperience: 2,
category: 'Strategic',
status: 'Active',
icon: 'Presentation',
prescribingHistory: [
{ year: 2024, description: 'Started: CMO presentations, executive stakeholder engagement' },
{ year: 2025, description: 'Current: System-level programme board reporting' },
],
},
{
id: 'financial-modelling',
name: 'Financial Scenario Modelling',
shortName: 'Financial Modelling',
frequency: 'Weekly',
startYear: 2022, startYear: 2022,
yearsOfExperience: 4, yearsOfExperience: 4,
category: 'Strategic', category: 'Strategic',
status: 'Active', status: 'Active',
icon: 'Calculator', icon: 'Handshake',
prescribingHistory: [ prescribingHistory: [
{ year: 2022, description: 'Started: High-cost drug financial impact modelling' }, { year: 2022, description: 'Started: Clinical lead engagement across care sectors' },
{ year: 2024, description: 'Increased: DOAC switching scenario model, rebate mechanics' }, { year: 2024, description: 'Increased: Executive communication, CMO presentations' },
{ year: 2025, description: 'Current: Efficiency programme prioritisation, £215M budget forecasting' }, { year: 2025, description: 'Current: System-level programme board reporting' },
],
},
{
id: 'healthcare-leadership',
name: 'Healthcare Leadership',
shortName: 'Leadership',
frequency: 'Daily',
startYear: 2018,
yearsOfExperience: 8,
category: 'Strategic',
status: 'Active',
icon: 'Users',
prescribingHistory: [
{ year: 2018, description: 'Started: NHS Mary Seacole Leadership Programme, system-level thinking' },
{ year: 2022, description: 'Increased: ICS-level strategic leadership, cross-sector engagement' },
{ year: 2025, description: 'Current: Interim Head of Population Health, CMO reporting line' },
], ],
}, },
{ {
@@ -311,18 +311,33 @@ export const skills: SkillMedication[] = [
], ],
}, },
{ {
id: 'stakeholder-engagement', id: 'financial-modelling',
name: 'Stakeholder Engagement', name: 'Financial Scenario Modelling',
shortName: 'Stakeholders', shortName: 'Financial Modelling',
frequency: 'Daily', frequency: 'Weekly',
startYear: 2022, startYear: 2022,
yearsOfExperience: 4, yearsOfExperience: 4,
category: 'Strategic', category: 'Strategic',
status: 'Active', status: 'Active',
icon: 'Handshake', icon: 'Calculator',
prescribingHistory: [ prescribingHistory: [
{ year: 2022, description: 'Started: Clinical lead engagement across care sectors' }, { year: 2022, description: 'Started: High-cost drug financial impact modelling' },
{ year: 2024, description: 'Increased: Executive communication, CMO presentations' }, { year: 2024, description: 'Increased: DOAC switching scenario model, rebate mechanics' },
{ year: 2025, description: 'Current: Efficiency programme prioritisation, £215M budget forecasting' },
],
},
{
id: 'executive-comms',
name: 'Executive Communication',
shortName: 'Exec. Comms',
frequency: 'Bi-monthly',
startYear: 2024,
yearsOfExperience: 2,
category: 'Strategic',
status: 'Active',
icon: 'Presentation',
prescribingHistory: [
{ year: 2024, description: 'Started: CMO presentations, executive stakeholder engagement' },
{ year: 2025, description: 'Current: System-level programme board reporting' }, { year: 2025, description: 'Current: System-level programme board reporting' },
], ],
}, },
@@ -338,19 +353,20 @@ export const skills: SkillMedication[] = [
icon: 'RefreshCw', icon: 'RefreshCw',
}, },
{ {
id: 'healthcare-leadership', id: 'team-development',
name: 'Healthcare Leadership', name: 'Team Development',
shortName: 'Leadership', shortName: 'Team Dev',
frequency: 'Daily', frequency: 'Daily',
startYear: 2018, startYear: 2011,
yearsOfExperience: 8, yearsOfExperience: 15,
category: 'Strategic', category: 'Strategic',
status: 'Active', status: 'Active',
icon: 'Crown', icon: 'UserPlus',
prescribingHistory: [ prescribingHistory: [
{ year: 2018, description: 'Started: NHS Mary Seacole Leadership Programme, system-level thinking' }, { year: 2017, description: 'Started: National induction training, NVQ supervision, NMS training video' },
{ year: 2022, description: 'Increased: ICS-level strategic leadership, cross-sector engagement' }, { year: 2018, description: 'Developed: Mary Seacole Programme, formal leadership development' },
{ year: 2025, description: 'Current: Interim Head of Population Health, CMO reporting line' }, { year: 2024, description: 'Increased: Data literacy programme, self-serve analytics capability building' },
{ year: 2025, description: 'Current: Team-wide analytical upskilling, self-serve model adoption' },
], ],
}, },
] ]
+45 -2
View File
@@ -66,6 +66,7 @@ const timelineEntitySeeds: TimelineEntity[] = [
'stakeholder-engagement', 'stakeholder-engagement',
'change-management', 'change-management',
'healthcare-leadership', 'healthcare-leadership',
'team-development',
], ],
skillStrengths: { skillStrengths: {
'data-analysis': 1.0, 'data-analysis': 1.0,
@@ -88,6 +89,7 @@ const timelineEntitySeeds: TimelineEntity[] = [
'stakeholder-engagement': 0.9, 'stakeholder-engagement': 0.9,
'change-management': 0.7, 'change-management': 0.7,
'healthcare-leadership': 0.9, 'healthcare-leadership': 0.9,
'team-development': 0.8,
}, },
}, },
{ {
@@ -149,6 +151,7 @@ const timelineEntitySeeds: TimelineEntity[] = [
'stakeholder-engagement', 'stakeholder-engagement',
'change-management', 'change-management',
'healthcare-leadership', 'healthcare-leadership',
'team-development',
], ],
skillStrengths: { skillStrengths: {
'data-analysis': 0.95, 'data-analysis': 0.95,
@@ -171,6 +174,7 @@ const timelineEntitySeeds: TimelineEntity[] = [
'stakeholder-engagement': 0.9, 'stakeholder-engagement': 0.9,
'change-management': 0.7, 'change-management': 0.7,
'healthcare-leadership': 0.85, 'healthcare-leadership': 0.85,
'team-development': 0.75,
}, },
}, },
{ {
@@ -281,6 +285,7 @@ const timelineEntitySeeds: TimelineEntity[] = [
'change-management', 'change-management',
'stakeholder-engagement', 'stakeholder-engagement',
'healthcare-leadership', 'healthcare-leadership',
'team-development',
], ],
skillStrengths: { skillStrengths: {
'data-analysis': 0.7, 'data-analysis': 0.7,
@@ -290,6 +295,7 @@ const timelineEntitySeeds: TimelineEntity[] = [
'change-management': 0.6, 'change-management': 0.6,
'stakeholder-engagement': 0.6, 'stakeholder-engagement': 0.6,
'healthcare-leadership': 0.7, 'healthcare-leadership': 0.7,
'team-development': 0.8,
}, },
}, },
{ {
@@ -375,13 +381,50 @@ const timelineEntitySeeds: TimelineEntity[] = [
'change-management': 0.4, 'change-management': 0.4,
}, },
}, },
{
id: 'nhs-mary-seacole-2018',
kind: 'education',
title: 'Mary Seacole Programme',
graphLabel: 'Mary Seacole',
organization: 'NHS Leadership Academy',
orgColor: '#6B21A8',
dateRange: {
start: '2018-01-01',
end: '2018-12-31',
display: '2018',
startYear: 2018,
endYear: 2018,
},
description: 'Formal NHS leadership qualification providing theoretical grounding in healthcare leadership approaches, change management, and system-level thinking. Achieved programme score of 78%.',
details: [
'Programme score: 78%',
'Healthcare leadership and change management',
'System-level thinking and leading without authority',
],
outcomes: [
'Theoretical grounding in healthcare leadership approaches',
'Enhanced change management capabilities',
'System-level strategic thinking skills',
],
codedEntries: [
{ code: 'LDR001', description: 'NHS Leadership qualification — 78%' },
{ code: 'CHG001', description: 'Change management and system-level thinking' },
],
skills: ['healthcare-leadership', 'change-management', 'stakeholder-engagement', 'team-development'],
skillStrengths: {
'healthcare-leadership': 0.7,
'change-management': 0.6,
'stakeholder-engagement': 0.5,
'team-development': 0.5,
},
},
{ {
id: 'uea-mpharm-2011', id: 'uea-mpharm-2011',
kind: 'education', kind: 'education',
title: 'MPharm (Hons) 2:1', title: 'MPharm (Hons) 2:1',
graphLabel: 'MPharm', graphLabel: 'MPharm',
organization: 'University of East Anglia', organization: 'University of East Anglia',
orgColor: '#7B2D8E', orgColor: '#6B21A8',
dateRange: { dateRange: {
start: '2011-09-01', start: '2011-09-01',
end: '2015-06-30', end: '2015-06-30',
@@ -416,7 +459,7 @@ const timelineEntitySeeds: TimelineEntity[] = [
title: 'A-Levels', title: 'A-Levels',
graphLabel: 'A-Levels', graphLabel: 'A-Levels',
organization: 'Highworth Grammar School', organization: 'Highworth Grammar School',
orgColor: '#9C27B0', orgColor: '#6B21A8',
dateRange: { dateRange: {
start: '2009-09-01', start: '2009-09-01',
end: '2011-06-30', end: '2011-06-30',
+34 -9
View File
@@ -26,6 +26,10 @@ function hashString(input: string): number {
return Math.abs(hash) return Math.abs(hash)
} }
function isRoleNode(type: string): boolean {
return type === 'role'
}
function isEntityNode(type: string): boolean { function isEntityNode(type: string): boolean {
return type === 'role' || type === 'education' return type === 'role' || type === 'education'
} }
@@ -48,7 +52,8 @@ function getHeight(width: number, containerHeight?: number | null): number {
return 400 return 400
} }
const roleNodes = constellationNodes.filter(n => (n.type === 'role' || n.type === 'education') && !HIDDEN_ENTITY_IDS.has(n.id)) const roleNodes = constellationNodes.filter(n => n.type === 'role' && !HIDDEN_ENTITY_IDS.has(n.id))
const educationNodes = constellationNodes.filter(n => n.type === 'education' && !HIDDEN_ENTITY_IDS.has(n.id))
export function useForceSimulation( export function useForceSimulation(
svgRef: React.RefObject<SVGSVGElement | null>, svgRef: React.RefObject<SVGSVGElement | null>,
@@ -84,7 +89,8 @@ export function useForceSimulation(
svg.selectAll('*').remove() svg.selectAll('*').remove()
const years = roleNodes.map(n => fractionalYear(n)) const allEntityNodes = [...roleNodes, ...educationNodes]
const years = allEntityNodes.map(n => fractionalYear(n))
const minYear = Math.min(...years) const minYear = Math.min(...years)
const maxYear = Math.max(...years) const maxYear = Math.max(...years)
@@ -301,6 +307,16 @@ export function useForceSimulation(
const skillZoneLeft = sidePadding + srActive const skillZoneLeft = sidePadding + srActive
const skillZoneWidth = skillZoneRight - skillZoneLeft const skillZoneWidth = skillZoneRight - skillZoneLeft
// Education nodes sit on the left side, timeline-anchored on Y
const educationInitialMap = new Map<string, { x: number; y: number }>()
const eduX = skillZoneLeft + rw / 2 + (isMobile ? 8 : Math.round(12 * sf))
educationNodes.forEach((edu) => {
educationInitialMap.set(edu.id, {
x: eduX,
y: yScale(fractionalYear(edu)),
})
})
// Pre-compute skill homeY and group by role-set to offset overlaps // Pre-compute skill homeY and group by role-set to offset overlaps
const skillRoleKey = new Map<string, string>() // skillId -> sorted role key const skillRoleKey = new Map<string, string>() // skillId -> sorted role key
const skillBaseY = new Map<string, number>() // skillId -> base homeY const skillBaseY = new Map<string, number>() // skillId -> base homeY
@@ -312,7 +328,7 @@ export function useForceSimulation(
skillRoleKey.set(n.id, key) skillRoleKey.set(n.id, key)
const positions = roleIds const positions = roleIds
.map(roleId => roleInitialMap.get(roleId)) .map(roleId => roleInitialMap.get(roleId) ?? educationInitialMap.get(roleId))
.filter(Boolean) as Array<{ x: number; y: number }> .filter(Boolean) as Array<{ x: number; y: number }>
const baseY = positions.length > 0 const baseY = positions.length > 0
? positions.reduce((sum, p) => sum + p.y, 0) / positions.length ? positions.reduce((sum, p) => sum + p.y, 0) / positions.length
@@ -336,7 +352,11 @@ export function useForceSimulation(
}) })
const nodes: SimNode[] = visibleNodeData.map(n => { const nodes: SimNode[] = visibleNodeData.map(n => {
if (isEntityNode(n.type)) { if (n.type === 'education') {
const pos = educationInitialMap.get(n.id)!
return { ...n, x: pos.x, y: pos.y, vx: 0, vy: 0, homeX: pos.x, homeY: pos.y }
}
if (isRoleNode(n.type)) {
const pos = roleInitialMap.get(n.id)! const pos = roleInitialMap.get(n.id)!
return { ...n, x: pos.x, y: pos.y, vx: 0, vy: 0, homeX: pos.x, homeY: pos.y } return { ...n, x: pos.x, y: pos.y, vx: 0, vy: 0, homeX: pos.x, homeY: pos.y }
} }
@@ -517,9 +537,9 @@ export function useForceSimulation(
}) })
}) })
// Entity connectors to timeline // Role connectors to timeline (education nodes on left don't connect)
const roleConnectors = connectorGroup.selectAll('line.role-connector') const roleConnectors = connectorGroup.selectAll('line.role-connector')
.data(nodes.filter(n => isEntityNode(n.type))) .data(nodes.filter(n => isRoleNode(n.type)))
.join('line') .join('line')
.attr('class', 'role-connector') .attr('class', 'role-connector')
.attr('stroke', 'var(--border)') .attr('stroke', 'var(--border)')
@@ -539,13 +559,15 @@ export function useForceSimulation(
.id(d => d.id) .id(d => d.id)
.distance(isMobile ? 56 : Math.round(120 * sf)) .distance(isMobile ? 56 : Math.round(120 * sf))
.strength(d => (d as SimLink).strength * 0.15)) .strength(d => (d as SimLink).strength * 0.15))
.force('x', d3.forceX<SimNode>(d => d.homeX).strength(d => isEntityNode(d.type) ? 1.0 : 0.6)) .force('x', d3.forceX<SimNode>(d => d.homeX).strength(d =>
isRoleNode(d.type) ? 1.0 : d.type === 'education' ? 0.9 : 0.6
))
.force('y', d3.forceY<SimNode>(d => { .force('y', d3.forceY<SimNode>(d => {
if (isEntityNode(d.type)) { if (isEntityNode(d.type)) {
return yScale(fractionalYear(d)) return yScale(fractionalYear(d))
} }
return d.homeY return d.homeY
}).strength(d => isEntityNode(d.type) ? 0.98 : 0.25)) }).strength(d => isRoleNode(d.type) ? 0.98 : d.type === 'education' ? 0.85 : 0.25))
.force('collide', d3.forceCollide<SimNode>(d => .force('collide', d3.forceCollide<SimNode>(d =>
isEntityNode(d.type) ? Math.max(rw, rh) / 2 + (isMobile ? 8 : Math.round(10 * sf)) : srActive + (isMobile ? 14 : Math.round(16 * sf)) isEntityNode(d.type) ? Math.max(rw, rh) / 2 + (isMobile ? 8 : Math.round(10 * sf)) : srActive + (isMobile ? 14 : Math.round(16 * sf))
).iterations(3)) ).iterations(3))
@@ -556,9 +578,12 @@ export function useForceSimulation(
const renderTick = () => { const renderTick = () => {
nodes.forEach(d => { nodes.forEach(d => {
if (isEntityNode(d.type)) { if (isRoleNode(d.type)) {
d.x = Math.max(rw / 2 + 6, Math.min(axisX - roleGap - rw / 2 + rw / 2, d.x)) d.x = Math.max(rw / 2 + 6, Math.min(axisX - roleGap - rw / 2 + rw / 2, d.x))
d.y = Math.max(rh / 2 + topPadding, Math.min(height - rh / 2 - bottomPadding, d.y)) d.y = Math.max(rh / 2 + topPadding, Math.min(height - rh / 2 - bottomPadding, d.y))
} else if (d.type === 'education') {
d.x = Math.max(rw / 2 + 6, Math.min(skillZoneRight, d.x))
d.y = Math.max(rh / 2 + topPadding, Math.min(height - rh / 2 - bottomPadding, d.y))
} else { } else {
d.x = Math.max(srActive + 6, Math.min(skillZoneRight, d.x)) d.x = Math.max(srActive + 6, Math.min(skillZoneRight, d.x))
d.y = Math.max(srActive + topPadding, Math.min(height - skillBottomPadding, d.y)) d.y = Math.max(srActive + topPadding, Math.min(height - skillBottomPadding, d.y))