File size: 9,032 Bytes
24e6f5b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
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>
    );

    // Calculate total predicted spend safely
    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;