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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user