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:
@@ -16,6 +16,12 @@ class PurchaseCreate(BaseModel):
|
|||||||
price_eur: float
|
price_eur: float
|
||||||
|
|
||||||
|
|
||||||
|
class PurchaseUpdate(BaseModel):
|
||||||
|
amount_eur: float
|
||||||
|
price_eur: float
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOut(BaseModel):
|
class PurchaseOut(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
amount_eur: float
|
amount_eur: float
|
||||||
@@ -51,6 +57,27 @@ def add_purchase(
|
|||||||
return 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)
|
@router.delete("/purchases/{purchase_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
def delete_purchase(
|
def delete_purchase(
|
||||||
purchase_id: int,
|
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';
|
const API = process.env.REACT_APP_API_URL || 'http://localhost:8000';
|
||||||
|
|
||||||
@@ -8,20 +8,61 @@ const styles = {
|
|||||||
table: { width: '100%', borderCollapse: 'collapse' },
|
table: { width: '100%', borderCollapse: 'collapse' },
|
||||||
th: { textAlign: 'left', padding: '0.5rem 0.75rem', borderBottom: '1px solid #333', color: '#888', fontSize: '0.85rem' },
|
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' },
|
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' },
|
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' },
|
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 handleDelete = async (id) => {
|
||||||
const token = localStorage.getItem('token');
|
|
||||||
await fetch(`${API}/purchases/${id}`, {
|
await fetch(`${API}/purchases/${id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token()}` },
|
||||||
});
|
});
|
||||||
onDeleted();
|
onChanged();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const set = (field) => (e) => setEditForm(f => ({ ...f, [field]: e.target.value }));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={styles.card}>
|
<div style={styles.card}>
|
||||||
<div style={styles.title}>Purchases</div>
|
<div style={styles.title}>Purchases</div>
|
||||||
@@ -39,13 +80,33 @@ export default function PurchaseList({ purchases, onDeleted }) {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<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}>
|
<tr key={p.id}>
|
||||||
<td style={styles.td}>{new Date(p.created_at).toLocaleDateString()}</td>
|
<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.amount_eur.toLocaleString()}</td>
|
||||||
<td style={styles.td}>€{p.price_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}>{(p.amount_eur / p.price_eur).toFixed(6)}</td>
|
||||||
<td style={styles.td}>
|
<td style={styles.td}>
|
||||||
|
<button style={styles.editBtn} onClick={() => startEdit(p)}>Edit</button>
|
||||||
<button style={styles.deleteBtn} onClick={() => handleDelete(p.id)}>Delete</button>
|
<button style={styles.deleteBtn} onClick={() => handleDelete(p.id)}>Delete</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
<PortfolioChart purchases={purchases} stats={stats} />
|
<PortfolioChart purchases={purchases} stats={stats} />
|
||||||
<AddPurchase onAdded={fetchData} />
|
<AddPurchase onAdded={fetchData} />
|
||||||
<PurchaseList purchases={purchases} onDeleted={fetchData} />
|
<PurchaseList purchases={purchases} onChanged={fetchData} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user