/** * Maps TBFF API response data to FlowNode[] for visualization. * Shared between folk-funds-app (data loading) and folk-budget-river (rendering). */ import type { FlowNode, FunnelNodeData, OutcomeNodeData, SourceNodeData } from "./types"; const SPENDING_COLORS = ["#3b82f6", "#8b5cf6", "#ec4899", "#06b6d4", "#10b981", "#6366f1"]; const OVERFLOW_COLORS = ["#f59e0b", "#ef4444", "#f97316", "#eab308", "#dc2626", "#ea580c"]; export function mapFlowToNodes(apiData: any): FlowNode[] { const nodes: FlowNode[] = []; // Map sources (income/deposit streams) if (apiData.sources) { for (const src of apiData.sources) { nodes.push({ id: src.id, type: "source", position: { x: 0, y: 0 }, data: { label: src.label || src.name || "Source", flowRate: src.flowRate ?? src.amount ?? 0, sourceType: src.sourceType || "recurring", targetAllocations: (src.targetAllocations || src.allocations || []).map((a: any, i: number) => ({ targetId: a.targetId, percentage: a.percentage, color: a.color || SPENDING_COLORS[i % SPENDING_COLORS.length], })), } as SourceNodeData, }); } } // Map funnels (budget buckets) if (apiData.funnels) { for (const funnel of apiData.funnels) { nodes.push({ id: funnel.id, type: "funnel", position: { x: 0, y: 0 }, data: { label: funnel.label || funnel.name || "Funnel", currentValue: funnel.currentValue ?? funnel.balance ?? 0, minThreshold: funnel.minThreshold ?? 0, maxThreshold: funnel.maxThreshold ?? funnel.currentValue ?? 10000, maxCapacity: funnel.maxCapacity ?? funnel.maxThreshold ?? 100000, inflowRate: funnel.inflowRate ?? 0, sufficientThreshold: funnel.sufficientThreshold, dynamicOverflow: funnel.dynamicOverflow ?? false, overflowAllocations: (funnel.overflowAllocations || []).map((a: any, i: number) => ({ targetId: a.targetId, percentage: a.percentage, color: a.color || OVERFLOW_COLORS[i % OVERFLOW_COLORS.length], })), spendingAllocations: (funnel.spendingAllocations || []).map((a: any, i: number) => ({ targetId: a.targetId, percentage: a.percentage, color: a.color || SPENDING_COLORS[i % SPENDING_COLORS.length], })), } as FunnelNodeData, }); } } // Map outcomes (funding targets) if (apiData.outcomes) { for (const outcome of apiData.outcomes) { nodes.push({ id: outcome.id, type: "outcome", position: { x: 0, y: 0 }, data: { label: outcome.label || outcome.name || "Outcome", description: outcome.description || "", fundingReceived: outcome.fundingReceived ?? outcome.received ?? 0, fundingTarget: outcome.fundingTarget ?? outcome.target ?? 0, status: outcome.status || "not-started", } as OutcomeNodeData, }); } } return nodes; }