Skip to content

Commit ab5e48b

Browse files
Copilotankurrera
andcommitted
feat: Sync Skills ↔ Core Metrics ↔ Physical Balance Radar (Live & Reactive)
- Add contributes_to field to Skill interface for explicit metric mapping - Update useCoreMetrics hook to use skill's explicit mappings - Remove manual XP editing from EditSkillDialog (XP is now derived only) - Add metric contribution display to SkillCard for bi-directional debugging - Add click handler to RadarChart to show contributing skills per metric - Add MetricDetailDialog for viewing skill contributions per axis - Add comprehensive test suite for skill-metric synchronization Co-authored-by: ankurrera <186232326+ankurrera@users.noreply.github.com>
1 parent f0c0423 commit ab5e48b

6 files changed

Lines changed: 690 additions & 42 deletions

File tree

src/components/skills/EditSkillDialog.tsx

Lines changed: 65 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import { useState } from "react";
1+
import { useState, useEffect } from "react";
22
import { Skill, useSkills } from "@/hooks/useSkills";
33
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
44
import { Button } from "@/components/ui/button";
55
import { Input } from "@/components/ui/input";
66
import { Textarea } from "@/components/ui/textarea";
7+
import { getSkillMetricContributions } from "@/lib/coreMetricCalculation";
8+
import { getDefaultMapping } from "@/lib/coreMetrics";
79

