Added Mary Seacole back in
This commit is contained in:
@@ -4,8 +4,8 @@ import {
|
||||
BarChart3, Code2, Database, LayoutDashboard, Bot, FileCode2,
|
||||
Pill, Users, FileCheck, Stethoscope,
|
||||
TrendingUp, Route, BookOpen, Store,
|
||||
Presentation, Calculator, Banknote, Handshake, RefreshCw, Crown,
|
||||
ChevronRight,
|
||||
Presentation, Calculator, Banknote, Handshake, RefreshCw,
|
||||
GitBranch, Workflow, UserPlus, ChevronRight,
|
||||
} from 'lucide-react'
|
||||
import { CardHeader } from './Card'
|
||||
import { skills } from '@/data/skills'
|
||||
@@ -17,7 +17,8 @@ const iconMap: Record<string, LucideIcon> = {
|
||||
BarChart3, Code2, Database, LayoutDashboard, Bot, FileCode2,
|
||||
Pill, Users, FileCheck, Stethoscope,
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
const { openPanel } = useDetailPanel()
|
||||
const skillsCopy = getSkillsUICopy()
|
||||
@@ -219,7 +229,7 @@ export function RepeatMedicationsSubsection({ onNodeHighlight, focusRelatedIds }
|
||||
label,
|
||||
skills: skills
|
||||
.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) => {
|
||||
|
||||
@@ -4,11 +4,18 @@ import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { ExpandableCardShell } from './ExpandableCardShell'
|
||||
import { useDetailPanel } from '@/contexts/DetailPanelContext'
|
||||
import { timelineEntities, timelineConsultations } from '@/data/timeline'
|
||||
import { documents } from '@/data/documents'
|
||||
import { getExperienceEducationUICopy } from '@/lib/profile-content'
|
||||
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'
|
||||
|
||||
const VISIBLE_COUNT = 4
|
||||
const VISIBLE_COUNT = 5
|
||||
|
||||
interface TimelineInterventionItemProps {
|
||||
entity: TimelineEntity
|
||||
@@ -304,6 +311,12 @@ export function TimelineInterventionsSubsection({ onNodeHighlight, highlightedRo
|
||||
}, [])
|
||||
|
||||
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)
|
||||
if (!consultation) return
|
||||
openPanel({ type: 'career-role', consultation })
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
BarChart3, Code2, Database, LayoutDashboard, Bot, FileCode2,
|
||||
Pill, Users, FileCheck, Stethoscope,
|
||||
TrendingUp, Route, BookOpen, Store,
|
||||
Presentation, Calculator, Banknote, Handshake, RefreshCw, Crown,
|
||||
ChevronRight,
|
||||
Presentation, Calculator, Banknote, Handshake, RefreshCw,
|
||||
GitBranch, Workflow, UserPlus, ChevronRight,
|
||||
} from 'lucide-react'
|
||||
import { skills } from '@/data/skills'
|
||||
import { useDetailPanel } from '@/contexts/DetailPanelContext'
|
||||
@@ -16,7 +16,8 @@ const iconMap: Record<string, LucideIcon> = {
|
||||
BarChart3, Code2, Database, LayoutDashboard, Bot, FileCode2,
|
||||
Pill, Users, FileCheck, Stethoscope,
|
||||
TrendingUp, Route, BookOpen, Store,
|
||||
Presentation, Calculator, Banknote, Handshake, RefreshCw, Crown,
|
||||
Presentation, Calculator, Banknote, Handshake, RefreshCw,
|
||||
GitBranch, Workflow, UserPlus,
|
||||
}
|
||||
|
||||
interface SkillsAllDetailProps {
|
||||
@@ -35,12 +36,21 @@ export function SkillsAllDetail({ category }: SkillsAllDetailProps) {
|
||||
}
|
||||
}, [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 }) => ({
|
||||
id,
|
||||
label,
|
||||
skills: skills
|
||||
.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) => {
|
||||
|
||||
@@ -90,13 +90,13 @@ export const profileContent: DeepReadonly<ProfileContent> = {
|
||||
title: '£2.6M Savings via Automated Algorithm',
|
||||
subtitle: '14,000 patients identified in 3 days',
|
||||
keywords: '2.6m savings automated algorithm python switching 14000 patients cost-effective alternatives prescribing analytics',
|
||||
kpiId: 'years',
|
||||
kpiId: 'algorithm',
|
||||
},
|
||||
{
|
||||
title: '1.2M Population Served',
|
||||
subtitle: 'Norfolk & Waveney Integrated Care System',
|
||||
keywords: '1.2m population served norfolk waveney ics integrated care system primary care secondary care commissioning',
|
||||
kpiId: 'population',
|
||||
title: '20M Prescriptions Decoded Annually',
|
||||
subtitle: '90% coverage — free text to structured data',
|
||||
keywords: '20m prescriptions decoded parsed free text structured data regex ai pipeline daily quantities adherence population scale',
|
||||
kpiId: 'prescriptions',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
+137
-121
@@ -1,7 +1,7 @@
|
||||
import type { SkillMedication } from '@/types/pmr'
|
||||
|
||||
export const skills: SkillMedication[] = [
|
||||
// Technical (6 skills)
|
||||
// Technical (8 skills) — sorted by frequency
|
||||
{
|
||||
id: 'data-analysis',
|
||||
name: 'Data Analysis',
|
||||
@@ -19,6 +19,21 @@ export const skills: SkillMedication[] = [
|
||||
{ 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',
|
||||
name: 'Python',
|
||||
@@ -67,38 +82,6 @@ export const skills: SkillMedication[] = [
|
||||
{ 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',
|
||||
name: 'Algorithm Design',
|
||||
@@ -132,8 +115,24 @@ export const skills: SkillMedication[] = [
|
||||
{ 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',
|
||||
name: 'Medicines Optimisation',
|
||||
@@ -151,52 +150,6 @@ export const skills: SkillMedication[] = [
|
||||
{ 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',
|
||||
name: 'Health Economics',
|
||||
@@ -213,6 +166,36 @@ export const skills: SkillMedication[] = [
|
||||
{ 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',
|
||||
name: 'Clinical Pathways',
|
||||
@@ -229,6 +212,22 @@ export const skills: SkillMedication[] = [
|
||||
{ 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',
|
||||
name: 'Formulary & Commissioning',
|
||||
@@ -251,7 +250,7 @@ export const skills: SkillMedication[] = [
|
||||
shortName: 'Community Pharm.',
|
||||
frequency: 'As needed',
|
||||
startYear: 2015,
|
||||
yearsOfExperience: 11,
|
||||
yearsOfExperience: 7,
|
||||
category: 'Clinical',
|
||||
status: 'Active',
|
||||
icon: 'Store',
|
||||
@@ -262,36 +261,37 @@ export const skills: SkillMedication[] = [
|
||||
],
|
||||
},
|
||||
|
||||
// Strategic (6 skills)
|
||||
// Strategic (6 skills) — sorted by frequency
|
||||
{
|
||||
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' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'financial-modelling',
|
||||
name: 'Financial Scenario Modelling',
|
||||
shortName: 'Financial Modelling',
|
||||
frequency: 'Weekly',
|
||||
id: 'stakeholder-engagement',
|
||||
name: 'Stakeholder Engagement',
|
||||
shortName: 'Stakeholders',
|
||||
frequency: 'Daily',
|
||||
startYear: 2022,
|
||||
yearsOfExperience: 4,
|
||||
category: 'Strategic',
|
||||
status: 'Active',
|
||||
icon: 'Calculator',
|
||||
icon: 'Handshake',
|
||||
prescribingHistory: [
|
||||
{ year: 2022, description: 'Started: High-cost drug financial impact modelling' },
|
||||
{ year: 2024, description: 'Increased: DOAC switching scenario model, rebate mechanics' },
|
||||
{ year: 2025, description: 'Current: Efficiency programme prioritisation, £215M budget forecasting' },
|
||||
{ year: 2022, description: 'Started: Clinical lead engagement across care sectors' },
|
||||
{ year: 2024, description: 'Increased: Executive communication, CMO presentations' },
|
||||
{ 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',
|
||||
name: 'Stakeholder Engagement',
|
||||
shortName: 'Stakeholders',
|
||||
frequency: 'Daily',
|
||||
id: 'financial-modelling',
|
||||
name: 'Financial Scenario Modelling',
|
||||
shortName: 'Financial Modelling',
|
||||
frequency: 'Weekly',
|
||||
startYear: 2022,
|
||||
yearsOfExperience: 4,
|
||||
category: 'Strategic',
|
||||
status: 'Active',
|
||||
icon: 'Handshake',
|
||||
icon: 'Calculator',
|
||||
prescribingHistory: [
|
||||
{ year: 2022, description: 'Started: Clinical lead engagement across care sectors' },
|
||||
{ year: 2024, description: 'Increased: Executive communication, CMO presentations' },
|
||||
{ year: 2022, description: 'Started: High-cost drug financial impact modelling' },
|
||||
{ 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' },
|
||||
],
|
||||
},
|
||||
@@ -338,19 +353,20 @@ export const skills: SkillMedication[] = [
|
||||
icon: 'RefreshCw',
|
||||
},
|
||||
{
|
||||
id: 'healthcare-leadership',
|
||||
name: 'Healthcare Leadership',
|
||||
shortName: 'Leadership',
|
||||
id: 'team-development',
|
||||
name: 'Team Development',
|
||||
shortName: 'Team Dev',
|
||||
frequency: 'Daily',
|
||||
startYear: 2018,
|
||||
yearsOfExperience: 8,
|
||||
startYear: 2011,
|
||||
yearsOfExperience: 15,
|
||||
category: 'Strategic',
|
||||
status: 'Active',
|
||||
icon: 'Crown',
|
||||
icon: 'UserPlus',
|
||||
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' },
|
||||
{ year: 2017, description: 'Started: National induction training, NVQ supervision, NMS training video' },
|
||||
{ year: 2018, description: 'Developed: Mary Seacole Programme, formal leadership development' },
|
||||
{ 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',
|
||||
'change-management',
|
||||
'healthcare-leadership',
|
||||
'team-development',
|
||||
],
|
||||
skillStrengths: {
|
||||
'data-analysis': 1.0,
|
||||
@@ -88,6 +89,7 @@ const timelineEntitySeeds: TimelineEntity[] = [
|
||||
'stakeholder-engagement': 0.9,
|
||||
'change-management': 0.7,
|
||||
'healthcare-leadership': 0.9,
|
||||
'team-development': 0.8,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -149,6 +151,7 @@ const timelineEntitySeeds: TimelineEntity[] = [
|
||||
'stakeholder-engagement',
|
||||
'change-management',
|
||||
'healthcare-leadership',
|
||||
'team-development',
|
||||
],
|
||||
skillStrengths: {
|
||||
'data-analysis': 0.95,
|
||||
@@ -171,6 +174,7 @@ const timelineEntitySeeds: TimelineEntity[] = [
|
||||
'stakeholder-engagement': 0.9,
|
||||
'change-management': 0.7,
|
||||
'healthcare-leadership': 0.85,
|
||||
'team-development': 0.75,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -281,6 +285,7 @@ const timelineEntitySeeds: TimelineEntity[] = [
|
||||
'change-management',
|
||||
'stakeholder-engagement',
|
||||
'healthcare-leadership',
|
||||
'team-development',
|
||||
],
|
||||
skillStrengths: {
|
||||
'data-analysis': 0.7,
|
||||
@@ -290,6 +295,7 @@ const timelineEntitySeeds: TimelineEntity[] = [
|
||||
'change-management': 0.6,
|
||||
'stakeholder-engagement': 0.6,
|
||||
'healthcare-leadership': 0.7,
|
||||
'team-development': 0.8,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -375,13 +381,50 @@ const timelineEntitySeeds: TimelineEntity[] = [
|
||||
'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',
|
||||
kind: 'education',
|
||||
title: 'MPharm (Hons) 2:1',
|
||||
graphLabel: 'MPharm',
|
||||
organization: 'University of East Anglia',
|
||||
orgColor: '#7B2D8E',
|
||||
orgColor: '#6B21A8',
|
||||
dateRange: {
|
||||
start: '2011-09-01',
|
||||
end: '2015-06-30',
|
||||
@@ -416,7 +459,7 @@ const timelineEntitySeeds: TimelineEntity[] = [
|
||||
title: 'A-Levels',
|
||||
graphLabel: 'A-Levels',
|
||||
organization: 'Highworth Grammar School',
|
||||
orgColor: '#9C27B0',
|
||||
orgColor: '#6B21A8',
|
||||
dateRange: {
|
||||
start: '2009-09-01',
|
||||
end: '2011-06-30',
|
||||
|
||||
@@ -26,6 +26,10 @@ function hashString(input: string): number {
|
||||
return Math.abs(hash)
|
||||
}
|
||||
|
||||
function isRoleNode(type: string): boolean {
|
||||
return type === 'role'
|
||||
}
|
||||
|
||||
function isEntityNode(type: string): boolean {
|
||||
return type === 'role' || type === 'education'
|
||||
}
|
||||
@@ -48,7 +52,8 @@ function getHeight(width: number, containerHeight?: number | null): number {
|
||||
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(
|
||||
svgRef: React.RefObject<SVGSVGElement | null>,
|
||||
@@ -84,7 +89,8 @@ export function useForceSimulation(
|
||||
|
||||
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 maxYear = Math.max(...years)
|
||||
|
||||
@@ -301,6 +307,16 @@ export function useForceSimulation(
|
||||
const skillZoneLeft = sidePadding + srActive
|
||||
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
|
||||
const skillRoleKey = new Map<string, string>() // skillId -> sorted role key
|
||||
const skillBaseY = new Map<string, number>() // skillId -> base homeY
|
||||
@@ -312,7 +328,7 @@ export function useForceSimulation(
|
||||
skillRoleKey.set(n.id, key)
|
||||
|
||||
const positions = roleIds
|
||||
.map(roleId => roleInitialMap.get(roleId))
|
||||
.map(roleId => roleInitialMap.get(roleId) ?? educationInitialMap.get(roleId))
|
||||
.filter(Boolean) as Array<{ x: number; y: number }>
|
||||
const baseY = positions.length > 0
|
||||
? positions.reduce((sum, p) => sum + p.y, 0) / positions.length
|
||||
@@ -336,7 +352,11 @@ export function useForceSimulation(
|
||||
})
|
||||
|
||||
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)!
|
||||
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')
|
||||
.data(nodes.filter(n => isEntityNode(n.type)))
|
||||
.data(nodes.filter(n => isRoleNode(n.type)))
|
||||
.join('line')
|
||||
.attr('class', 'role-connector')
|
||||
.attr('stroke', 'var(--border)')
|
||||
@@ -539,13 +559,15 @@ export function useForceSimulation(
|
||||
.id(d => d.id)
|
||||
.distance(isMobile ? 56 : Math.round(120 * sf))
|
||||
.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 => {
|
||||
if (isEntityNode(d.type)) {
|
||||
return yScale(fractionalYear(d))
|
||||
}
|
||||
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 =>
|
||||
isEntityNode(d.type) ? Math.max(rw, rh) / 2 + (isMobile ? 8 : Math.round(10 * sf)) : srActive + (isMobile ? 14 : Math.round(16 * sf))
|
||||
).iterations(3))
|
||||
@@ -556,9 +578,12 @@ export function useForceSimulation(
|
||||
|
||||
const renderTick = () => {
|
||||
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.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 {
|
||||
d.x = Math.max(srActive + 6, Math.min(skillZoneRight, d.x))
|
||||
d.y = Math.max(srActive + topPadding, Math.min(height - skillBottomPadding, d.y))
|
||||
|
||||
Reference in New Issue
Block a user