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) => (
-
@@ -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',