397 lines
20 KiB
TypeScript
397 lines
20 KiB
TypeScript
import Fuse from 'fuse.js'
|
||
|
||
import { consultations } from '@/data/consultations'
|
||
import { documents } from '@/data/documents'
|
||
import { investigations } from '@/data/investigations'
|
||
import { skills } from '@/data/skills'
|
||
import { kpis } from '@/data/kpis'
|
||
import type { DetailPanelContent } from '@/types/pmr'
|
||
|
||
export type PaletteSection = 'Experience' | 'Core Skills' | 'Significant Interventions' | 'Achievements' | 'Education' | 'Quick Actions'
|
||
|
||
export type PaletteAction =
|
||
| { type: 'scroll'; tileId: string }
|
||
| { type: 'expand'; tileId: string; itemId: string }
|
||
| { type: 'link'; url: string }
|
||
| { type: 'download' }
|
||
| { type: 'panel'; panelContent: DetailPanelContent }
|
||
|
||
export type IconColorVariant = 'teal' | 'green' | 'amber' | 'purple'
|
||
|
||
export interface PaletteItem {
|
||
id: string
|
||
title: string
|
||
subtitle: string
|
||
section: PaletteSection
|
||
iconVariant: IconColorVariant
|
||
iconType: 'role' | 'skill' | 'project' | 'achievement' | 'edu' | 'action'
|
||
keywords: string
|
||
action: PaletteAction
|
||
}
|
||
|
||
// Build the full palette dataset matching the concept HTML structure
|
||
export function buildPaletteData(): PaletteItem[] {
|
||
const items: PaletteItem[] = []
|
||
|
||
// Experience — all 4 roles from consultations.ts, open detail panel on select
|
||
consultations.forEach((c) => {
|
||
items.push({
|
||
id: `exp-${c.id}`,
|
||
title: c.role,
|
||
subtitle: `${c.organization} \u00b7 ${c.duration}`,
|
||
section: 'Experience',
|
||
iconVariant: 'teal',
|
||
iconType: 'role',
|
||
keywords: `${c.role.toLowerCase()} ${c.organization.toLowerCase()} ${c.duration.toLowerCase()}`,
|
||
action: { type: 'panel', panelContent: { type: 'career-role', consultation: c } },
|
||
})
|
||
})
|
||
|
||
// Core Skills — all ~21 skills from skills.ts, opening detail panel on select
|
||
skills.forEach((skill) => {
|
||
items.push({
|
||
id: `skill-${skill.id}`,
|
||
title: `${skill.name} \u2014 ${skill.proficiency}%`,
|
||
subtitle: `${skill.frequency} \u00b7 Since ${skill.startYear} \u00b7 ${skill.category}`,
|
||
section: 'Core Skills',
|
||
iconVariant: 'green',
|
||
iconType: 'skill',
|
||
keywords: `${skill.name.toLowerCase()} ${skill.proficiency} ${skill.frequency.toLowerCase()} ${skill.category.toLowerCase()}`,
|
||
action: { type: 'panel', panelContent: { type: 'skill', skill } },
|
||
})
|
||
})
|
||
|
||
// Significant Interventions — all 5 investigations from investigations.ts
|
||
investigations.forEach((inv) => {
|
||
items.push({
|
||
id: `proj-${inv.id}`,
|
||
title: inv.name,
|
||
subtitle: `${inv.methodology.split('.')[0]} \u00b7 ${inv.requestedYear}`,
|
||
section: 'Significant Interventions',
|
||
iconVariant: 'amber',
|
||
iconType: 'project',
|
||
keywords: `${inv.name.toLowerCase()} ${inv.methodology.toLowerCase()} ${inv.techStack.join(' ').toLowerCase()} ${inv.requestedYear}`,
|
||
action: { type: 'panel', panelContent: { type: 'project', investigation: inv } },
|
||
})
|
||
})
|
||
|
||
// Achievements — open corresponding KPI detail panel
|
||
const achievementEntries: Array<{ title: string; sub: string; keywords: string; kpiId: string }> = [
|
||
{
|
||
title: '\u00a314.6M Efficiency Savings Identified',
|
||
sub: 'Data-driven prescribing interventions',
|
||
keywords: '14.6m efficiency savings identified data-driven prescribing interventions money cost',
|
||
kpiId: 'savings',
|
||
},
|
||
{
|
||
title: '\u00a3220M Budget Oversight',
|
||
sub: 'Full analytical accountability to ICB board',
|
||
keywords: '220m budget oversight analytical accountability icb board',
|
||
kpiId: 'budget',
|
||
},
|
||
{
|
||
title: 'Power BI Dashboards for 200+ Users',
|
||
sub: 'Clinicians & commissioners across ICB',
|
||
keywords: 'power bi dashboards 200 users clinicians commissioners',
|
||
kpiId: 'years',
|
||
},
|
||
{
|
||
title: '1.2M Population Served',
|
||
sub: 'Norfolk & Waveney Integrated Care System',
|
||
keywords: '1.2m population served norfolk waveney ics integrated care system',
|
||
kpiId: 'population',
|
||
},
|
||
]
|
||
|
||
achievementEntries.forEach((entry, i) => {
|
||
const kpi = kpis.find(k => k.id === entry.kpiId)
|
||
items.push({
|
||
id: `ach-${i}`,
|
||
title: entry.title,
|
||
subtitle: entry.sub,
|
||
section: 'Achievements',
|
||
iconVariant: 'amber',
|
||
iconType: 'achievement',
|
||
keywords: entry.keywords,
|
||
action: kpi
|
||
? { type: 'panel', panelContent: { type: 'kpi', kpi } }
|
||
: { type: 'scroll', tileId: 'patient-summary' },
|
||
})
|
||
})
|
||
|
||
// Education — matching actual entries in EducationSubsection
|
||
const educationEntries: Array<{ title: string; sub: string; keywords: string }> = [
|
||
{
|
||
title: 'NHS Leadership Academy \u2014 Mary Seacole Programme',
|
||
sub: 'NHS Leadership Academy \u00b7 2018',
|
||
keywords: 'nhs leadership academy mary seacole programme 2018 qualification management',
|
||
},
|
||
{
|
||
title: 'MPharm (Hons) \u2014 2:1',
|
||
sub: 'University of East Anglia \u00b7 2011\u20132015',
|
||
keywords: 'mpharm hons 2:1 university east anglia uea 2011 2015 pharmacy degree',
|
||
},
|
||
{
|
||
title: 'A-Levels',
|
||
sub: 'Highworth Grammar School \u00b7 2009\u20132011',
|
||
keywords: 'a-levels mathematics chemistry politics highworth grammar school 2009 2011',
|
||
},
|
||
{
|
||
title: 'GPhC Registration',
|
||
sub: 'General Pharmaceutical Council \u00b7 August 2016',
|
||
keywords: 'gphc registration general pharmaceutical council 2016 registered pharmacist',
|
||
},
|
||
]
|
||
|
||
educationEntries.forEach((entry, i) => {
|
||
items.push({
|
||
id: `edu-${i}`,
|
||
title: entry.title,
|
||
subtitle: entry.sub,
|
||
section: 'Education',
|
||
iconVariant: 'purple',
|
||
iconType: 'edu',
|
||
keywords: entry.keywords,
|
||
action: { type: 'scroll', tileId: 'patient-pathway' },
|
||
})
|
||
})
|
||
|
||
// Quick Actions
|
||
const quickActions: Array<{ title: string; sub: string; keywords: string; action: PaletteAction }> = [
|
||
{
|
||
title: 'Download CV',
|
||
sub: 'Export as PDF',
|
||
keywords: 'download cv export pdf resume',
|
||
action: { type: 'download' },
|
||
},
|
||
{
|
||
title: 'Send Email',
|
||
sub: 'andy@charlwood.xyz',
|
||
keywords: 'send email contact andy charlwood',
|
||
action: { type: 'link', url: 'mailto:andy@charlwood.xyz' },
|
||
},
|
||
{
|
||
title: 'View LinkedIn',
|
||
sub: 'Professional profile',
|
||
keywords: 'view linkedin professional profile social',
|
||
action: { type: 'link', url: 'https://linkedin.com/in/andycharlwood' },
|
||
},
|
||
{
|
||
title: 'View Projects',
|
||
sub: 'GitHub & portfolio',
|
||
keywords: 'view projects github portfolio code repositories',
|
||
action: { type: 'link', url: 'https://github.com/andycharlwood' },
|
||
},
|
||
]
|
||
|
||
quickActions.forEach((entry, i) => {
|
||
items.push({
|
||
id: `action-${i}`,
|
||
title: entry.title,
|
||
subtitle: entry.sub,
|
||
section: 'Quick Actions',
|
||
iconVariant: 'teal',
|
||
iconType: 'action',
|
||
keywords: entry.keywords,
|
||
action: entry.action,
|
||
})
|
||
})
|
||
|
||
return items
|
||
}
|
||
|
||
// Build a fuse.js search index from palette items
|
||
export function buildSearchIndex(items: PaletteItem[]): Fuse<PaletteItem> {
|
||
return new Fuse(items, {
|
||
keys: [
|
||
{ name: 'title', weight: 2 },
|
||
{ name: 'subtitle', weight: 1 },
|
||
{ name: 'keywords', weight: 1.5 },
|
||
],
|
||
threshold: 0.3,
|
||
includeScore: true,
|
||
minMatchCharLength: 2,
|
||
})
|
||
}
|
||
|
||
// Section ordering for grouped display
|
||
const SECTION_ORDER: PaletteSection[] = [
|
||
'Experience',
|
||
'Core Skills',
|
||
'Significant Interventions',
|
||
'Achievements',
|
||
'Education',
|
||
'Quick Actions',
|
||
]
|
||
|
||
// Group palette items by section, maintaining defined order
|
||
export function groupBySection(items: PaletteItem[]): Array<{ section: PaletteSection; items: PaletteItem[] }> {
|
||
const groups = new Map<PaletteSection, PaletteItem[]>()
|
||
|
||
for (const item of items) {
|
||
const existing = groups.get(item.section)
|
||
if (existing) {
|
||
existing.push(item)
|
||
} else {
|
||
groups.set(item.section, [item])
|
||
}
|
||
}
|
||
|
||
return SECTION_ORDER
|
||
.filter(section => groups.has(section))
|
||
.map(section => ({ section, items: groups.get(section)! }))
|
||
}
|
||
|
||
// Build rich natural-language text representations for semantic embedding.
|
||
// IDs match PaletteItem IDs so embeddings can be correlated back to palette entries.
|
||
export function buildEmbeddingTexts(): Array<{ id: string; text: string }> {
|
||
const texts: Array<{ id: string; text: string }> = []
|
||
|
||
// Consultations (Experience) — enriched with plan outcomes, employer classification, clinical specialties
|
||
consultations.forEach((c) => {
|
||
const isNHS = c.organization.includes('NHS') || c.organization.includes('ICB')
|
||
const employer = isNHS
|
||
? `NHS employer: ${c.organization}`
|
||
: `Private sector employer: ${c.organization} (not NHS)`
|
||
const examBullets = c.examination.join('. ')
|
||
const planOutcomes = c.plan.join('. ')
|
||
const codedDescriptions = c.codedEntries.map(e => e.description).join('. ')
|
||
|
||
// Role-specific enrichment for clinical specialties and methodology
|
||
let roleContext = ''
|
||
if (c.id === 'high-cost-drugs-2022') {
|
||
roleContext = ' Clinical specialties covered: rheumatology, ophthalmology (wet AMD, DMO, RVO), dermatology, gastroenterology, neurology, and migraine. Wrote most of the system\'s high-cost drug pathways, implementing NICE technology appraisals while balancing legal requirements against financial costs and local clinical preferences.'
|
||
} else if (c.id === 'deputy-head-2024') {
|
||
roleContext = ' Created dm+d medicines data table integrating all dictionary of medicines and devices products with standardised strengths, morphine equivalents, and Anticholinergic Burden scoring — single source of truth for all medicines analytics. Supported tirzepatide commissioning (NICE TA1026) with financial projections and authored executive paper advocating primary care model, driving system shift to GP-led delivery.'
|
||
} else if (c.id === 'interim-head-2025') {
|
||
roleContext = ' Built Python switching algorithm using real-world GP prescribing data to identify patients eligible for cost-effective alternatives — compressed months of manual analysis into 3 days. Created novel GP payment system linking incentive rewards to prescribing savings.'
|
||
} else if (c.id === 'pharmacy-manager-2017') {
|
||
roleContext = ' Community pharmacy role at Tesco PLC, a private sector employer. Served as Local Pharmaceutical Committee (LPC) representative for Norfolk. Full HR responsibilities including recruitment, performance management, grievances.'
|
||
}
|
||
|
||
texts.push({
|
||
id: `exp-${c.id}`,
|
||
text: `${c.role} at ${c.organization}, ${c.duration}. ${employer}. ${c.history} Key achievements: ${examBullets}. Outcomes: ${planOutcomes}. ${codedDescriptions}.${roleContext}`,
|
||
})
|
||
})
|
||
|
||
// Skills — enriched with role context and practical application
|
||
const skillContextMap: Record<string, string> = {
|
||
'data-analysis': 'Applied across NHS medicines optimisation, identifying £14.6M efficiency programme. Used for prescribing pattern analysis, budget forecasting, and population health analytics serving 1.2M people.',
|
||
'python': 'Used to build switching algorithms (14,000 patients, £2.6M savings), controlled drug monitoring systems, Blueteq form automation, and Sankey chart visualisation tools. Self-taught.',
|
||
'sql': 'Core tool for patient-level analytics, dm+d data integration, and transforming practice-level data to patient-level SQL analysis. Used across all NHS data roles.',
|
||
'power-bi': 'Built PharMetrics interactive dashboard tracking £220M prescribing budget. Created dashboards used by 200+ clinicians and commissioners across Norfolk & Waveney ICB.',
|
||
'javascript-typescript': 'Used for web development including this portfolio website. Built with React, TypeScript, and Vite.',
|
||
'excel': 'Used for financial modelling, data validation, and ad-hoc analysis. Foundational tool across all roles from community pharmacy to NHS ICB.',
|
||
'algorithm-design': 'Designed patient switching algorithm and automated incentive scheme analysis. Applied to real-world GP prescribing data at population scale.',
|
||
'data-pipelines': 'Built automated data processing pipelines for medicines analytics, enabling self-serve models for team data fluency.',
|
||
'medicines-optimisation': 'Core domain expertise spanning community pharmacy through to NHS ICB-level population health. Led efficiency programmes worth £14.6M+.',
|
||
'population-health': 'Leading population health analytics for 1.2M people across Norfolk & Waveney ICS. Developing patient-level datasets from real-world GP prescribing data.',
|
||
'nice-ta': 'Led NICE technology appraisal implementation across high-cost drug pathways. Covered rheumatology, ophthalmology, dermatology, gastroenterology, neurology, and migraine.',
|
||
'health-economics': 'Financial modelling for DOAC switching programmes, tirzepatide commissioning, and pharmaceutical rebate negotiations.',
|
||
'clinical-pathways': 'Wrote most of the Norfolk & Waveney ICB high-cost drug pathways. Created Sankey chart tool for patient pathway visualisation and trust compliance auditing.',
|
||
'controlled-drugs': 'Built Python-based population-scale monitoring system calculating oral morphine equivalents (OME) across all opioid prescriptions. Enables high-risk patient identification and potential diversion detection.',
|
||
'budget-management': 'Managed £220M NHS prescribing budget with sophisticated forecasting models, variance analysis, and monthly financial reporting to the ICB executive team.',
|
||
'stakeholder-engagement': 'Presented to Chief Medical Officer bimonthly. Engaged with GP practices, trusts, commissioners, and pharmaceutical companies across the integrated care system.',
|
||
'pharma-negotiation': 'Renegotiated pharmaceutical rebate terms ahead of patent expiry, securing improved commercial position for the ICB.',
|
||
'team-development': 'Improved team data fluency through training and documentation. Supervised staff through NVQ3 to pharmacy technician registration. Created national induction training at Tesco.',
|
||
'change-management': 'Completed NHS Mary Seacole Programme (2018, 78%). Led transformation to patient-level SQL analytics and self-serve analytical models.',
|
||
'financial-modelling': 'Built interactive DOAC switching dashboard with rebate mechanics, workforce constraints, and patent expiry timelines. Financial projections for tirzepatide commissioning.',
|
||
'executive-comms': 'Authored executive papers for ICB board including tirzepatide commissioning advocacy. Presented evidence-based recommendations to CMO bimonthly.',
|
||
}
|
||
|
||
skills.forEach((skill) => {
|
||
const context = skillContextMap[skill.id] ?? ''
|
||
texts.push({
|
||
id: `skill-${skill.id}`,
|
||
text: `${skill.name} is a ${skill.category.toLowerCase()} skill used ${skill.frequency.toLowerCase()}, with ${skill.proficiency}% proficiency and ${skill.yearsOfExperience} years of experience since ${skill.startYear}. ${context}`,
|
||
})
|
||
})
|
||
|
||
// KPI-backed Achievements — enriched with full story context and outcomes
|
||
const achievementMap: Array<{ id: string; title: string; subtitle: string; kpiId: string }> = [
|
||
{ id: 'ach-0', title: '£14.6M Efficiency Savings Identified', subtitle: 'Data-driven prescribing interventions', kpiId: 'savings' },
|
||
{ id: 'ach-1', title: '£220M Budget Oversight', subtitle: 'Full analytical accountability to ICB board', kpiId: 'budget' },
|
||
{ id: 'ach-2', title: 'Power BI Dashboards for 200+ Users', subtitle: 'Clinicians & commissioners across ICB', kpiId: 'years' },
|
||
{ id: 'ach-3', title: '1.2M Population Served', subtitle: 'Norfolk & Waveney Integrated Care System', kpiId: 'population' },
|
||
]
|
||
|
||
achievementMap.forEach((entry) => {
|
||
const kpi = kpis.find(k => k.id === entry.kpiId)
|
||
const explanation = kpi?.explanation ?? ''
|
||
const storyParts: string[] = []
|
||
if (kpi?.story) {
|
||
storyParts.push(kpi.story.context)
|
||
storyParts.push(kpi.story.role)
|
||
if (kpi.story.period) storyParts.push(`Period: ${kpi.story.period}.`)
|
||
storyParts.push(`Outcomes: ${kpi.story.outcomes.join('. ')}.`)
|
||
}
|
||
texts.push({
|
||
id: entry.id,
|
||
text: `Achievement: ${entry.title}. ${entry.subtitle}. ${explanation} ${storyParts.join(' ')}`,
|
||
})
|
||
})
|
||
|
||
// Investigations (Significant Interventions) — enriched with role context and cross-references
|
||
const projectContextMap: Record<string, string> = {
|
||
'inv-pharmetrics': 'Built during Deputy Head role at NHS Norfolk & Waveney ICB. Provides self-serve analytics for budget holders across the integrated care system. Live at medicines.charlwood.xyz.',
|
||
'inv-switching-algorithm': 'Built during Interim Head role at NHS Norfolk & Waveney ICB. Uses real-world GP prescribing data to auto-identify patients on expensive drugs suitable for cost-effective alternatives. Compressed months of manual analysis into 3 days. Includes novel GP payment system linking incentive rewards to prescribing savings.',
|
||
'inv-blueteq-gen': 'Built during High-Cost Drugs & Interface Pharmacist role at NHS Norfolk & Waveney ICB. Automates prior approval form creation for high-cost drug pathways spanning rheumatology, ophthalmology, dermatology, gastroenterology, neurology, and migraine.',
|
||
'inv-cd-monitoring': 'Built during Deputy Head role at NHS Norfolk & Waveney ICB. Calculates oral morphine equivalents (OME) across all opioid prescriptions at population scale. Enables previously impossible population-level controlled drug analysis. Related to controlled drugs skill.',
|
||
'inv-sankey-tool': 'Built during High-Cost Drugs & Interface Pharmacist role at NHS Norfolk & Waveney ICB. Visualises patient journeys through high-cost drug pathways. Enables trust-level compliance auditing across multiple clinical specialties.',
|
||
}
|
||
|
||
investigations.forEach((inv) => {
|
||
const techList = inv.techStack.join(', ')
|
||
const resultList = inv.results.join('. ')
|
||
const context = projectContextMap[inv.id] ?? ''
|
||
texts.push({
|
||
id: `proj-${inv.id}`,
|
||
text: `Project: ${inv.name} (${inv.status}, ${inv.requestedYear}). ${inv.methodology} Tech stack: ${techList}. Results: ${resultList}. ${context}`,
|
||
})
|
||
})
|
||
|
||
// Education — enriched with research grades and specific subject details
|
||
const educationItems: Array<{ id: string; docId: string; fallbackTitle: string; fallbackSub: string }> = [
|
||
{ id: 'edu-0', docId: 'doc-mary-seacole', fallbackTitle: 'NHS Leadership Academy — Mary Seacole Programme', fallbackSub: 'NHS Leadership Academy · 2018' },
|
||
{ id: 'edu-1', docId: 'doc-mpharm', fallbackTitle: 'MPharm (Hons) — 2:1', fallbackSub: 'University of East Anglia · 2011–2015' },
|
||
{ id: 'edu-2', docId: 'doc-alevels', fallbackTitle: 'A-Levels', fallbackSub: 'Highworth Grammar School · 2009–2011' },
|
||
{ id: 'edu-3', docId: 'doc-gphc', fallbackTitle: 'GPhC Registration', fallbackSub: 'General Pharmaceutical Council · August 2016' },
|
||
]
|
||
|
||
educationItems.forEach((entry) => {
|
||
const doc = documents.find(d => d.id === entry.docId)
|
||
if (doc) {
|
||
const research = doc.researchDetail ? ` Research: ${doc.researchDetail}.` : ''
|
||
const researchGrade = doc.researchGrade ? ` Research grade: ${doc.researchGrade}.` : ''
|
||
const classification = doc.classification ? ` Classification: ${doc.classification}.` : ''
|
||
texts.push({
|
||
id: entry.id,
|
||
text: `Education: ${doc.title}. ${doc.type} from ${doc.institution ?? doc.source}, ${doc.duration ?? doc.date}.${classification}${research}${researchGrade} ${doc.notes ?? ''}`,
|
||
})
|
||
} else {
|
||
texts.push({
|
||
id: entry.id,
|
||
text: `Education: ${entry.fallbackTitle}. ${entry.fallbackSub}.`,
|
||
})
|
||
}
|
||
})
|
||
|
||
// Quick Actions
|
||
const quickActionTexts: Array<{ id: string; title: string; subtitle: string }> = [
|
||
{ id: 'action-0', title: 'Download CV', subtitle: 'Export as PDF' },
|
||
{ id: 'action-1', title: 'Send Email', subtitle: 'andy@charlwood.xyz' },
|
||
{ id: 'action-2', title: 'View LinkedIn', subtitle: 'Professional profile' },
|
||
{ id: 'action-3', title: 'View Projects', subtitle: 'GitHub & portfolio' },
|
||
]
|
||
|
||
quickActionTexts.forEach((entry) => {
|
||
texts.push({
|
||
id: entry.id,
|
||
text: `${entry.title}. ${entry.subtitle}.`,
|
||
})
|
||
})
|
||
|
||
return texts
|
||
}
|