'use client' import { useState, useEffect, useRef, useCallback } from 'react' interface FundingFunnelProps { name: string currentBalance: number minThreshold: number maxThreshold: number inflowRate: number // per hour outflowRate: number // per hour maxCapacity?: number onMinThresholdChange?: (value: number) => void onMaxThresholdChange?: (value: number) => void } export default function FundingFunnel({ name, currentBalance, minThreshold: initialMin, maxThreshold: initialMax, inflowRate, outflowRate, maxCapacity = 100000, onMinThresholdChange, onMaxThresholdChange, }: FundingFunnelProps) { const [minThreshold, setMinThreshold] = useState(initialMin) const [maxThreshold, setMaxThreshold] = useState(initialMax) const [balance, setBalance] = useState(currentBalance) const [isDraggingMin, setIsDraggingMin] = useState(false) const [isDraggingMax, setIsDraggingMax] = useState(false) const containerRef = useRef(null) // Funnel dimensions const width = 300 const height = 500 const funnelTopWidth = 280 const funnelNarrowWidth = 80 const padding = 10 // Calculate Y positions for thresholds (inverted - 0 is at bottom) const minY = height - (minThreshold / maxCapacity) * height const maxY = height - (maxThreshold / maxCapacity) * height const balanceY = height - (balance / maxCapacity) * height // Funnel shape points const funnelPath = ` M ${padding} ${maxY} L ${padding} ${minY} L ${(width - funnelNarrowWidth) / 2} ${height - padding} L ${(width + funnelNarrowWidth) / 2} ${height - padding} L ${width - padding} ${minY} L ${width - padding} ${maxY} L ${padding} ${maxY} ` // Overflow zone (above max) const overflowPath = ` M ${padding} ${padding} L ${padding} ${maxY} L ${width - padding} ${maxY} L ${width - padding} ${padding} Z ` // Calculate fill path based on current balance const getFillPath = () => { if (balance <= 0) return '' const fillY = Math.max(balanceY, padding) if (balance >= maxThreshold) { // Overflow zone - straight walls above max const overflowY = Math.max(fillY, padding) return ` M ${padding} ${minY} L ${(width - funnelNarrowWidth) / 2} ${height - padding} L ${(width + funnelNarrowWidth) / 2} ${height - padding} L ${width - padding} ${minY} L ${width - padding} ${overflowY} L ${padding} ${overflowY} Z ` } else if (balance >= minThreshold) { // Between min and max - straight walls return ` M ${padding} ${minY} L ${(width - funnelNarrowWidth) / 2} ${height - padding} L ${(width + funnelNarrowWidth) / 2} ${height - padding} L ${width - padding} ${minY} L ${width - padding} ${fillY} L ${padding} ${fillY} Z ` } else { // Below min - in the funnel narrowing section const ratio = balance / minThreshold const bottomWidth = funnelNarrowWidth const topWidth = funnelTopWidth - 2 * padding const currentWidth = bottomWidth + (topWidth - bottomWidth) * ratio const leftX = (width - currentWidth) / 2 const rightX = (width + currentWidth) / 2 return ` M ${(width - funnelNarrowWidth) / 2} ${height - padding} L ${(width + funnelNarrowWidth) / 2} ${height - padding} L ${rightX} ${fillY} L ${leftX} ${fillY} Z ` } } // Simulate balance changes useEffect(() => { const interval = setInterval(() => { setBalance((prev) => { const netFlow = (inflowRate - outflowRate) / 3600 // per second const newBalance = prev + netFlow return Math.max(0, Math.min(maxCapacity * 1.2, newBalance)) }) }, 100) return () => clearInterval(interval) }, [inflowRate, outflowRate, maxCapacity]) // Handle threshold dragging const handleMouseMove = useCallback( (e: MouseEvent) => { if (!containerRef.current) return const rect = containerRef.current.getBoundingClientRect() const y = e.clientY - rect.top const value = Math.max(0, Math.min(maxCapacity, ((height - y) / height) * maxCapacity)) if (isDraggingMin) { const newMin = Math.min(value, maxThreshold - 1000) setMinThreshold(Math.max(0, newMin)) onMinThresholdChange?.(Math.max(0, newMin)) } else if (isDraggingMax) { const newMax = Math.max(value, minThreshold + 1000) setMaxThreshold(Math.min(maxCapacity, newMax)) onMaxThresholdChange?.(Math.min(maxCapacity, newMax)) } }, [isDraggingMin, isDraggingMax, maxThreshold, minThreshold, maxCapacity, onMinThresholdChange, onMaxThresholdChange] ) const handleMouseUp = useCallback(() => { setIsDraggingMin(false) setIsDraggingMax(false) }, []) useEffect(() => { if (isDraggingMin || isDraggingMax) { window.addEventListener('mousemove', handleMouseMove) window.addEventListener('mouseup', handleMouseUp) return () => { window.removeEventListener('mousemove', handleMouseMove) window.removeEventListener('mouseup', handleMouseUp) } } }, [isDraggingMin, isDraggingMax, handleMouseMove, handleMouseUp]) // Determine zone status const getZoneStatus = () => { if (balance < minThreshold) return { zone: 'critical', color: '#F43F5E', label: 'Below Minimum' } if (balance > maxThreshold) return { zone: 'overflow', color: '#F59E0B', label: 'Overflow' } return { zone: 'healthy', color: '#10B981', label: 'Healthy Range' } } const status = getZoneStatus() return (

{name}

{/* Main Funnel Visualization */}
{/* Gradient for the fill */} {/* Glow filter */} {/* Wave pattern for liquid effect */} {/* Background zones */} {/* Overflow zone (above max) */} {/* Healthy zone (between min and max) - straight walls */} {/* Funnel zone (below min) */} {/* Funnel outline */} {/* Fill (current balance) */} {/* Animated inflow particles */} {inflowRate > 0 && ( <> {[...Array(5)].map((_, i) => ( ))} )} {/* Animated outflow particles */} {outflowRate > 0 && balance > 0 && ( <> {[...Array(3)].map((_, i) => ( ))} )} {/* Max threshold line (draggable) */} setIsDraggingMax(true)} > MAX ${maxThreshold.toLocaleString()} {/* Min threshold line (draggable) */} setIsDraggingMin(true)} > MIN ${minThreshold.toLocaleString()} {/* Current balance indicator */} {/* Zone labels */}
OVERFLOW
HEALTHY
CRITICAL
{/* Stats Panel */}
{/* Current Balance */}
Current Balance
${balance.toLocaleString(undefined, { maximumFractionDigits: 0 })}
{status.label}
{/* Flow Rates */}
Flow Rates
Inflow
+${inflowRate}/hr
Outflow
-${outflowRate}/hr
Net Flow = 0 ? 'text-emerald-400' : 'text-rose-400' }`} > {inflowRate - outflowRate >= 0 ? '+' : ''}${inflowRate - outflowRate}/hr
{/* Thresholds */}
Thresholds
Minimum ${minThreshold.toLocaleString()}
Drag the red line to adjust
Maximum ${maxThreshold.toLocaleString()}
Drag the yellow line to adjust
{/* Progress to thresholds */}
Progress
To Minimum {Math.min(100, (balance / minThreshold) * 100).toFixed(0)}%
To Maximum {Math.min(100, (balance / maxThreshold) * 100).toFixed(0)}%
) }