rspace-online/modules/funds/lib/map-flow.ts

86 lines
2.8 KiB
TypeScript

/**
* 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;
}