'use client' import { useState, useEffect, useMemo, useCallback } from 'react' import type { FlowNode, FunnelNodeData, OutcomeNodeData, SourceNodeData, SufficiencyState } from '@/lib/types' import { computeSufficiencyState, computeSystemSufficiency } from '@/lib/simulation' // ─── Layout Types ──────────────────────────────────────── interface RiverLayout { sources: SourceLayout[] funnels: FunnelLayout[] outcomes: OutcomeLayout[] sourceWaterfalls: WaterfallLayout[] overflowBranches: BranchLayout[] spendingWaterfalls: WaterfallLayout[] width: number height: number } interface SourceLayout { id: string label: string flowRate: number x: number y: number width: number } interface FunnelLayout { id: string label: string data: FunnelNodeData x: number y: number riverWidth: number segmentLength: number layer: number status: 'healthy' | 'overflow' | 'critical' sufficiency: SufficiencyState } interface OutcomeLayout { id: string label: string data: OutcomeNodeData x: number y: number poolWidth: number fillPercent: number } interface WaterfallLayout { id: string sourceId: string targetId: string label: string percentage: number x: number xSource: number yStart: number yEnd: number width: number riverEndWidth: number farEndWidth: number direction: 'inflow' | 'outflow' color: string flowAmount: number } interface BranchLayout { sourceId: string targetId: string percentage: number x1: number y1: number x2: number y2: number width: number color: string } // ─── Constants ─────────────────────────────────────────── const LAYER_HEIGHT = 160 const WATERFALL_HEIGHT = 120 const GAP = 40 const MIN_RIVER_WIDTH = 24 const MAX_RIVER_WIDTH = 100 const MIN_WATERFALL_WIDTH = 4 const SEGMENT_LENGTH = 200 const POOL_WIDTH = 100 const POOL_HEIGHT = 60 const SOURCE_HEIGHT = 40 const COLORS = { sourceWaterfall: '#10b981', riverHealthy: ['#0ea5e9', '#06b6d4'], riverOverflow: ['#f59e0b', '#fbbf24'], riverCritical: ['#ef4444', '#f87171'], riverSufficient: ['#fbbf24', '#10b981'], overflowBranch: '#f59e0b', spendingWaterfall: ['#8b5cf6', '#ec4899', '#06b6d4', '#3b82f6', '#10b981', '#6366f1'], outcomePool: '#3b82f6', goldenGlow: '#fbbf24', bg: '#0f172a', text: '#e2e8f0', textMuted: '#94a3b8', } // ─── Helpers ──────────────────────────────────────────── /** Distribute widths proportionally with a minimum floor */ function distributeWidths( percentages: number[], totalAvailable: number, minWidth: number ): number[] { const totalPct = percentages.reduce((s, p) => s + p, 0) if (totalPct === 0) return percentages.map(() => minWidth) let widths = percentages.map(p => (p / totalPct) * totalAvailable) // Enforce minimums: bump small ones up, proportionally reduce large ones const belowMin = widths.filter(w => w < minWidth) if (belowMin.length > 0 && belowMin.length < widths.length) { const deficit = belowMin.reduce((s, w) => s + (minWidth - w), 0) const aboveMinTotal = widths.filter(w => w >= minWidth).reduce((s, w) => s + w, 0) widths = widths.map(w => { if (w < minWidth) return minWidth return Math.max(minWidth, w - (w / aboveMinTotal) * deficit) }) } return widths } // ─── Layout Engine ─────────────────────────────────────── function computeLayout(nodes: FlowNode[]): RiverLayout { const funnelNodes = nodes.filter(n => n.type === 'funnel') const outcomeNodes = nodes.filter(n => n.type === 'outcome') const sourceNodes = nodes.filter(n => n.type === 'source') // Build adjacency: which funnels are children (overflow targets) of which const overflowTargets = new Set() const spendingTargets = new Set() const sourceTargets = new Set() funnelNodes.forEach(n => { const data = n.data as FunnelNodeData data.overflowAllocations?.forEach(a => overflowTargets.add(a.targetId)) data.spendingAllocations?.forEach(a => spendingTargets.add(a.targetId)) }) sourceNodes.forEach(n => { const data = n.data as SourceNodeData data.targetAllocations?.forEach(a => sourceTargets.add(a.targetId)) }) // Root funnels = funnels that are NOT overflow targets of other funnels const rootFunnels = funnelNodes.filter(n => !overflowTargets.has(n.id)) // Assign layers via BFS const funnelLayers = new Map() rootFunnels.forEach(n => funnelLayers.set(n.id, 0)) const queue = [...rootFunnels] while (queue.length > 0) { const current = queue.shift()! const data = current.data as FunnelNodeData const parentLayer = funnelLayers.get(current.id) ?? 0 data.overflowAllocations?.forEach(a => { const child = funnelNodes.find(n => n.id === a.targetId) if (child && !funnelLayers.has(child.id)) { funnelLayers.set(child.id, parentLayer + 1) queue.push(child) } }) } // Find max flow rate for implicit waterfall normalization const allFlowRates = funnelNodes.map(n => (n.data as FunnelNodeData).inflowRate || 1) const maxFlowRate = Math.max(...allFlowRates, 1) // Group funnels by layer, center each layer const layerGroups = new Map() funnelNodes.forEach(n => { const layer = funnelLayers.get(n.id) ?? 0 if (!layerGroups.has(layer)) layerGroups.set(layer, []) layerGroups.get(layer)!.push(n) }) const maxLayer = Math.max(...Array.from(layerGroups.keys()), 0) const sourceLayerY = GAP const funnelStartY = sourceLayerY + SOURCE_HEIGHT + WATERFALL_HEIGHT + GAP const funnelLayouts: FunnelLayout[] = [] for (let layer = 0; layer <= maxLayer; layer++) { const layerNodes = layerGroups.get(layer) || [] const layerY = funnelStartY + layer * (LAYER_HEIGHT + WATERFALL_HEIGHT + GAP) const totalWidth = layerNodes.length * SEGMENT_LENGTH + (layerNodes.length - 1) * GAP * 2 layerNodes.forEach((n, i) => { const data = n.data as FunnelNodeData const fillRatio = Math.min(1, data.currentValue / (data.maxCapacity || 1)) const riverWidth = MIN_RIVER_WIDTH + fillRatio * (MAX_RIVER_WIDTH - MIN_RIVER_WIDTH) const x = -totalWidth / 2 + i * (SEGMENT_LENGTH + GAP * 2) const status: 'healthy' | 'overflow' | 'critical' = data.currentValue > data.maxThreshold ? 'overflow' : data.currentValue < data.minThreshold ? 'critical' : 'healthy' funnelLayouts.push({ id: n.id, label: data.label, data, x, y: layerY, riverWidth, segmentLength: SEGMENT_LENGTH, layer, status, sufficiency: computeSufficiencyState(data), }) }) } // Source layouts const sourceLayouts: SourceLayout[] = sourceNodes.map((n, i) => { const data = n.data as SourceNodeData const totalWidth = sourceNodes.length * 120 + (sourceNodes.length - 1) * GAP return { id: n.id, label: data.label, flowRate: data.flowRate, x: -totalWidth / 2 + i * (120 + GAP), y: sourceLayerY, width: 120, } }) // ─── Source waterfalls (sankey-proportional inflows) ────── // First pass: collect all inflows per funnel to compute shares const inflowsByFunnel = new Map() sourceNodes.forEach(sn => { const data = sn.data as SourceNodeData data.targetAllocations?.forEach((alloc, i) => { const flowAmount = (alloc.percentage / 100) * data.flowRate if (!inflowsByFunnel.has(alloc.targetId)) inflowsByFunnel.set(alloc.targetId, []) inflowsByFunnel.get(alloc.targetId)!.push({ sourceNodeId: sn.id, allocIndex: i, flowAmount, percentage: alloc.percentage }) }) }) const sourceWaterfalls: WaterfallLayout[] = [] sourceNodes.forEach(sn => { const data = sn.data as SourceNodeData const sourceLayout = sourceLayouts.find(s => s.id === sn.id) if (!sourceLayout) return data.targetAllocations?.forEach((alloc, allocIdx) => { const targetLayout = funnelLayouts.find(f => f.id === alloc.targetId) if (!targetLayout) return const flowAmount = (alloc.percentage / 100) * data.flowRate const allInflowsToTarget = inflowsByFunnel.get(alloc.targetId) || [] const totalInflowToTarget = allInflowsToTarget.reduce((s, i) => s + i.flowAmount, 0) // Sankey width: proportional share of the target river width const share = totalInflowToTarget > 0 ? flowAmount / totalInflowToTarget : 1 const riverEndWidth = Math.max(MIN_WATERFALL_WIDTH, share * targetLayout.riverWidth) // Far end width: proportional share of source box width const farEndWidth = Math.max(MIN_WATERFALL_WIDTH, (alloc.percentage / 100) * sourceLayout.width * 0.8) // Position along river top edge: distribute side by side const myIndex = allInflowsToTarget.findIndex(i => i.sourceNodeId === sn.id && i.allocIndex === allocIdx) const inflowWidths = distributeWidths( allInflowsToTarget.map(i => i.flowAmount), targetLayout.segmentLength * 0.7, MIN_WATERFALL_WIDTH ) const startX = targetLayout.x + targetLayout.segmentLength * 0.15 let offsetX = 0 for (let k = 0; k < myIndex; k++) offsetX += inflowWidths[k] const riverCenterX = startX + offsetX + inflowWidths[myIndex] / 2 // Source center x const sourceCenterX = sourceLayout.x + sourceLayout.width / 2 sourceWaterfalls.push({ id: `src-wf-${sn.id}-${alloc.targetId}`, sourceId: sn.id, targetId: alloc.targetId, label: `${alloc.percentage}%`, percentage: alloc.percentage, x: riverCenterX, xSource: sourceCenterX, yStart: sourceLayout.y + SOURCE_HEIGHT, yEnd: targetLayout.y, width: riverEndWidth, riverEndWidth, farEndWidth, direction: 'inflow', color: COLORS.sourceWaterfall, flowAmount, }) }) }) // Implicit waterfalls for root funnels with inflowRate but no source nodes if (sourceNodes.length === 0) { rootFunnels.forEach(rn => { const data = rn.data as FunnelNodeData if (data.inflowRate <= 0) return const layout = funnelLayouts.find(f => f.id === rn.id) if (!layout) return const riverEndWidth = layout.riverWidth const farEndWidth = Math.max(MIN_WATERFALL_WIDTH, layout.riverWidth * 0.4) sourceWaterfalls.push({ id: `implicit-wf-${rn.id}`, sourceId: 'implicit', targetId: rn.id, label: `$${Math.floor(data.inflowRate)}/mo`, percentage: 100, x: layout.x + layout.segmentLength / 2, xSource: layout.x + layout.segmentLength / 2, yStart: GAP, yEnd: layout.y, width: riverEndWidth, riverEndWidth, farEndWidth, direction: 'inflow', color: COLORS.sourceWaterfall, flowAmount: data.inflowRate, }) }) } // ─── Overflow branches (sankey-proportional) ────── const overflowBranches: BranchLayout[] = [] funnelNodes.forEach(n => { const data = n.data as FunnelNodeData const parentLayout = funnelLayouts.find(f => f.id === n.id) if (!parentLayout) return data.overflowAllocations?.forEach((alloc) => { const childLayout = funnelLayouts.find(f => f.id === alloc.targetId) if (!childLayout) return const width = Math.max(MIN_WATERFALL_WIDTH, (alloc.percentage / 100) * parentLayout.riverWidth) overflowBranches.push({ sourceId: n.id, targetId: alloc.targetId, percentage: alloc.percentage, x1: parentLayout.x + parentLayout.segmentLength, y1: parentLayout.y + parentLayout.riverWidth / 2, x2: childLayout.x, y2: childLayout.y + childLayout.riverWidth / 2, width, color: alloc.color || COLORS.overflowBranch, }) }) }) // ─── Outcome layouts ────── const outcomeY = funnelStartY + (maxLayer + 1) * (LAYER_HEIGHT + WATERFALL_HEIGHT + GAP) + WATERFALL_HEIGHT const totalOutcomeWidth = outcomeNodes.length * (POOL_WIDTH + GAP) - GAP const outcomeLayouts: OutcomeLayout[] = outcomeNodes.map((n, i) => { const data = n.data as OutcomeNodeData const fillPercent = data.fundingTarget > 0 ? Math.min(100, (data.fundingReceived / data.fundingTarget) * 100) : 0 return { id: n.id, label: data.label, data, x: -totalOutcomeWidth / 2 + i * (POOL_WIDTH + GAP), y: outcomeY, poolWidth: POOL_WIDTH, fillPercent, } }) // ─── Spending waterfalls (sankey-proportional outflows) ────── const spendingWaterfalls: WaterfallLayout[] = [] funnelNodes.forEach(n => { const data = n.data as FunnelNodeData const parentLayout = funnelLayouts.find(f => f.id === n.id) if (!parentLayout) return const allocations = data.spendingAllocations || [] if (allocations.length === 0) return // Distribute outflows along the river bottom edge const percentages = allocations.map(a => a.percentage) const slotWidths = distributeWidths( percentages, parentLayout.segmentLength * 0.7, MIN_WATERFALL_WIDTH ) const riverEndWidths = distributeWidths( percentages, parentLayout.riverWidth, MIN_WATERFALL_WIDTH ) const startX = parentLayout.x + parentLayout.segmentLength * 0.15 let offsetX = 0 allocations.forEach((alloc, i) => { const outcomeLayout = outcomeLayouts.find(o => o.id === alloc.targetId) if (!outcomeLayout) return const riverEndWidth = riverEndWidths[i] const farEndWidth = Math.max(MIN_WATERFALL_WIDTH, outcomeLayout.poolWidth * 0.6) const riverCenterX = startX + offsetX + slotWidths[i] / 2 offsetX += slotWidths[i] const poolCenterX = outcomeLayout.x + outcomeLayout.poolWidth / 2 spendingWaterfalls.push({ id: `spend-wf-${n.id}-${alloc.targetId}`, sourceId: n.id, targetId: alloc.targetId, label: `${alloc.percentage}%`, percentage: alloc.percentage, x: riverCenterX, xSource: poolCenterX, yStart: parentLayout.y + parentLayout.riverWidth + 4, yEnd: outcomeLayout.y, width: riverEndWidth, riverEndWidth, farEndWidth, direction: 'outflow', color: alloc.color || COLORS.spendingWaterfall[i % COLORS.spendingWaterfall.length], flowAmount: (alloc.percentage / 100) * (data.inflowRate || 1), }) }) }) // Compute total bounds const allX = [ ...funnelLayouts.map(f => f.x), ...funnelLayouts.map(f => f.x + f.segmentLength), ...outcomeLayouts.map(o => o.x), ...outcomeLayouts.map(o => o.x + o.poolWidth), ...sourceLayouts.map(s => s.x), ...sourceLayouts.map(s => s.x + s.width), ] const allY = [ ...funnelLayouts.map(f => f.y + f.riverWidth), ...outcomeLayouts.map(o => o.y + POOL_HEIGHT), sourceLayerY, ] const minX = Math.min(...allX, -100) const maxX = Math.max(...allX, 100) const maxY = Math.max(...allY, 400) const padding = 80 // Shift everything so minX starts at padding const offsetXGlobal = -minX + padding const offsetYGlobal = padding // Apply offsets funnelLayouts.forEach(f => { f.x += offsetXGlobal; f.y += offsetYGlobal }) outcomeLayouts.forEach(o => { o.x += offsetXGlobal; o.y += offsetYGlobal }) sourceLayouts.forEach(s => { s.x += offsetXGlobal; s.y += offsetYGlobal }) sourceWaterfalls.forEach(w => { w.x += offsetXGlobal; w.xSource += offsetXGlobal; w.yStart += offsetYGlobal; w.yEnd += offsetYGlobal }) overflowBranches.forEach(b => { b.x1 += offsetXGlobal; b.y1 += offsetYGlobal; b.x2 += offsetXGlobal; b.y2 += offsetYGlobal }) spendingWaterfalls.forEach(w => { w.x += offsetXGlobal; w.xSource += offsetXGlobal; w.yStart += offsetYGlobal; w.yEnd += offsetYGlobal }) return { sources: sourceLayouts, funnels: funnelLayouts, outcomes: outcomeLayouts, sourceWaterfalls, overflowBranches, spendingWaterfalls, width: maxX - minX + padding * 2, height: maxY + offsetYGlobal + padding, } } // ─── SVG Sub-components ────────────────────────────────── function SankeyWaterfall({ wf }: { wf: WaterfallLayout }) { const isInflow = wf.direction === 'inflow' const height = wf.yEnd - wf.yStart if (height <= 0) return null // For inflows: narrow at top (source), wide at bottom (river) // For outflows: wide at top (river), narrow at bottom (pool) const topWidth = isInflow ? wf.farEndWidth : wf.riverEndWidth const bottomWidth = isInflow ? wf.riverEndWidth : wf.farEndWidth const topCx = isInflow ? wf.xSource : wf.x const bottomCx = isInflow ? wf.x : wf.xSource // Bezier control points for the taper curve // Inflow: narrow for 55%, then flares in last 45% // Outflow: wide for first 20%, then tapers const cpFrac1 = isInflow ? 0.55 : 0.2 const cpFrac2 = isInflow ? 0.75 : 0.45 const cpY1 = wf.yStart + height * cpFrac1 const cpY2 = wf.yStart + height * cpFrac2 const tl = topCx - topWidth / 2 const tr = topCx + topWidth / 2 const bl = bottomCx - bottomWidth / 2 const br = bottomCx + bottomWidth / 2 const shapePath = [ `M ${tl} ${wf.yStart}`, `C ${tl} ${cpY1}, ${bl} ${cpY2}, ${bl} ${wf.yEnd}`, `L ${br} ${wf.yEnd}`, `C ${br} ${cpY2}, ${tr} ${cpY1}, ${tr} ${wf.yStart}`, `Z` ].join(' ') const clipId = `sankey-clip-${wf.id}` const gradId = `sankey-grad-${wf.id}` // Ripple at the wide (river) end const rippleCx = isInflow ? bottomCx : topCx const rippleCy = isInflow ? wf.yEnd : wf.yStart // Bounding box for animated strips const pathMinX = Math.min(tl, bl) - 5 const pathMaxW = Math.max(topWidth, bottomWidth) + 10 return ( {/* Glow behind shape */} {/* Main filled shape */} {/* Animated water strips inside the shape */} {[0, 1, 2].map(i => ( ))} {/* Edge highlight lines along the bezier edges */} {/* Merge ripples at river junction */} {[0, 1, 2].map(i => ( ))} {/* Droplets at narrow end */} {Array.from({ length: Math.max(2, Math.min(5, Math.floor(wf.riverEndWidth / 8))) }, (_, i) => { const narrowCx = isInflow ? topCx : bottomCx const narrowCy = isInflow ? wf.yStart : wf.yEnd const narrowW = isInflow ? topWidth : bottomWidth const dx = (Math.random() - 0.5) * narrowW return ( ) })} {/* Label */} {wf.label} ) } function RiverSegment({ funnel }: { funnel: FunnelLayout }) { const { id, label, data, x, y, riverWidth, segmentLength, status, sufficiency } = funnel const isSufficientOrAbundant = sufficiency === 'sufficient' || sufficiency === 'abundant' const gradColors = isSufficientOrAbundant ? COLORS.riverSufficient : status === 'overflow' ? COLORS.riverOverflow : status === 'critical' ? COLORS.riverCritical : COLORS.riverHealthy const thresholdMinY = y + riverWidth * 0.85 const thresholdMaxY = y + riverWidth * 0.15 // Sufficiency ring: arc fill from 0 to sufficientThreshold const sufficientThreshold = data.sufficientThreshold ?? data.maxThreshold const sufficiencyFill = Math.min(1, data.currentValue / (sufficientThreshold || 1)) // Status pill text/color const pillText = isSufficientOrAbundant ? 'ENOUGH' : status === 'overflow' ? 'OVER' : status === 'critical' ? 'LOW' : 'OK' const pillColor = isSufficientOrAbundant ? COLORS.goldenGlow : status === 'overflow' ? '#f59e0b' : status === 'critical' ? '#ef4444' : '#10b981' const pillWidth = isSufficientOrAbundant ? 42 : 32 return ( {/* Golden glow for sufficient/abundant funnels */} {isSufficientOrAbundant && ( )} {/* River glow */} {/* Sufficiency ring: thin progress bar above the river */} = 1 ? COLORS.goldenGlow : '#06b6d4'} opacity={0.8} /> {/* River body */} {/* Surface current lines */} {[0.3, 0.5, 0.7].map((pos, i) => ( ))} {/* Shimmer highlight */} {/* Threshold markers */} MAX MIN {/* Dynamic overflow indicator */} {data.dynamicOverflow && ( DYN )} {/* Label */} {label} {/* Value */} ${Math.floor(data.currentValue).toLocaleString()} {/* Status pill */} {pillText} ) } function OverflowBranch({ branch }: { branch: BranchLayout }) { const { x1, y1, x2, y2, width, color, percentage } = branch const midX = (x1 + x2) / 2 const midY = (y1 + y2) / 2 // Curved path connecting two river segments const path = `M ${x1} ${y1} C ${x1 + 60} ${y1}, ${x2 - 60} ${y2}, ${x2} ${y2}` return ( {/* Glow */} {/* Main branch */} {/* Flow direction label */} {percentage}% ) } function OutcomePool({ outcome }: { outcome: OutcomeLayout }) { const { id, label, data, x, y, poolWidth, fillPercent } = outcome const fillHeight = (fillPercent / 100) * POOL_HEIGHT const statusColor = data.status === 'completed' ? '#10b981' : data.status === 'in-progress' ? '#3b82f6' : data.status === 'blocked' ? '#ef4444' : '#64748b' return ( {/* Pool container */} {/* Water fill */} {/* Water surface wave */} {fillPercent > 5 && ( )} {/* Label */} {label} {/* Value */} ${Math.floor(data.fundingReceived).toLocaleString()} {/* Progress */} {Math.round(fillPercent)}% of ${Math.floor(data.fundingTarget).toLocaleString()} ) } function SourceBox({ source }: { source: SourceLayout }) { return ( {source.label} ${source.flowRate.toLocaleString()}/mo ) } // ─── Main Component ────────────────────────────────────── interface BudgetRiverProps { nodes: FlowNode[] isSimulating?: boolean } export default function BudgetRiver({ nodes, isSimulating = false }: BudgetRiverProps) { const [animatedNodes, setAnimatedNodes] = useState(nodes) // Update when parent nodes change useEffect(() => { setAnimatedNodes(nodes) }, [nodes]) // Simulation useEffect(() => { if (!isSimulating) return const interval = setInterval(() => { setAnimatedNodes(prev => prev.map(node => { if (node.type === 'funnel') { const data = node.data as FunnelNodeData const change = (Math.random() - 0.45) * 300 return { ...node, data: { ...data, currentValue: Math.max(0, Math.min(data.maxCapacity * 1.1, data.currentValue + change)), }, } } else if (node.type === 'outcome') { const data = node.data as OutcomeNodeData const change = Math.random() * 80 const newReceived = Math.min(data.fundingTarget * 1.05, data.fundingReceived + change) return { ...node, data: { ...data, fundingReceived: newReceived, status: newReceived >= data.fundingTarget ? 'completed' : data.status === 'not-started' && newReceived > 0 ? 'in-progress' : data.status, }, } } return node }) ) }, 500) return () => clearInterval(interval) }, [isSimulating]) const layout = useMemo(() => computeLayout(animatedNodes), [animatedNodes]) const systemSufficiency = useMemo(() => computeSystemSufficiency(animatedNodes), [animatedNodes]) const sufficiencyPct = Math.round(systemSufficiency * 100) const sufficiencyColor = sufficiencyPct >= 90 ? '#fbbf24' : sufficiencyPct >= 60 ? '#10b981' : sufficiencyPct >= 30 ? '#f59e0b' : '#ef4444' return (
{/* Background */} {/* Background stars/dots for atmosphere */} {Array.from({ length: 30 }, (_, i) => ( ))} {/* Layer 1: Source boxes */} {layout.sources.map(s => ( ))} {/* Layer 2: Source waterfalls (sankey inflows pouring into river) */} {layout.sourceWaterfalls.map(wf => ( ))} {/* Layer 3: River segments */} {layout.funnels.map(f => ( ))} {/* Layer 4: Overflow branches */} {layout.overflowBranches.map((b, i) => ( ))} {/* Layer 5: Spending waterfalls (sankey outflows pouring out) */} {layout.spendingWaterfalls.map(wf => ( ))} {/* Layer 6: Outcome pools */} {layout.outcomes.map(o => ( ))} {/* System Enoughness Badge (top-right) */} {/* Background circle */} {/* Track ring */} {/* Progress ring */} {/* Percentage text */} {sufficiencyPct}% {/* Label */} ENOUGHNESS {/* Title watermark */} rFUNDS BUDGET RIVER
) }