| import { useState, useEffect } from 'react'; |
| import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; |
| import { TrendingUp, Info } from 'lucide-react'; |
| import api from '../api/axios'; |
| import { useSettings } from '../context/SettingsContext'; |
|
|
| const ForecastChart = () => { |
| const { currencySymbol } = useSettings(); |
| const [data, setData] = useState([]); |
| const [loading, setLoading] = useState(true); |
| const [error, setError] = useState(null); |
|
|
| useEffect(() => { |
| const fetchForecast = async () => { |
| try { |
| const response = await api.get('analytics/forecast/'); |
| if (Array.isArray(response.data)) { |
| setData(response.data); |
| } else { |
| throw new Error("Invalid format"); |
| } |
| setLoading(false); |
| } catch (err) { |
| console.error("Forecast Error:", err); |
| const errMsg = err.response?.data?.error || "AI Engine Initializing..."; |
| setError(errMsg); |
| setLoading(false); |
| } |
| }; |
| fetchForecast(); |
| }, []); |
|
|
| if (loading) return ( |
| <div className="glass-panel" style={{ height: '300px', display: 'flex', justifyContent: 'center', alignItems: 'center' }}> |
| <div className="spinner"></div> |
| </div> |
| ); |
|
|
| if (error || !data.length) return ( |
| <div className="glass-panel" style={{ height: '300px', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', color: 'var(--text-muted)' }}> |
| <Info size={32} style={{ marginBottom: '1rem', opacity: 0.5 }} /> |
| <p>{error || "No forecast data available yet."}</p> |
| </div> |
| ); |
|
|
| |
| const totalPredicted = Array.isArray(data) ? data.reduce((acc, curr) => acc + (curr.amount || 0), 0) : 0; |
|
|
| return ( |
| <div className="glass-panel" style={{ |
| padding: '1.5rem', |
| position: 'relative', |
| overflow: 'hidden', |
| border: '1px solid rgba(99, 102, 241, 0.3)', |
| boxShadow: '0 0 20px rgba(99, 102, 241, 0.1)' |
| }}> |
| <div style={{ |
| position: 'absolute', top: 0, left: 0, right: 0, height: '2px', |
| background: 'linear-gradient(90deg, transparent, #6366f1, transparent)' |
| }} /> |
| |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '1.5rem' }}> |
| <div> |
| <h3 style={{ margin: 0, fontSize: '1.1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}> |
| <span style={{ fontSize: '1.2rem' }}>🔮</span> Smart Forecast (Chronos Bolt) |
| </h3> |
| <p style={{ fontSize: '0.8rem', color: 'var(--text-muted)', margin: '0.25rem 0 0 0' }}> |
| AI-predicted spending for next 30 days |
| </p> |
| </div> |
| <div style={{ textAlign: 'right' }}> |
| <div style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Projected Total</div> |
| <div style={{ fontSize: '1.4rem', fontWeight: 'bold', color: '#818cf8', display: 'flex', alignItems: 'center', gap: '0.5rem', justifyContent: 'flex-end' }}> |
| {currencySymbol}{totalPredicted.toLocaleString(undefined, { maximumFractionDigits: 0 })} |
| <TrendingUp size={16} /> |
| </div> |
| </div> |
| </div> |
| |
| <div style={{ height: '400px', width: '100%' }}> |
| <ResponsiveContainer width="100%" height="100%"> |
| <AreaChart data={data}> |
| <defs> |
| <linearGradient id="colorHigh" x1="0" y1="0" x2="0" y2="1"> |
| <stop offset="5%" stopColor="#6366f1" stopOpacity={0.1} /> |
| <stop offset="95%" stopColor="#6366f1" stopOpacity={0} /> |
| </linearGradient> |
| </defs> |
| <CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" vertical={false} /> |
| <XAxis |
| dataKey="date" |
| stroke="var(--text-muted)" |
| fontSize={10} |
| tickFormatter={(str) => { |
| const d = new Date(str); |
| return `${d.getDate()}/${d.getMonth() + 1}`; |
| }} |
| interval={4} |
| /> |
| <YAxis stroke="var(--text-muted)" fontSize={10} tickFormatter={(val) => `${currencySymbol}${val}`} /> |
| <Tooltip |
| contentStyle={{ |
| backgroundColor: 'rgba(15, 23, 42, 0.9)', |
| border: '1px solid rgba(99, 102, 241, 0.3)', |
| borderRadius: '0.5rem', |
| boxShadow: '0 4px 20px rgba(0,0,0,0.5)' |
| }} |
| itemStyle={{ color: '#e2e8f0' }} |
| formatter={(value, name) => { |
| if (name === 'high') return [`${currencySymbol}${value}`, 'Upper Bound (P90)']; |
| if (name === 'amount') return [`${currencySymbol}${value}`, 'Wait Forecast (P50)']; |
| if (name === 'low') return [`${currencySymbol}${value}`, 'Lower Bound (P10)']; |
| return [value, name]; |
| }} |
| labelStyle={{ color: '#818cf8', fontWeight: 'bold', marginBottom: '0.5rem' }} |
| /> |
| {/* Confidence Interval Area */} |
| {/* We stack to create the band? No, simple Area is easier. */} |
| {/* Actually, representing Low/High as area requires specific processing or multiple areas. |
| Simplest visual: Area for Amount, and Lines for Low/High, |
| OR a stacked approach: Low (invisible), (High-Low) as range. |
| Let's try a transparent range. |
| */} |
| <Area |
| type="monotone" |
| dataKey="high" |
| stroke="none" |
| fill="#6366f1" |
| fillOpacity={0.1} |
| /> |
| {/* We need to mask the bottom part if we want a band, but standard Area is 0-to-val. |
| Recharts Area 'baseValue' isn't dynamic. |
| Better visual: Just show the main line and a faint area for 'high' to give an impression of ceiling. |
| Or use `Area` with `dataKey="amount"` as the main visual. |
| */} |
| <Area |
| type="monotone" |
| dataKey="amount" |
| stroke="#818cf8" |
| strokeWidth={3} |
| fill="url(#colorHigh)" |
| activeDot={{ r: 6, strokeWidth: 0 }} |
| /> |
| <Area |
| type="monotone" |
| dataKey="low" |
| stroke="#4f46e5" |
| strokeDasharray="3 3" |
| fill="none" |
| strokeOpacity={0.5} |
| /> |
| <Area |
| type="monotone" |
| dataKey="high" |
| stroke="#4f46e5" |
| strokeDasharray="3 3" |
| fill="none" |
| strokeOpacity={0.5} |
| /> |
| </AreaChart> |
| </ResponsiveContainer> |
| </div> |
| |
| <div style={{ display: 'flex', gap: '1rem', justifyContent: 'center', marginTop: '1rem', fontSize: '0.75rem', color: 'var(--text-muted)' }}> |
| <div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}> |
| <div style={{ width: '8px', height: '8px', borderRadius: '50%', background: '#818cf8' }} /> |
| Predicted |
| </div> |
| <div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}> |
| <div style={{ width: '8px', height: '2px', background: '#4f46e5', borderTop: '1px dashed transparent' }} /> |
| Confidence Range |
| </div> |
| </div> |
| </div> |
| ); |
| }; |
|
|
| export default ForecastChart; |
|
|