ff7ba51e84
Backend: PUT /purchases/{id} endpoint accepts updated amount_eur,
price_eur, and created_at. Frontend: inline edit row in PurchaseList
with date picker and number inputs, Save/Cancel actions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
100 lines
3.7 KiB
JavaScript
100 lines
3.7 KiB
JavaScript
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import AddPurchase from '../components/AddPurchase';
|
|
import PurchaseList from '../components/PurchaseList';
|
|
import PortfolioChart from '../components/PortfolioChart';
|
|
|
|
const API = process.env.REACT_APP_API_URL || 'http://localhost:8000';
|
|
|
|
const styles = {
|
|
app: { maxWidth: '900px', margin: '0 auto', padding: '1.5rem' },
|
|
header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' },
|
|
logo: { fontSize: '1.4rem', fontWeight: 700, color: '#f7931a' },
|
|
logoutBtn: { background: 'none', border: '1px solid #555', color: '#aaa', borderRadius: '8px', padding: '0.4rem 1rem', cursor: 'pointer' },
|
|
statsGrid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: '1rem', marginBottom: '1.5rem' },
|
|
statCard: { background: '#1a1a1a', padding: '1rem', borderRadius: '12px', border: '1px solid #333' },
|
|
statLabel: { color: '#888', fontSize: '0.8rem', marginBottom: '0.3rem' },
|
|
statValue: { fontSize: '1.2rem', fontWeight: 700 },
|
|
positive: { color: '#6bff8e' },
|
|
negative: { color: '#ff6b6b' },
|
|
neutral: { color: '#f7931a' },
|
|
};
|
|
|
|
function StatCard({ label, value, highlight }) {
|
|
return (
|
|
<div style={styles.statCard}>
|
|
<div style={styles.statLabel}>{label}</div>
|
|
<div style={{ ...styles.statValue, ...(highlight ? styles[highlight] : {}) }}>{value}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function Dashboard() {
|
|
const [stats, setStats] = useState(null);
|
|
const [purchases, setPurchases] = useState([]);
|
|
const navigate = useNavigate();
|
|
|
|
const authHeaders = () => ({
|
|
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
|
});
|
|
|
|
const fetchData = useCallback(async () => {
|
|
try {
|
|
const [statsRes, purchasesRes] = await Promise.all([
|
|
fetch(`${API}/stats`, { headers: authHeaders() }),
|
|
fetch(`${API}/purchases`, { headers: authHeaders() }),
|
|
]);
|
|
if (statsRes.status === 401) {
|
|
localStorage.removeItem('token');
|
|
navigate('/login');
|
|
return;
|
|
}
|
|
setStats(await statsRes.json());
|
|
setPurchases(await purchasesRes.json());
|
|
} catch {
|
|
// silently fail — network may be unavailable
|
|
}
|
|
}, [navigate]);
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, [fetchData]);
|
|
|
|
const handleLogout = () => {
|
|
localStorage.removeItem('token');
|
|
navigate('/login');
|
|
};
|
|
|
|
const plHighlight = stats
|
|
? stats.profit_loss >= 0 ? 'positive' : 'negative'
|
|
: 'neutral';
|
|
|
|
return (
|
|
<div style={styles.app}>
|
|
<div style={styles.header}>
|
|
<div style={styles.logo}>₿ BTC Portfolio</div>
|
|
<button style={styles.logoutBtn} onClick={handleLogout}>Logout</button>
|
|
</div>
|
|
|
|
{stats && (
|
|
<div style={styles.statsGrid}>
|
|
<StatCard label="Total Invested" value={`€${stats.total_invested.toLocaleString()}`} />
|
|
<StatCard label="Total BTC" value={`₿${stats.total_btc}`} highlight="neutral" />
|
|
<StatCard label="Avg Buy Price" value={`€${stats.average_price.toLocaleString()}`} />
|
|
<StatCard label="Current BTC Price" value={`€${stats.current_price.toLocaleString()}`} />
|
|
<StatCard label="Portfolio Value" value={`€${stats.portfolio_value.toLocaleString()}`} />
|
|
<StatCard
|
|
label="Profit / Loss"
|
|
value={`${stats.profit_loss >= 0 ? '+' : ''}€${stats.profit_loss.toLocaleString()}`}
|
|
highlight={plHighlight}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<PortfolioChart purchases={purchases} stats={stats} />
|
|
<AddPurchase onAdded={fetchData} />
|
|
<PurchaseList purchases={purchases} onChanged={fetchData} />
|
|
</div>
|
|
);
|
|
}
|