- {outflowAllocations.slice(0, 3).map((alloc, idx) => (
-
-
-
{alloc.percentage}%
+
+ {/* Min Threshold */}
+
+
+
+ setEditValues(v => ({ ...v, minThreshold: Number(e.target.value) }))}
+ className="flex-1"
+ />
+
+ ${(editValues.minThreshold / 1000).toFixed(0)}k
+
- ))}
+
+
+ {/* Max Threshold */}
+
+
+
+ setEditValues(v => ({ ...v, maxThreshold: Number(e.target.value) }))}
+ className="flex-1"
+ />
+
+ ${(editValues.maxThreshold / 1000).toFixed(0)}k
+
+
+
+
+ {/* Visual preview */}
+
+
Threshold Range
+
+
+ 0
+ ${(maxCapacity / 1000).toFixed(0)}k
+
+
+
+ {/* Overflow allocations info */}
+ {hasOverflow && (
+
+
Overflow Allocations (to other funnels)
+
+ {overflowAllocations.map((alloc, idx) => (
+
+
+
{alloc.targetId}
+
{alloc.percentage}%
+
+ ))}
+
+
+ )}
+
+ {/* Spending allocations info */}
+ {hasSpending && (
+
+
Spending Allocations (to outcomes)
+
+ {spendingAllocations.map((alloc, idx) => (
+
+
+
{alloc.targetId}
+
{alloc.percentage}%
+
+ ))}
+
+
+ )}
+
+
+ {/* Buttons */}
+
+
+
- )}
-
-
- MIN: ${(minThreshold/1000).toFixed(0)}k
- MAX: ${(maxThreshold/1000).toFixed(0)}k
-
e.stopPropagation()}
- >
- {/* Track background */}
-
- {/* Red zone (0 to min) */}
-
- {/* Green zone (min to max) */}
-
- {/* Amber zone (max to capacity) */}
-
-
-
- {/* Min handle */}
-
handleSliderMouseDown(e, 'min')}
- />
-
- {/* Max handle */}
-
handleSliderMouseDown(e, 'max')}
- />
-
-
-
- {/* Bottom Handle - Outflow */}
-
-
- {/* Side Handles - Overflow */}
-
-
-
+ )}
+ >
)
}
diff --git a/components/nodes/OutcomeNode.tsx b/components/nodes/OutcomeNode.tsx
new file mode 100644
index 0000000..c93ee8d
--- /dev/null
+++ b/components/nodes/OutcomeNode.tsx
@@ -0,0 +1,97 @@
+'use client'
+
+import { memo } from 'react'
+import { Handle, Position } from '@xyflow/react'
+import type { NodeProps } from '@xyflow/react'
+import type { OutcomeNodeData } from '@/lib/types'
+
+function OutcomeNode({ data, selected }: NodeProps) {
+ const nodeData = data as OutcomeNodeData
+ const { label, description, fundingReceived, fundingTarget, status } = nodeData
+
+ const progress = fundingTarget > 0 ? Math.min(100, (fundingReceived / fundingTarget) * 100) : 0
+ const isFunded = fundingReceived >= fundingTarget
+ const isPartial = fundingReceived > 0 && fundingReceived < fundingTarget
+
+ // Status colors
+ const statusColors = {
+ 'not-started': { bg: 'bg-slate-100', text: 'text-slate-600', border: 'border-slate-300' },
+ 'in-progress': { bg: 'bg-blue-100', text: 'text-blue-700', border: 'border-blue-300' },
+ 'completed': { bg: 'bg-emerald-100', text: 'text-emerald-700', border: 'border-emerald-300' },
+ 'blocked': { bg: 'bg-red-100', text: 'text-red-700', border: 'border-red-300' },
+ }
+
+ const colors = statusColors[status] || statusColors['not-started']
+
+ return (
+
+ {/* Input Handle - Top */}
+
+
+ {/* Header with icon */}
+
+
+ {/* Body */}
+
+ {/* Description */}
+ {description && (
+
{description}
+ )}
+
+ {/* Status badge */}
+
+
+ {status.replace('-', ' ')}
+
+ {isFunded && (
+
+ )}
+
+
+ {/* Funding progress */}
+
+
+ Funding
+
+ ${Math.floor(fundingReceived).toLocaleString()} / ${fundingTarget.toLocaleString()}
+
+
+
+
+ {progress.toFixed(0)}%
+
+
+
+
+ )
+}
+
+export default memo(OutcomeNode)
diff --git a/lib/types.ts b/lib/types.ts
index 55c9ba7..c5425ef 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -1,6 +1,14 @@
import type { Node, Edge } from '@xyflow/react'
-export interface OutflowAllocation {
+// Overflow allocation - funds flowing to OTHER FUNNELS when above max threshold
+export interface OverflowAllocation {
+ targetId: string
+ percentage: number // 0-100
+ color: string
+}
+
+// Spending allocation - funds flowing DOWN to OUTCOMES/OUTPUTS
+export interface SpendingAllocation {
targetId: string
percentage: number // 0-100
color: string
@@ -13,15 +21,28 @@ export interface FunnelNodeData {
maxThreshold: number
maxCapacity: number
inflowRate: number
- outflowAllocations: OutflowAllocation[]
+ // Overflow goes SIDEWAYS to other funnels
+ overflowAllocations: OverflowAllocation[]
+ // Spending goes DOWN to outcomes/outputs
+ spendingAllocations: SpendingAllocation[]
[key: string]: unknown
}
-export type FlowNode = Node
+export interface OutcomeNodeData {
+ label: string
+ description?: string
+ fundingReceived: number
+ fundingTarget: number
+ status: 'not-started' | 'in-progress' | 'completed' | 'blocked'
+ [key: string]: unknown
+}
+
+export type FlowNode = Node
export interface FlowEdgeData {
allocation: number // percentage 0-100
color: string
+ edgeType: 'overflow' | 'spending' // overflow = sideways, spending = downward
[key: string]: unknown
}