|
| 1 | +/** |
| 2 | + * useCoreMetrics Hook |
| 3 | + * |
| 4 | + * This hook provides reactive Core Metric XP computation from Skills and Characteristics. |
| 5 | + * It recalculates automatically when: |
| 6 | + * - Skill XP changes |
| 7 | + * - Attendance is marked |
| 8 | + * - Time spent is edited |
| 9 | + * |
| 10 | + * CORE PRINCIPLE (NON-NEGOTIABLE): |
| 11 | + * - Core Metric XP is COMPUTED, never stored manually |
| 12 | + * - Radar chart reads ONLY Core Metric XP |
| 13 | + * - No hardcoded radar values |
| 14 | + * |
| 15 | + * HARD OVERRIDE LINE: |
| 16 | + * "If the radar chart is not driven entirely by computed Core Metric XP derived from Skills, |
| 17 | + * the implementation is incorrect." |
| 18 | + */ |
| 19 | + |
| 20 | +import { useMemo } from 'react'; |
| 21 | +import { useSkills } from './useSkills'; |
| 22 | +import { useCharacteristics } from './useCharacteristics'; |
| 23 | +import { |
| 24 | + computeAllCoreMetrics, |
| 25 | + getRadarChartData, |
| 26 | + calculateBalanceScore, |
| 27 | + getTotalMetricXP, |
| 28 | + getAverageMetricLevel, |
| 29 | + getSkillMetricContributions, |
| 30 | + ComputedCoreMetric, |
| 31 | + SkillContributionData, |
| 32 | + CharacteristicContributionData, |
| 33 | +} from '@/lib/coreMetricCalculation'; |
| 34 | +import { CoreMetricName, PHYSICAL_BALANCE_METRICS } from '@/lib/coreMetrics'; |
| 35 | + |
| 36 | +export interface UseCoreMetricsResult { |
| 37 | + // Computed Core Metrics with XP values |
| 38 | + coreMetrics: ComputedCoreMetric[]; |
| 39 | + |
| 40 | + // Radar chart ready data |
| 41 | + radarData: { label: string; value: number }[]; |
| 42 | + |
| 43 | + // Aggregate stats |
| 44 | + balanceScore: number; |
| 45 | + totalXP: number; |
| 46 | + averageLevel: number; |
| 47 | + |
| 48 | + // Loading state |
| 49 | + isLoading: boolean; |
| 50 | + |
| 51 | + // Helper functions for traceability |
| 52 | + getMetricContributors: (metricName: CoreMetricName) => ComputedCoreMetric['contributions']; |
| 53 | + getSkillContributions: (skillId: string) => { metricName: CoreMetricName; weight: number; contributedXp: number }[]; |
| 54 | +} |
| 55 | + |
| 56 | +/** |
| 57 | + * Map skill data to contribution format |
| 58 | + */ |
| 59 | +function mapSkillToContributionData(skill: { |
| 60 | + id: string; |
| 61 | + name: string; |
| 62 | + xp: number; |
| 63 | + area: string | null; |
| 64 | +}): SkillContributionData { |
| 65 | + return { |
| 66 | + id: skill.id, |
| 67 | + name: skill.name, |
| 68 | + xp: skill.xp, |
| 69 | + area: skill.area, |
| 70 | + // contributesTo could be extended if skills store their own mappings |
| 71 | + }; |
| 72 | +} |
| 73 | + |
| 74 | +/** |
| 75 | + * Map characteristic data to contribution format |
| 76 | + * Characteristics contribute to their namesake metric if it exists |
| 77 | + */ |
| 78 | +function mapCharacteristicToContributionData(char: { |
| 79 | + id: string; |
| 80 | + name: string; |
| 81 | + xp: number; |
| 82 | +}): CharacteristicContributionData { |
| 83 | + // Check if the characteristic name matches a Core Metric |
| 84 | + const metricName = PHYSICAL_BALANCE_METRICS.find( |
| 85 | + m => m.toLowerCase() === char.name.toLowerCase() |
| 86 | + ); |
| 87 | + |
| 88 | + return { |
| 89 | + id: char.id, |
| 90 | + name: char.name, |
| 91 | + xp: char.xp, |
| 92 | + // If characteristic matches a metric name, it contributes 100% to that metric |
| 93 | + contributesTo: metricName ? { [metricName]: 1.0 } : {}, |
| 94 | + }; |
| 95 | +} |
| 96 | + |
| 97 | +/** |
| 98 | + * Hook to compute and access Core Metrics XP |
| 99 | + * |
| 100 | + * This hook is the single source of truth for radar chart data. |
| 101 | + * It automatically recomputes when skills or characteristics change. |
| 102 | + */ |
| 103 | +export function useCoreMetrics(): UseCoreMetricsResult { |
| 104 | + const { skills, isLoading: skillsLoading } = useSkills(); |
| 105 | + const { characteristics, isLoading: characteristicsLoading } = useCharacteristics(); |
| 106 | + |
| 107 | + const isLoading = skillsLoading || characteristicsLoading; |
| 108 | + |
| 109 | + // Map skills to contribution data |
| 110 | + const skillContributions = useMemo(() => { |
| 111 | + return skills.map(mapSkillToContributionData); |
| 112 | + }, [skills]); |
| 113 | + |
| 114 | + // Map characteristics to contribution data |
| 115 | + const characteristicContributions = useMemo(() => { |
| 116 | + return characteristics.map(mapCharacteristicToContributionData); |
| 117 | + }, [characteristics]); |
| 118 | + |
| 119 | + // Compute all Core Metrics (this is the main calculation) |
| 120 | + const coreMetrics = useMemo(() => { |
| 121 | + return computeAllCoreMetrics(skillContributions, characteristicContributions); |
| 122 | + }, [skillContributions, characteristicContributions]); |
| 123 | + |
| 124 | + // Get radar chart data (clamped to MAX_METRIC_XP) |
| 125 | + const radarData = useMemo(() => { |
| 126 | + return getRadarChartData(coreMetrics); |
| 127 | + }, [coreMetrics]); |
| 128 | + |
| 129 | + // Calculate aggregate stats |
| 130 | + const balanceScore = useMemo(() => calculateBalanceScore(coreMetrics), [coreMetrics]); |
| 131 | + const totalXP = useMemo(() => getTotalMetricXP(coreMetrics), [coreMetrics]); |
| 132 | + const averageLevel = useMemo(() => getAverageMetricLevel(coreMetrics), [coreMetrics]); |
| 133 | + |
| 134 | + // Helper: Get which skills/characteristics contribute to a specific metric |
| 135 | + const getMetricContributors = useMemo(() => { |
| 136 | + return (metricName: CoreMetricName) => { |
| 137 | + const metric = coreMetrics.find(m => m.name === metricName); |
| 138 | + return metric?.contributions || []; |
| 139 | + }; |
| 140 | + }, [coreMetrics]); |
| 141 | + |
| 142 | + // Helper: Get which metrics a skill contributes to |
| 143 | + const getSkillContributions = useMemo(() => { |
| 144 | + return (skillId: string) => { |
| 145 | + const skill = skills.find(s => s.id === skillId); |
| 146 | + if (!skill) return []; |
| 147 | + |
| 148 | + const contributionData = mapSkillToContributionData(skill); |
| 149 | + return getSkillMetricContributions(contributionData); |
| 150 | + }; |
| 151 | + }, [skills]); |
| 152 | + |
| 153 | + return { |
| 154 | + coreMetrics, |
| 155 | + radarData, |
| 156 | + balanceScore, |
| 157 | + totalXP, |
| 158 | + averageLevel, |
| 159 | + isLoading, |
| 160 | + getMetricContributors, |
| 161 | + getSkillContributions, |
| 162 | + }; |
| 163 | +} |
0 commit comments