810
interface EditSkillDialogProps {
911
skill: Skill;
@@ -17,7 +19,14 @@ const EditSkillDialog = ({ skill, open, onOpenChange }: EditSkillDialogProps) =>
1719
const [description, setDescription] = useState(skill.description || "");
1820
const [area, setArea] = useState(skill.area || "");
1921
const [coverImage, setCoverImage] = useState(skill.cover_image || "");
20-
const [xp, setXP] = useState(skill.xp.toString());
22+
23+
// Reset form when skill changes
24+
useEffect(() => {
25+
setName(skill.name);
26+
setDescription(skill.description || "");
27+
setArea(skill.area || "");
28+
setCoverImage(skill.cover_image || "");
29+
}, [skill]);
2130

2231
const handleSubmit = (e: React.FormEvent) => {
2332
e.preventDefault();
@@ -30,7 +39,7 @@ const EditSkillDialog = ({ skill, open, onOpenChange }: EditSkillDialogProps) =>
3039
description: description.trim() || null,
3140
area: area.trim() || null,
3241
cover_image: coverImage.trim() || null,
33-
xp: parseInt(xp) || 0,
42+
// XP is computed from attendance records - not manually editable
3443
},
3544
{
3645
onSuccess: () => {
@@ -40,6 +49,16 @@ const EditSkillDialog = ({ skill, open, onOpenChange }: EditSkillDialogProps) =>
4049
);
4150
};
4251

52+
// Calculate which metrics this skill affects
53+
const skillContributionData = {
54+
id: skill.id,
55+
name: skill.name,
56+
xp: skill.xp,
57+
area: skill.area,
58+
contributesTo: skill.contributes_to || undefined,
59+
};
60+
const metricContributions = getSkillMetricContributions(skillContributionData);
61+
4362
return (
4463
<Dialog open={open} onOpenChange={onOpenChange}>
4564
<DialogContent className="system-panel max-w-2xl">
@@ -93,36 +112,53 @@ const EditSkillDialog = ({ skill, open, onOpenChange }: EditSkillDialogProps) =>
93112
/>
94113
</div>
95114

96-
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
97-
{/* Cover Image URL */}
98-
<div>
99-
<label className="text-xs text-muted-foreground uppercase tracking-wider mb-2 block">
100-
Cover Image URL
101-
</label>
102-
<Input
103-
value={coverImage}
104-
onChange={(e) => setCoverImage(e.target.value)}
105-
placeholder="https://..."
106-
className="bg-input border-border"
107-
/>
108-
</div>
115+
{/* Cover Image URL */}
116+
<div>
117+
<label className="text-xs text-muted-foreground uppercase tracking-wider mb-2 block">
118+
Cover Image URL
119+
</label>
120+
<Input
121+
value={coverImage}
122+
onChange={(e) => setCoverImage(e.target.value)}
123+
placeholder="https://..."
124+
className="bg-input border-border"
125+
/>
126+
</div>
109127

110-
{/* XP */}
111-
<div>
112-
<label className="text-xs text-muted-foreground uppercase tracking-wider mb-2 block">
113-
XP
114-
</label>
115-
<Input
116-
type="number"
117-
value={xp}
118-
onChange={(e) => setXP(e.target.value)}
119-
placeholder="0"
120-
className="bg-input border-border"
121-
min="0"
122-
/>
128+
{/* XP Display (Read-only) - Shows computed value */}
129+
<div className="border-t border-border/30 pt-4">
130+
<div className="flex items-center justify-between mb-3">
131+
<span className="text-xs text-muted-foreground uppercase tracking-wider">
132+
Current XP (computed from attendance)
133+
</span>
134+
<span className="text-lg font-medium text-foreground">
135+
{skill.xp} XP
136+
</span>
123137
</div>
124138
</div>
125139

140+
{/* Metric Contributions Display - Bi-directional debugging */}
141+
{metricContributions.length > 0 && (
142+
<div className="border-t border-border/30 pt-4">
143+
<h4 className="text-xs text-muted-foreground uppercase tracking-wider mb-3">
144+
Core Metrics Affected
145+
</h4>
146+
<div className="grid grid-cols-2 gap-2">
147+
{metricContributions.map((contribution) => (
148+
<div
149+
key={contribution.metricName}
150+
className="flex items-center justify-between p-2 bg-muted/50 rounded text-sm"
151+
>
152+
<span className="text-foreground">{contribution.metricName}</span>
153+
<span className="text-muted-foreground">
154+
+{contribution.contributedXp} XP ({Math.round(contribution.weight * 100)}%)
155+
</span>
156+
</div>
157+
))}
158+
</div>
159+
</div>
160+
)}
161+
126162
{/* Actions */}
127163
<div className="flex gap-3 pt-4">
128164
<Button

src/components/skills/SkillCard.tsx

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import { useState } from "react";
22
import { Skill, useSkills } from "@/hooks/useSkills";
33
import { Button } from "@/components/ui/button";
44
import { calculateLevelProgress } from "@/lib/levelCalculation";
5-
import { Edit2, Trash2, Star, Power, PowerOff, Calendar as CalendarIcon } from "lucide-react";
5+
import { Edit2, Trash2, Star, Power, PowerOff, Calendar as CalendarIcon, Target } from "lucide-react";
66
import { getConsistencyStatusMessage } from "@/lib/consistencyCalculations";
7+
import { getSkillMetricContributions } from "@/lib/coreMetricCalculation";
78
import EditSkillDialog from "./EditSkillDialog";
89
import ConfirmDialog from "./ConfirmDialog";
910
import SkillCalendar from "./SkillCalendar";
1011
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
12+
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip";
1113

1214
interface SkillCardProps {
1315
skill: Skill;
@@ -21,6 +23,16 @@ const SkillCard = ({ skill }: SkillCardProps) => {
2123

2224
const progress = calculateLevelProgress(skill.xp);
2325

26+
// Calculate which metrics this skill affects (bi-directional debugging)
27+
const skillContributionData = {
28+
id: skill.id,
29+
name: skill.name,
30+
xp: skill.xp,
31+
area: skill.area,
32+
contributesTo: skill.contributes_to || undefined,
33+
};
34+
const metricContributions = getSkillMetricContributions(skillContributionData);
35+
2436
const handleToggleActive = () => {
2537
updateSkill.mutate({
2638
id: skill.id,
@@ -106,6 +118,34 @@ const SkillCard = ({ skill }: SkillCardProps) => {
106118
</div>
107119
</div>
108120

121+
{/* Metric Contributions - shows which Core Metrics this skill affects */}
122+
{metricContributions.length > 0 && (
123+
<TooltipProvider>
124+
<div className="flex items-center gap-1 text-xs flex-wrap">
125+
<Target className="w-3 h-3 text-muted-foreground" />
126+
{metricContributions.slice(0, 3).map((contribution) => (
127+
<Tooltip key={contribution.metricName}>
128+
<TooltipTrigger asChild>
129+
<span className="text-muted-foreground bg-muted/50 px-1.5 py-0.5 rounded cursor-help">
130+
{contribution.metricName}
131+
</span>
132+
</TooltipTrigger>
133+
<TooltipContent>
134+
<p className="text-xs">
135+
+{contribution.contributedXp} XP ({Math.round(contribution.weight * 100)}% weight)
136+
</p>
137+
</TooltipContent>
138+
</Tooltip>
139+
))}
140+
{metricContributions.length > 3 && (
141+
<span className="text-muted-foreground">
142+
+{metricContributions.length - 3} more
143+
</span>
144+
)}
145+
</div>
146+
</TooltipProvider>
147+
)}
148+
109149
{/* Streak and Consistency */}
110150
<div className="flex items-center justify-between text-xs border-t border-border/30 pt-2">
111151
<div className="flex items-center gap-2">

0 commit comments

Comments
 (0)