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 <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-03-23 22:24:27 +01:00
parent a2dad16f21
commit ff7ba51e84
3 changed files with 95 additions and 7 deletions
@@ -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,
@@ -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 (
<div style={styles.card}>
<div style={styles.title}>Purchases</div>
@@ -39,13 +80,33 @@ export default function PurchaseList({ purchases, onDeleted }) {
</tr>
</thead>
<tbody>
{purchases.map(p => (
{purchases.map(p => editingId === p.id ? (
<tr key={p.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.amount_eur} onChange={set('amount_eur')} />
</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.amount_eur) / parseFloat(editForm.price_eur) || 0).toFixed(6)}
</td>
<td style={styles.td}>
<button style={styles.saveBtn} onClick={() => handleSave(p.id)}>Save</button>
<button style={styles.cancelBtn} onClick={cancelEdit}>Cancel</button>
</td>
</tr>
) : (
<tr key={p.id}>
<td style={styles.td}>{new Date(p.created_at).toLocaleDateString()}</td>
<td style={styles.td}>{p.amount_eur.toLocaleString()}</td>
<td style={styles.td}>{p.price_eur.toLocaleString()}</td>
<td style={styles.td}>{(p.amount_eur / p.price_eur).toFixed(6)}</td>
<td style={styles.td}>
<button style={styles.editBtn} onClick={() => startEdit(p)}>Edit</button>
<button style={styles.deleteBtn} onClick={() => handleDelete(p.id)}>Delete</button>
</td>
</tr>
@@ -93,7 +93,7 @@ export default function Dashboard() {
<PortfolioChart purchases={purchases} stats={stats} />
<AddPurchase onAdded={fetchData} />
<PurchaseList purchases={purchases} onDeleted={fetchData} />
<PurchaseList purchases={purchases} onChanged={fetchData} />
</div>
);
}