Added Mary Seacole back in
This commit is contained in:
@@ -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 })
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
@@ -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: '1–2x 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: '1–2x 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
@@ -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',
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user