From ff7ba51e840a5042addadcf917b5b182132f21b5 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Mon, 23 Mar 2026 22:24:27 +0100 Subject: [PATCH] Add edit purchase feature (date, amount, price) 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 --- btc-portfolio/backend/app/routes/purchases.py | 27 +++++++ .../frontend/src/components/PurchaseList.js | 73 +++++++++++++++++-- btc-portfolio/frontend/src/pages/Dashboard.js | 2 +- 3 files changed, 95 insertions(+), 7 deletions(-) diff --git a/btc-portfolio/backend/app/routes/purchases.py b/btc-portfolio/backend/app/routes/purchases.py index 58ebecf..ff9ef96 100644 --- a/btc-portfolio/backend/app/routes/purchases.py +++ b/btc-portfolio/backend/app/routes/purchases.py @@ -16,6 +16,12 @@ class PurchaseCreate(BaseModel): price_eur: float +class PurchaseUpdate(BaseModel): + amount_eur: float + price_eur: float + created_at: datetime + + class PurchaseOut(BaseModel): id: int amount_eur: float @@ -51,6 +57,27 @@ def add_purchase( return purchase +@router.put("/purchases/{purchase_id}", response_model=PurchaseOut) +def update_purchase( + purchase_id: int, + purchase_in: PurchaseUpdate, + db: Session = Depends(get_db), + current_user: models.User = Depends(get_current_user), +): + purchase = db.query(models.Purchase).filter( + models.Purchase.id == purchase_id, + models.Purchase.user_id == current_user.id, + ).first() + if not purchase: + raise HTTPException(status_code=404, detail="Purchase not found") + purchase.amount_eur = purchase_in.amount_eur + purchase.price_eur = purchase_in.price_eur + purchase.created_at = purchase_in.created_at + db.commit() + db.refresh(purchase) + return purchase + + @router.delete("/purchases/{purchase_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_purchase( purchase_id: int, diff --git a/btc-portfolio/frontend/src/components/PurchaseList.js b/btc-portfolio/frontend/src/components/PurchaseList.js index 2c82705..971f9a2 100644 --- a/btc-portfolio/frontend/src/components/PurchaseList.js +++ b/btc-portfolio/frontend/src/components/PurchaseList.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; const API = process.env.REACT_APP_API_URL || 'http://localhost:8000'; @@ -8,20 +8,61 @@ const styles = { 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' }, }; -export default function PurchaseList({ purchases, onDeleted }) { +function toDateInputValue(isoString) { + return isoString ? isoString.slice(0, 10) : ''; +} + +export default function PurchaseList({ purchases, onChanged }) { + const [editingId, setEditingId] = useState(null); + const [editForm, setEditForm] = useState({}); + + const token = () => localStorage.getItem('token'); + + const startEdit = (p) => { + setEditingId(p.id); + setEditForm({ + amount_eur: p.amount_eur, + price_eur: p.price_eur, + created_at: toDateInputValue(p.created_at), + }); + }; + + const cancelEdit = () => setEditingId(null); + + const handleSave = async (id) => { + const res = await fetch(`${API}/purchases/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token()}` }, + body: JSON.stringify({ + amount_eur: parseFloat(editForm.amount_eur), + price_eur: parseFloat(editForm.price_eur), + created_at: new Date(editForm.created_at).toISOString(), + }), + }); + if (res.ok) { + setEditingId(null); + onChanged(); + } + }; + const handleDelete = async (id) => { - const token = localStorage.getItem('token'); await fetch(`${API}/purchases/${id}`, { method: 'DELETE', - headers: { Authorization: `Bearer ${token}` }, + headers: { Authorization: `Bearer ${token()}` }, }); - onDeleted(); + onChanged(); }; + const set = (field) => (e) => setEditForm(f => ({ ...f, [field]: e.target.value })); + return (
Purchases
@@ -39,13 +80,33 @@ export default function PurchaseList({ purchases, onDeleted }) { - {purchases.map(p => ( + {purchases.map(p => editingId === p.id ? ( + + + + + + + + + + + + {(parseFloat(editForm.amount_eur) / parseFloat(editForm.price_eur) || 0).toFixed(6)} + + + + + + + ) : ( {new Date(p.created_at).toLocaleDateString()} €{p.amount_eur.toLocaleString()} €{p.price_eur.toLocaleString()} {(p.amount_eur / p.price_eur).toFixed(6)} + diff --git a/btc-portfolio/frontend/src/pages/Dashboard.js b/btc-portfolio/frontend/src/pages/Dashboard.js index cd13a85..7269991 100644 --- a/btc-portfolio/frontend/src/pages/Dashboard.js +++ b/btc-portfolio/frontend/src/pages/Dashboard.js @@ -93,7 +93,7 @@ export default function Dashboard() { - +
); }