Add purchase date picker and sells feature
- Purchase form now includes a date picker (defaults to today) - New Sell model, CRUD endpoints (/sells), and stats integration - AddSell and SellList components added to dashboard - Portfolio chart updated to reflect sells over time Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from sqlalchemy import text
|
||||
|
||||
from .database import engine, Base, SessionLocal
|
||||
from .routes import users, purchases, stats, history, admin, candles
|
||||
from .routes import users, purchases, stats, history, admin, candles, sells
|
||||
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
@@ -23,6 +23,7 @@ app.include_router(stats.router)
|
||||
app.include_router(history.router)
|
||||
app.include_router(admin.router, prefix="/admin")
|
||||
app.include_router(candles.router)
|
||||
app.include_router(sells.router)
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
|
||||
@@ -13,6 +13,7 @@ class User(Base):
|
||||
is_admin = Column(Boolean, default=False, nullable=False, server_default='0')
|
||||
|
||||
purchases = relationship("Purchase", back_populates="owner", cascade="all, delete")
|
||||
sells = relationship("Sell", back_populates="owner", cascade="all, delete")
|
||||
|
||||
|
||||
class Purchase(Base):
|
||||
@@ -27,6 +28,18 @@ class Purchase(Base):
|
||||
owner = relationship("User", back_populates="purchases")
|
||||
|
||||
|
||||
class Sell(Base):
|
||||
__tablename__ = "sells"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
btc_amount = Column(Float, nullable=False)
|
||||
price_eur = Column(Float, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
|
||||
owner = relationship("User", back_populates="sells")
|
||||
|
||||
|
||||
class OHLCCandle(Base):
|
||||
__tablename__ = "ohlc_candles"
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from ..database import get_db
|
||||
@@ -14,6 +14,7 @@ router = APIRouter()
|
||||
class PurchaseCreate(BaseModel):
|
||||
amount_eur: float = Field(gt=0, le=10_000_000)
|
||||
price_eur: float = Field(gt=0, le=10_000_000)
|
||||
created_at: Optional[datetime] = None
|
||||
|
||||
|
||||
class PurchaseUpdate(BaseModel):
|
||||
@@ -54,6 +55,7 @@ def add_purchase(
|
||||
purchase = models.Purchase(
|
||||
amount_eur=purchase_in.amount_eur,
|
||||
price_eur=purchase_in.price_eur,
|
||||
created_at=purchase_in.created_at or datetime.utcnow(),
|
||||
user_id=current_user.id,
|
||||
)
|
||||
db.add(purchase)
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from ..database import get_db
|
||||
from .. import models
|
||||
from ..dependencies import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class SellCreate(BaseModel):
|
||||
btc_amount: float = Field(gt=0, le=21_000_000)
|
||||
price_eur: float = Field(gt=0, le=10_000_000)
|
||||
created_at: Optional[datetime] = None
|
||||
|
||||
|
||||
class SellUpdate(BaseModel):
|
||||
btc_amount: float = Field(gt=0, le=21_000_000)
|
||||
price_eur: float = Field(gt=0, le=10_000_000)
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class SellOut(BaseModel):
|
||||
id: int
|
||||
btc_amount: float
|
||||
price_eur: float
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@router.get("/sells", response_model=List[SellOut])
|
||||
def list_sells(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user),
|
||||
):
|
||||
return (
|
||||
db.query(models.Sell)
|
||||
.filter(models.Sell.user_id == current_user.id)
|
||||
.order_by(models.Sell.created_at)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
@router.post("/sells", response_model=SellOut, status_code=status.HTTP_201_CREATED)
|
||||
def add_sell(
|
||||
sell_in: SellCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user),
|
||||
):
|
||||
sell = models.Sell(
|
||||
btc_amount=sell_in.btc_amount,
|
||||
price_eur=sell_in.price_eur,
|
||||
created_at=sell_in.created_at or datetime.utcnow(),
|
||||
user_id=current_user.id,
|
||||
)
|
||||
db.add(sell)
|
||||
db.commit()
|
||||
db.refresh(sell)
|
||||
return sell
|
||||
|
||||
|
||||
@router.put("/sells/{sell_id}", response_model=SellOut)
|
||||
def update_sell(
|
||||
sell_id: int,
|
||||
sell_in: SellUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user),
|
||||
):
|
||||
sell = db.query(models.Sell).filter(
|
||||
models.Sell.id == sell_id,
|
||||
models.Sell.user_id == current_user.id,
|
||||
).first()
|
||||
if not sell:
|
||||
raise HTTPException(status_code=404, detail="Sell not found")
|
||||
sell.btc_amount = sell_in.btc_amount
|
||||
sell.price_eur = sell_in.price_eur
|
||||
sell.created_at = sell_in.created_at
|
||||
db.commit()
|
||||
db.refresh(sell)
|
||||
return sell
|
||||
|
||||
|
||||
@router.delete("/sells/{sell_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_sell(
|
||||
sell_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user),
|
||||
):
|
||||
sell = db.query(models.Sell).filter(
|
||||
models.Sell.id == sell_id,
|
||||
models.Sell.user_id == current_user.id,
|
||||
).first()
|
||||
if not sell:
|
||||
raise HTTPException(status_code=404, detail="Sell not found")
|
||||
db.delete(sell)
|
||||
db.commit()
|
||||
@@ -15,17 +15,24 @@ def get_stats(
|
||||
current_user: models.User = Depends(get_current_user),
|
||||
):
|
||||
purchases = db.query(models.Purchase).filter(models.Purchase.user_id == current_user.id).all()
|
||||
sells = db.query(models.Sell).filter(models.Sell.user_id == current_user.id).all()
|
||||
|
||||
total_invested = sum(p.amount_eur for p in purchases)
|
||||
total_btc = sum(p.amount_eur / p.price_eur for p in purchases) if purchases else 0.0
|
||||
average_price = total_invested / total_btc if total_btc > 0 else 0.0
|
||||
total_btc_bought = sum(p.amount_eur / p.price_eur for p in purchases) if purchases else 0.0
|
||||
|
||||
total_btc_sold = sum(s.btc_amount for s in sells)
|
||||
proceeds_eur = sum(s.btc_amount * s.price_eur for s in sells)
|
||||
|
||||
net_btc = total_btc_bought - total_btc_sold
|
||||
net_invested = total_invested - proceeds_eur
|
||||
average_price = net_invested / net_btc if net_btc > 0 else 0.0
|
||||
current_price = get_btc_price_eur()
|
||||
portfolio_value = total_btc * current_price
|
||||
profit_loss = portfolio_value - total_invested
|
||||
portfolio_value = net_btc * current_price
|
||||
profit_loss = portfolio_value - net_invested
|
||||
|
||||
return {
|
||||
"total_invested": round(total_invested, 2),
|
||||
"total_btc": round(total_btc, 8),
|
||||
"total_invested": round(net_invested, 2),
|
||||
"total_btc": round(net_btc, 8),
|
||||
"average_price": round(average_price, 2),
|
||||
"current_price": round(current_price, 2),
|
||||
"portfolio_value": round(portfolio_value, 2),
|
||||
|
||||
@@ -12,6 +12,7 @@ const styles = {
|
||||
};
|
||||
|
||||
export default function AddPurchase({ onAdded }) {
|
||||
const [purchaseDate, setPurchaseDate] = useState(new Date().toISOString().split('T')[0]);
|
||||
const [amountEur, setAmountEur] = useState('');
|
||||
const [priceEur, setPriceEur] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
@@ -30,12 +31,14 @@ export default function AddPurchase({ onAdded }) {
|
||||
body: JSON.stringify({
|
||||
amount_eur: parseFloat(amountEur),
|
||||
price_eur: parseFloat(priceEur),
|
||||
created_at: new Date(purchaseDate + 'T12:00:00').toISOString(),
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
setError('Failed to add purchase');
|
||||
return;
|
||||
}
|
||||
setPurchaseDate(new Date().toISOString().split('T')[0]);
|
||||
setAmountEur('');
|
||||
setPriceEur('');
|
||||
onAdded();
|
||||
@@ -50,6 +53,13 @@ export default function AddPurchase({ onAdded }) {
|
||||
<div style={styles.title}>Add Purchase</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div style={styles.row}>
|
||||
<input
|
||||
style={styles.input}
|
||||
type="date"
|
||||
value={purchaseDate}
|
||||
onChange={e => setPurchaseDate(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
style={styles.input}
|
||||
type="number"
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const API = process.env.REACT_APP_API_URL || 'http://localhost:8000';
|
||||
|
||||
const styles = {
|
||||
card: { background: '#1a1a1a', padding: '1.5rem', borderRadius: '12px', border: '1px solid #333', marginBottom: '1.5rem' },
|
||||
title: { fontSize: '1.1rem', fontWeight: 700, marginBottom: '1rem', color: '#f7931a' },
|
||||
row: { display: 'flex', gap: '0.75rem', flexWrap: 'wrap' },
|
||||
input: { flex: 1, minWidth: '140px', padding: '0.65rem', background: '#2a2a2a', border: '1px solid #444', borderRadius: '8px', color: '#e0e0e0', fontSize: '1rem' },
|
||||
button: { padding: '0.65rem 1.5rem', background: '#f7931a', color: '#000', border: 'none', borderRadius: '8px', fontWeight: 700, cursor: 'pointer' },
|
||||
error: { color: '#ff6b6b', marginTop: '0.5rem', fontSize: '0.9rem' },
|
||||
};
|
||||
|
||||
export default function AddSell({ onAdded }) {
|
||||
const [sellDate, setSellDate] = useState(new Date().toISOString().split('T')[0]);
|
||||
const [btcAmount, setBtcAmount] = useState('');
|
||||
const [priceEur, setPriceEur] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
const token = localStorage.getItem('token');
|
||||
try {
|
||||
const res = await fetch(`${API}/sells`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
btc_amount: parseFloat(btcAmount),
|
||||
price_eur: parseFloat(priceEur),
|
||||
created_at: new Date(sellDate + 'T12:00:00').toISOString(),
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
setError('Failed to add sell');
|
||||
return;
|
||||
}
|
||||
setSellDate(new Date().toISOString().split('T')[0]);
|
||||
setBtcAmount('');
|
||||
setPriceEur('');
|
||||
onAdded();
|
||||
} catch (err) {
|
||||
console.error('AddSell network error:', err);
|
||||
setError('Network error');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.card}>
|
||||
<div style={styles.title}>Add Sell</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div style={styles.row}>
|
||||
<input
|
||||
style={styles.input}
|
||||
type="date"
|
||||
value={sellDate}
|
||||
onChange={e => setSellDate(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
style={styles.input}
|
||||
type="number"
|
||||
step="any"
|
||||
placeholder="BTC Amount"
|
||||
value={btcAmount}
|
||||
onChange={e => setBtcAmount(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
style={styles.input}
|
||||
type="number"
|
||||
step="any"
|
||||
placeholder="BTC Price (EUR)"
|
||||
value={priceEur}
|
||||
onChange={e => setPriceEur(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<button style={styles.button} type="submit">Add</button>
|
||||
</div>
|
||||
{error && <div style={styles.error}>{error}</div>}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -42,12 +42,13 @@ function priceOn(date, priceMap, currentPrice, isToday, sortedPurchases) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export default function PortfolioChart({ purchases, stats, btcHistory }) {
|
||||
export default function PortfolioChart({ purchases, sells, stats, btcHistory }) {
|
||||
const chartRef = useRef(null);
|
||||
|
||||
if (!purchases || purchases.length === 0) return null;
|
||||
|
||||
const sorted = [...purchases].sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
||||
const sortedSells = [...(sells || [])].sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
@@ -89,6 +90,14 @@ export default function PortfolioChart({ purchases, stats, btcHistory }) {
|
||||
cumInvested += p.amount_eur;
|
||||
}
|
||||
});
|
||||
sortedSells.forEach(s => {
|
||||
const sDate = new Date(s.created_at);
|
||||
sDate.setHours(0, 0, 0, 0);
|
||||
if (sDate <= date) {
|
||||
cumBtc -= s.btc_amount;
|
||||
cumInvested -= s.btc_amount * s.price_eur;
|
||||
}
|
||||
});
|
||||
|
||||
if (cumBtc === 0) return; // no purchases yet at this date
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const API = process.env.REACT_APP_API_URL || 'http://localhost:8000';
|
||||
|
||||
const styles = {
|
||||
card: { background: '#1a1a1a', padding: '1.5rem', borderRadius: '12px', border: '1px solid #333', marginBottom: '1.5rem' },
|
||||
title: { fontSize: '1.1rem', fontWeight: 700, marginBottom: '1rem', color: '#f7931a' },
|
||||
table: { width: '100%', borderCollapse: 'collapse' },
|
||||
th: { textAlign: 'left', padding: '0.5rem 0.75rem', borderBottom: '1px solid #333', color: '#888', fontSize: '0.85rem' },
|
||||
td: { padding: '0.6rem 0.75rem', borderBottom: '1px solid #222', fontSize: '0.95rem' },
|
||||
input: { background: '#2a2a2a', border: '1px solid #555', borderRadius: '6px', color: '#e0e0e0', padding: '0.3rem 0.5rem', fontSize: '0.9rem', width: '100%' },
|
||||
editBtn: { background: 'none', border: '1px solid #555', color: '#4fc3f7', borderRadius: '6px', padding: '0.25rem 0.6rem', cursor: 'pointer', fontSize: '0.85rem', marginRight: '0.4rem' },
|
||||
saveBtn: { background: 'none', border: '1px solid #6bff8e', color: '#6bff8e', borderRadius: '6px', padding: '0.25rem 0.6rem', cursor: 'pointer', fontSize: '0.85rem', marginRight: '0.4rem' },
|
||||
cancelBtn: { background: 'none', border: '1px solid #555', color: '#aaa', borderRadius: '6px', padding: '0.25rem 0.6rem', cursor: 'pointer', fontSize: '0.85rem', marginRight: '0.4rem' },
|
||||
deleteBtn: { background: 'none', border: '1px solid #555', color: '#ff6b6b', borderRadius: '6px', padding: '0.25rem 0.6rem', cursor: 'pointer', fontSize: '0.85rem' },
|
||||
empty: { color: '#555', textAlign: 'center', padding: '1rem' },
|
||||
};
|
||||
|
||||
function toDateInputValue(isoString) {
|
||||
return isoString ? isoString.slice(0, 10) : '';
|
||||
}
|
||||
|
||||
export default function SellList({ sells, onChanged }) {
|
||||
const [editingId, setEditingId] = useState(null);
|
||||
const [editForm, setEditForm] = useState({});
|
||||
|
||||
const token = () => localStorage.getItem('token');
|
||||
|
||||
const startEdit = (s) => {
|
||||
setEditingId(s.id);
|
||||
setEditForm({
|
||||
btc_amount: s.btc_amount,
|
||||
price_eur: s.price_eur,
|
||||
created_at: toDateInputValue(s.created_at),
|
||||
});
|
||||
};
|
||||
|
||||
const cancelEdit = () => setEditingId(null);
|
||||
|
||||
const handleSave = async (id) => {
|
||||
const res = await fetch(`${API}/sells/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token()}` },
|
||||
body: JSON.stringify({
|
||||
btc_amount: parseFloat(editForm.btc_amount),
|
||||
price_eur: parseFloat(editForm.price_eur),
|
||||
created_at: new Date(editForm.created_at + 'T12:00:00').toISOString(),
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
setEditingId(null);
|
||||
onChanged();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
await fetch(`${API}/sells/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${token()}` },
|
||||
});
|
||||
onChanged();
|
||||
};
|
||||
|
||||
const set = (field) => (e) => setEditForm(f => ({ ...f, [field]: e.target.value }));
|
||||
|
||||
return (
|
||||
<div style={styles.card}>
|
||||
<div style={styles.title}>Sells</div>
|
||||
{sells.length === 0 ? (
|
||||
<div style={styles.empty}>No sells yet.</div>
|
||||
) : (
|
||||
<table style={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={styles.th}>Date</th>
|
||||
<th style={styles.th}>BTC Amount</th>
|
||||
<th style={styles.th}>Price (€/BTC)</th>
|
||||
<th style={styles.th}>Value (€)</th>
|
||||
<th style={styles.th}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sells.map(s => editingId === s.id ? (
|
||||
<tr key={s.id}>
|
||||
<td style={styles.td}>
|
||||
<input style={styles.input} type="date" value={editForm.created_at} onChange={set('created_at')} />
|
||||
</td>
|
||||
<td style={styles.td}>
|
||||
<input style={styles.input} type="number" step="any" value={editForm.btc_amount} onChange={set('btc_amount')} />
|
||||
</td>
|
||||
<td style={styles.td}>
|
||||
<input style={styles.input} type="number" step="any" value={editForm.price_eur} onChange={set('price_eur')} />
|
||||
</td>
|
||||
<td style={styles.td}>
|
||||
€{(parseFloat(editForm.btc_amount) * parseFloat(editForm.price_eur) || 0).toLocaleString()}
|
||||
</td>
|
||||
<td style={styles.td}>
|
||||
<button style={styles.saveBtn} onClick={() => handleSave(s.id)}>Save</button>
|
||||
<button style={styles.cancelBtn} onClick={cancelEdit}>Cancel</button>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
<tr key={s.id}>
|
||||
<td style={styles.td}>{new Date(s.created_at).toLocaleDateString('en-GB')}</td>
|
||||
<td style={styles.td}>₿{s.btc_amount.toFixed(8)}</td>
|
||||
<td style={styles.td}>€{s.price_eur.toLocaleString()}</td>
|
||||
<td style={styles.td}>€{(s.btc_amount * s.price_eur).toLocaleString()}</td>
|
||||
<td style={styles.td}>
|
||||
<button style={styles.editBtn} onClick={() => startEdit(s)}>Edit</button>
|
||||
<button style={styles.deleteBtn} onClick={() => handleDelete(s.id)}>Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import AddPurchase from '../components/AddPurchase';
|
||||
import PurchaseList from '../components/PurchaseList';
|
||||
import AddSell from '../components/AddSell';
|
||||
import SellList from '../components/SellList';
|
||||
import PortfolioChart from '../components/PortfolioChart';
|
||||
import BTCCandlestickChart from '../components/BTCCandlestickChart';
|
||||
|
||||
@@ -38,6 +40,7 @@ function StatCard({ label, value, highlight }) {
|
||||
export default function Dashboard() {
|
||||
const [stats, setStats] = useState(null);
|
||||
const [purchases, setPurchases] = useState([]);
|
||||
const [sells, setSells] = useState([]);
|
||||
const [candles, setCandles] = useState(null);
|
||||
const [candlesAll, setCandlesAll] = useState(null);
|
||||
const [fullscreenChart, setFullscreenChart] = useState(false);
|
||||
@@ -50,9 +53,10 @@ export default function Dashboard() {
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const [statsRes, purchasesRes, candlesRes] = await Promise.all([
|
||||
const [statsRes, purchasesRes, sellsRes, candlesRes] = await Promise.all([
|
||||
fetch(`${API}/stats`, { headers: authHeaders() }),
|
||||
fetch(`${API}/purchases`, { headers: authHeaders() }),
|
||||
fetch(`${API}/sells`, { headers: authHeaders() }),
|
||||
fetch(`${API}/candles?days=365`, { headers: authHeaders() }),
|
||||
]);
|
||||
if (statsRes.status === 401) {
|
||||
@@ -62,6 +66,7 @@ export default function Dashboard() {
|
||||
}
|
||||
setStats(await statsRes.json());
|
||||
setPurchases(await purchasesRes.json());
|
||||
setSells(await sellsRes.json());
|
||||
setCandles(await candlesRes.json());
|
||||
} catch {
|
||||
// silently fail — network may be unavailable
|
||||
@@ -135,7 +140,7 @@ export default function Dashboard() {
|
||||
>{label}</button>
|
||||
))}
|
||||
</div>
|
||||
{(chartView === 'both' || chartView === 'portfolio') && <PortfolioChart purchases={purchases} stats={stats} btcHistory={candles?.candles ?? []} />}
|
||||
{(chartView === 'both' || chartView === 'portfolio') && <PortfolioChart purchases={purchases} sells={sells} stats={stats} btcHistory={candles?.candles ?? []} />}
|
||||
{(chartView === 'both' || chartView === 'history') && (
|
||||
<BTCCandlestickChart
|
||||
candles={activeCandles?.candles ?? null}
|
||||
@@ -147,6 +152,8 @@ export default function Dashboard() {
|
||||
)}
|
||||
<AddPurchase onAdded={fetchData} />
|
||||
<PurchaseList purchases={purchases} onChanged={fetchData} />
|
||||
<AddSell onAdded={fetchData} />
|
||||
<SellList sells={sells} onChanged={fetchData} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user