diff --git a/.claude/settings.local.json b/.claude/settings.local.json index d5c844a..1f57d21 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -8,7 +8,12 @@ "Bash(td list:*)", "Bash(git add:*)", "Bash(git commit:*)", - "Bash(td start:*)" + "Bash(td start:*)", + "Bash(td close:*)", + "Bash(td review:*)", + "Bash(td approve:*)", + "Bash(td complete:*)", + "Bash(td update:*)" ] } } diff --git a/client/src/pages/PaycheckView.jsx b/client/src/pages/PaycheckView.jsx index 8011101..68d232d 100644 --- a/client/src/pages/PaycheckView.jsx +++ b/client/src/pages/PaycheckView.jsx @@ -26,7 +26,9 @@ function todayISO() { return new Date().toISOString().slice(0, 10); } -function PaycheckColumn({ paycheck, onBillPaidToggle, categories }) { +function PaycheckColumn({ paycheck, onBillPaidToggle, categories, onOtePaidToggle, onOteDelete, onOteAdd }) { + const [newOteName, setNewOteName] = useState(''); + const [newOteAmount, setNewOteAmount] = useState(''); const [actuals, setActuals] = useState([]); const [actualsLoading, setActualsLoading] = useState(false); const [actualsError, setActualsError] = useState(null); @@ -180,12 +182,50 @@ function PaycheckColumn({ paycheck, onBillPaidToggle, categories }) {
(none)
) : ( paycheck.one_time_expenses.map((ote) => ( -
- {ote.name} +
+ onOtePaidToggle(ote.id, !ote.paid)} + style={styles.checkbox} + /> + + {ote.name} + {formatCurrency(ote.amount)} +
)) )} +
+ setNewOteName(e.target.value)} + style={styles.oteAddInput} + /> + setNewOteAmount(e.target.value)} + min="0" + step="0.01" + style={{ ...styles.oteAddInput, width: '80px' }} + /> + +
@@ -341,6 +381,45 @@ function PaycheckView() { } } + async function handleOtePaidToggle(oteId, paid) { + try { + const res = await fetch(`/api/one-time-expenses/${oteId}/paid`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ paid }), + }); + if (!res.ok) throw new Error(`Server error: ${res.status}`); + await loadPaychecks(year, month); + } catch (err) { + alert(`Failed to update expense: ${err.message}`); + } + } + + async function handleOteDelete(oteId) { + if (!window.confirm('Remove this expense?')) return; + try { + const res = await fetch(`/api/one-time-expenses/${oteId}`, { method: 'DELETE' }); + if (!res.ok) throw new Error(`Server error: ${res.status}`); + await loadPaychecks(year, month); + } catch (err) { + alert(`Failed to delete expense: ${err.message}`); + } + } + + async function handleOteAdd(paycheckId, name, amount) { + try { + const res = await fetch('/api/one-time-expenses', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ paycheck_id: paycheckId, name, amount }), + }); + if (!res.ok) throw new Error(`Server error: ${res.status}`); + await loadPaychecks(year, month); + } catch (err) { + alert(`Failed to add expense: ${err.message}`); + } + } + async function handleBillPaidToggle(paycheckBillId, paid) { // Optimistic update setPaychecks(prev => @@ -411,11 +490,17 @@ function PaycheckView() {
@@ -661,6 +746,28 @@ const styles = { color: '#c0392b', fontSize: '0.8rem', }, + oteAddForm: { + display: 'flex', + gap: '0.4rem', + marginTop: '0.5rem', + alignItems: 'center', + }, + oteAddInput: { + padding: '0.2rem 0.4rem', + fontSize: '0.875rem', + border: '1px solid #ccc', + borderRadius: '3px', + flex: 1, + }, + oteAddButton: { + padding: '0.2rem 0.6rem', + fontSize: '0.875rem', + cursor: 'pointer', + border: '1px solid #bbb', + borderRadius: '3px', + background: '#f0f0f0', + flexShrink: 0, + }, remainingRow: { display: 'flex', justifyContent: 'space-between',