feat: The Big One UI #47
@@ -306,14 +306,17 @@ function PhaseDashboard({
|
|||||||
setActiveIds: React.Dispatch<React.SetStateAction<Record<string, boolean>>>,
|
setActiveIds: React.Dispatch<React.SetStateAction<Record<string, boolean>>>,
|
||||||
goalIndex: number
|
goalIndex: number
|
||||||
}) {
|
}) {
|
||||||
const getGoals = useProgramStore((s) => s.getGoalsInPhase);
|
const getGoalsWithDepth = useProgramStore((s) => s.getGoalsWithDepth);
|
||||||
const getTriggers = useProgramStore((s) => s.getTriggersInPhase);
|
const getTriggers = useProgramStore((s) => s.getTriggersInPhase);
|
||||||
const getNorms = useProgramStore((s) => s.getNormsInPhase);
|
const getNorms = useProgramStore((s) => s.getNormsInPhase);
|
||||||
|
|
||||||
// Prepare data view models
|
// Prepare data view models
|
||||||
const goals = (getGoals(phaseId) as GoalNode[]).map(g => ({
|
const goals = getGoalsWithDepth(phaseId).map((g) => ({
|
||||||
...g,
|
...g,
|
||||||
achieved: activeIds[g.id] ?? false,
|
id: g.id as string,
|
||||||
|
name: g.name as string,
|
||||||
|
achieved: activeIds[g.id as string] ?? false,
|
||||||
|
level: g.level, // Pass this new property to the UI
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const triggers = (getTriggers(phaseId) as TriggerNode[]).map(t => ({
|
const triggers = (getTriggers(phaseId) as TriggerNode[]).map(t => ({
|
||||||
|
|||||||
@@ -91,13 +91,14 @@ export const DirectSpeechInput: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// --- interface for goals/triggers/norms/conditional norms ---
|
// --- interface for goals/triggers/norms/conditional norms ---
|
||||||
type StatusItem = {
|
export type StatusItem = {
|
||||||
id?: string | number;
|
id?: string | number;
|
||||||
achieved?: boolean;
|
achieved?: boolean;
|
||||||
description?: string;
|
description?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
norm?: string;
|
norm?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
level?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface StatusListProps {
|
interface StatusListProps {
|
||||||
@@ -129,7 +130,7 @@ export const StatusList: React.FC<StatusListProps> = ({
|
|||||||
const isCurrentGoal = type === 'goal' && idx === currentGoalIndex;
|
const isCurrentGoal = type === 'goal' && idx === currentGoalIndex;
|
||||||
const canOverride = (showIndicator && !isActive) || (type === 'cond_norm' && isActive);
|
const canOverride = (showIndicator && !isActive) || (type === 'cond_norm' && isActive);
|
||||||
|
|
||||||
|
const indentation = (item.level || 0) * 20;
|
||||||
|
|
||||||
const handleOverrideClick = () => {
|
const handleOverrideClick = () => {
|
||||||
if (!canOverride) return;
|
if (!canOverride) return;
|
||||||
@@ -147,7 +148,10 @@ export const StatusList: React.FC<StatusListProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={item.id ?? idx} className={styles.statusItem}>
|
<li key={item.id ?? idx}
|
||||||
|
className={styles.statusItem}
|
||||||
|
style={{ paddingLeft: `${indentation}px` }}
|
||||||
|
>
|
||||||
{showIndicator && (
|
{showIndicator && (
|
||||||
<span
|
<span
|
||||||
className={`${styles.statusIndicator} ${isActive ? styles.active : styles.inactive} ${canOverride ? styles.clickable : ''}`}
|
className={`${styles.statusIndicator} ${isActive ? styles.active : styles.inactive} ${canOverride ? styles.clickable : ''}`}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import {create} from "zustand";
|
|||||||
// the type of a reduced program
|
// the type of a reduced program
|
||||||
export type ReducedProgram = { phases: Record<string, unknown>[] };
|
export type ReducedProgram = { phases: Record<string, unknown>[] };
|
||||||
|
|
||||||
|
export type GoalWithDepth = Record<string, unknown> & { level: number };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the type definition of the programStore
|
* the type definition of the programStore
|
||||||
*/
|
*/
|
||||||
@@ -18,6 +20,7 @@ export type ProgramState = {
|
|||||||
getPhaseNames: () => string[];
|
getPhaseNames: () => string[];
|
||||||
getNormsInPhase: (currentPhaseId: string) => Record<string, unknown>[];
|
getNormsInPhase: (currentPhaseId: string) => Record<string, unknown>[];
|
||||||
getGoalsInPhase: (currentPhaseId: string) => Record<string, unknown>[];
|
getGoalsInPhase: (currentPhaseId: string) => Record<string, unknown>[];
|
||||||
|
getGoalsWithDepth: (currentPhaseId: string) => GoalWithDepth[];
|
||||||
getTriggersInPhase: (currentPhaseId: string) => Record<string, unknown>[];
|
getTriggersInPhase: (currentPhaseId: string) => Record<string, unknown>[];
|
||||||
// if more specific utility functions are needed they can be added here:
|
// if more specific utility functions are needed they can be added here:
|
||||||
}
|
}
|
||||||
@@ -70,6 +73,51 @@ const useProgramStore = create<ProgramState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
throw new Error(`phase with id:"${currentPhaseId}" not found`)
|
throw new Error(`phase with id:"${currentPhaseId}" not found`)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getGoalsWithDepth: (currentPhaseId: string) => {
|
||||||
|
const program = get().currentProgram;
|
||||||
|
const phase = program.phases.find(val => val["id"] === currentPhaseId);
|
||||||
|
|
||||||
|
if (!phase) {
|
||||||
|
throw new Error(`phase with id:"${currentPhaseId}" not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootGoals = phase["goals"] as Record<string, unknown>[];
|
||||||
|
const flatList: GoalWithDepth[] = [];
|
||||||
|
|
||||||
|
// Helper: Define this ONCE, outside the loop
|
||||||
|
const isGoal = (item: Record<string, unknown>) => {
|
||||||
|
return item["plan"] !== undefined && item["plan"] !== null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Recursive helper function
|
||||||
|
const traverse = (goals: Record<string, unknown>[], depth: number) => {
|
||||||
|
goals.forEach((goal) => {
|
||||||
|
// 1. Add the current goal to the list
|
||||||
|
flatList.push({ ...goal, level: depth });
|
||||||
|
|
||||||
|
// 2. Check for children
|
||||||
|
const plan = goal["plan"] as Record<string, unknown> | undefined;
|
||||||
|
|
||||||
|
if (plan && Array.isArray(plan["steps"])) {
|
||||||
|
const steps = plan["steps"] as Record<string, unknown>[];
|
||||||
|
|
||||||
|
// 3. FILTER: Only recurse on steps that are actually goals
|
||||||
|
// If we just passed 'steps', we might accidentally add Actions/Speeches to the goal list
|
||||||
|
const childGoals = steps.filter(isGoal);
|
||||||
|
|
||||||
|
if (childGoals.length > 0) {
|
||||||
|
traverse(childGoals, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start traversal
|
||||||
|
traverse(rootGoals, 0);
|
||||||
|
|
||||||
|
return flatList;
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* gets the triggers for the provided phase
|
* gets the triggers for the provided phase
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user