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 sqlalchemy import text
|
||||||
|
|
||||||
from .database import engine, Base, SessionLocal
|
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)
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
@@ -23,6 +23,7 @@ app.include_router(stats.router)
|
|||||||
app.include_router(history.router)
|
app.include_router(history.router)
|
||||||
app.include_router(admin.router, prefix="/admin")
|
app.include_router(admin.router, prefix="/admin")
|
||||||
app.include_router(candles.router)
|
app.include_router(candles.router)
|
||||||
|
app.include_router(sells.router)
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class User(Base):
|
|||||||
is_admin = Column(Boolean, default=False, nullable=False, server_default='0')
|
is_admin = Column(Boolean, default=False, nullable=False, server_default='0')
|
||||||
|
|
||||||
purchases = relationship("Purchase", back_populates="owner", cascade="all, delete")
|
purchases = relationship("Purchase", back_populates="owner", cascade="all, delete")
|
||||||
|
sells = relationship("Sell", back_populates="owner", cascade="all, delete")
|
||||||
|
|
||||||
|
|
||||||
class Purchase(Base):
|
class Purchase(Base):
|
||||||
@@ -27,6 +28,18 @@ class Purchase(Base):
|
|||||||
owner = relationship("User", back_populates="purchases")
|
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):
|
class OHLCCandle(Base):
|
||||||
__tablename__ = "ohlc_candles"
|
__tablename__ = "ohlc_candles"
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from typing import List
|
from typing import List, Optional
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from ..database import get_db
|
from ..database import get_db
|
||||||
@@ -14,6 +14,7 @@ router = APIRouter()
|
|||||||
class PurchaseCreate(BaseModel):
|
class PurchaseCreate(BaseModel):
|
||||||
amount_eur: float = Field(gt=0, le=10_000_000)
|
amount_eur: float = Field(gt=0, le=10_000_000)
|
||||||
price_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):
|
class PurchaseUpdate(BaseModel):
|
||||||
@@ -54,6 +55,7 @@ def add_purchase(
|
|||||||
purchase = models.Purchase(
|
purchase = models.Purchase(
|
||||||
amount_eur=purchase_in.amount_eur,
|
amount_eur=purchase_in.amount_eur,
|
||||||
price_eur=purchase_in.price_eur,
|
price_eur=purchase_in.price_eur,
|
||||||
|
created_at=purchase_in.created_at or datetime.utcnow(),
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
)
|
)
|
||||||
db.add(purchase)
|
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),
|
current_user: models.User = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
purchases = db.query(models.Purchase).filter(models.Purchase.user_id == current_user.id).all()
|
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_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
|
total_btc_bought = 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_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()
|
current_price = get_btc_price_eur()
|
||||||
portfolio_value = total_btc * current_price
|
portfolio_value = net_btc * current_price
|
||||||
profit_loss = portfolio_value - total_invested
|
profit_loss = portfolio_value - net_invested
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"total_invested": round(total_invested, 2),
|
"total_invested": round(net_invested, 2),
|
||||||
"total_btc": round(total_btc, 8),
|
"total_btc": round(net_btc, 8),
|
||||||
"average_price": round(average_price, 2),
|
"average_price": round(average_price, 2),
|
||||||
"current_price": round(current_price, 2),
|
"current_price": round(current_price, 2),
|
||||||
"portfolio_value": round(portfolio_value, 2),
|
"portfolio_value": round(portfolio_value, 2),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const styles = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function AddPurchase({ onAdded }) {
|
export default function AddPurchase({ onAdded }) {
|
||||||
|
const [purchaseDate, setPurchaseDate] = useState(new Date().toISOString().split('T')[0]);
|
||||||
const [amountEur, setAmountEur] = useState('');
|
const [amountEur, setAmountEur] = useState('');
|
||||||
const [priceEur, setPriceEur] = useState('');
|
const [priceEur, setPriceEur] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@@ -30,12 +31,14 @@ export default function AddPurchase({ onAdded }) {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
amount_eur: parseFloat(amountEur),
|
amount_eur: parseFloat(amountEur),
|
||||||
price_eur: parseFloat(priceEur),
|
price_eur: parseFloat(priceEur),
|
||||||
|
created_at: new Date(purchaseDate + 'T12:00:00').toISOString(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
setError('Failed to add purchase');
|
setError('Failed to add purchase');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setPurchaseDate(new Date().toISOString().split('T')[0]);
|
||||||
setAmountEur('');
|
setAmountEur('');
|
||||||
setPriceEur('');
|
setPriceEur('');
|
||||||
onAdded();
|
onAdded();
|
||||||
@@ -50,6 +53,13 @@ export default function AddPurchase({ onAdded }) {
|
|||||||
<div style={styles.title}>Add Purchase</div>
|
<div style={styles.title}>Add Purchase</div>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div style={styles.row}>
|
<div style={styles.row}>
|
||||||
|
<input
|
||||||
|
style={styles.input}
|
||||||
|
type="date"
|
||||||
|
value={purchaseDate}
|
||||||
|
onChange={e => setPurchaseDate(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
<input
|
<input
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
type="number"
|
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;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PortfolioChart({ purchases, stats, btcHistory }) {
|
export default function PortfolioChart({ purchases, sells, stats, btcHistory }) {
|
||||||
const chartRef = useRef(null);
|
const chartRef = useRef(null);
|
||||||
|
|
||||||
if (!purchases || purchases.length === 0) return 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 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();
|
const today = new Date();
|
||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
@@ -89,6 +90,14 @@ export default function PortfolioChart({ purchases, stats, btcHistory }) {
|
|||||||
cumInvested += p.amount_eur;
|
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
|
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 { useNavigate, Link } from 'react-router-dom';
|
||||||
import AddPurchase from '../components/AddPurchase';
|
import AddPurchase from '../components/AddPurchase';
|
||||||
import PurchaseList from '../components/PurchaseList';
|
import PurchaseList from '../components/PurchaseList';
|
||||||
|
import AddSell from '../components/AddSell';
|
||||||
|
import SellList from '../components/SellList';
|
||||||
import PortfolioChart from '../components/PortfolioChart';
|
import PortfolioChart from '../components/PortfolioChart';
|
||||||
import BTCCandlestickChart from '../components/BTCCandlestickChart';
|
import BTCCandlestickChart from '../components/BTCCandlestickChart';
|
||||||
|
|
||||||
@@ -38,6 +40,7 @@ function StatCard({ label, value, highlight }) {
|
|||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const [stats, setStats] = useState(null);
|
const [stats, setStats] = useState(null);
|
||||||
const [purchases, setPurchases] = useState([]);
|
const [purchases, setPurchases] = useState([]);
|
||||||
|
const [sells, setSells] = useState([]);
|
||||||
const [candles, setCandles] = useState(null);
|
const [candles, setCandles] = useState(null);
|
||||||
const [candlesAll, setCandlesAll] = useState(null);
|
const [candlesAll, setCandlesAll] = useState(null);
|
||||||
const [fullscreenChart, setFullscreenChart] = useState(false);
|
const [fullscreenChart, setFullscreenChart] = useState(false);
|
||||||
@@ -50,9 +53,10 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
const fetchData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const [statsRes, purchasesRes, candlesRes] = await Promise.all([
|
const [statsRes, purchasesRes, sellsRes, candlesRes] = await Promise.all([
|
||||||
fetch(`${API}/stats`, { headers: authHeaders() }),
|
fetch(`${API}/stats`, { headers: authHeaders() }),
|
||||||
fetch(`${API}/purchases`, { headers: authHeaders() }),
|
fetch(`${API}/purchases`, { headers: authHeaders() }),
|
||||||
|
fetch(`${API}/sells`, { headers: authHeaders() }),
|
||||||
fetch(`${API}/candles?days=365`, { headers: authHeaders() }),
|
fetch(`${API}/candles?days=365`, { headers: authHeaders() }),
|
||||||
]);
|
]);
|
||||||
if (statsRes.status === 401) {
|
if (statsRes.status === 401) {
|
||||||
@@ -62,6 +66,7 @@ export default function Dashboard() {
|
|||||||
}
|
}
|
||||||
setStats(await statsRes.json());
|
setStats(await statsRes.json());
|
||||||
setPurchases(await purchasesRes.json());
|
setPurchases(await purchasesRes.json());
|
||||||
|
setSells(await sellsRes.json());
|
||||||
setCandles(await candlesRes.json());
|
setCandles(await candlesRes.json());
|
||||||
} catch {
|
} catch {
|
||||||
// silently fail — network may be unavailable
|
// silently fail — network may be unavailable
|
||||||
@@ -135,7 +140,7 @@ export default function Dashboard() {
|
|||||||
>{label}</button>
|
>{label}</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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') && (
|
{(chartView === 'both' || chartView === 'history') && (
|
||||||
<BTCCandlestickChart
|
<BTCCandlestickChart
|
||||||
candles={activeCandles?.candles ?? null}
|
candles={activeCandles?.candles ?? null}
|
||||||
@@ -147,6 +152,8 @@ export default function Dashboard() {
|
|||||||
)}
|
)}
|
||||||
<AddPurchase onAdded={fetchData} />
|
<AddPurchase onAdded={fetchData} />
|
||||||
<PurchaseList purchases={purchases} onChanged={fetchData} />
|
<PurchaseList purchases={purchases} onChanged={fetchData} />
|
||||||
|
<AddSell onAdded={fetchData} />
|
||||||
|
<SellList sells={sells} onChanged={fetchData} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user