inline editing

This commit is contained in:
null 2026-05-28 02:53:35 -05:00
parent 33f1bfd3c2
commit 8122d07069
1 changed files with 103 additions and 7 deletions

View File

@ -1231,6 +1231,11 @@ function Row({ row, year, month, refresh, index, onEditBill }) {
const [nudgeAmount, setNudgeAmount] = useState(null);
const [, startTransition] = useTransition();
const [editingExpected, setEditingExpected] = useState(false);
const [expectedDraft, setExpectedDraft] = useState('');
const [editingDue, setEditingDue] = useState(false);
const [dueDraft, setDueDraft] = useState('');
// Effective amount threshold: optimistic override monthly override template default.
const effectiveActual = optimisticActual !== undefined ? optimisticActual : row.actual_amount;
const threshold = effectiveActual != null ? effectiveActual : row.expected_amount;
@ -1357,6 +1362,50 @@ function Row({ row, year, month, refresh, index, onEditBill }) {
}
}
async function handleSaveExpected() {
setEditingExpected(false);
const val = parseFloat(expectedDraft);
if (!isFinite(val) || val < 0) return;
const current = effectiveActual ?? row.expected_amount;
if (val === current) return;
if (effectiveActual != null) {
setOptimisticActual(val);
try {
await api.saveBillMonthlyState(row.id, {
year, month,
actual_amount: val,
notes: row.monthly_notes || null,
is_skipped: row.is_skipped,
});
refresh?.();
} catch (err) {
setOptimisticActual(undefined);
toast.error(err.message || 'Failed to update amount');
}
} else {
try {
await api.updateBill(row.id, { name: row.name, due_day: row.due_day, expected_amount: val });
refresh?.();
} catch (err) {
toast.error(err.message || 'Failed to update expected amount');
}
}
}
async function handleSaveDue() {
setEditingDue(false);
const day = parseInt(dueDraft, 10);
if (!isFinite(day) || day < 1 || day > 31) return;
if (day === row.due_day) return;
try {
await api.updateBill(row.id, { name: row.name, due_day: day, expected_amount: row.expected_amount });
refresh?.();
} catch (err) {
toast.error(err.message || 'Failed to update due date');
}
}
async function handleConfirmSuggestion() {
setSuggestionLoading(true);
try {
@ -1444,21 +1493,68 @@ function Row({ row, year, month, refresh, index, onEditBill }) {
{/* Due */}
<TableCell className="w-[10%] py-3 text-sm font-mono text-muted-foreground">
{editingDue ? (
<input
type="number"
min="1" max="31"
value={dueDraft}
autoFocus
onChange={e => setDueDraft(e.target.value)}
onBlur={handleSaveDue}
onKeyDown={e => {
if (e.key === 'Enter') e.currentTarget.blur();
if (e.key === 'Escape') { setEditingDue(false); }
}}
className="w-12 rounded border border-border bg-transparent px-1 py-0.5 text-sm font-mono text-foreground outline-none focus:ring-[2px] focus:ring-ring/50"
title="Day of month (131)"
/>
) : (
<button
type="button"
onClick={() => { setDueDraft(String(row.due_day)); setEditingDue(true); }}
className="rounded px-1 py-0.5 transition-colors hover:bg-accent hover:text-foreground"
title="Click to edit due day"
>
{fmtDate(row.due_date)}
</button>
)}
</TableCell>
{/* Expected / Actual — shows actual_amount in amber when it overrides the template */}
<TableCell className="w-[10%] py-3 text-right font-mono text-sm">
{effectiveActual != null ? (
<span
className="text-amber-500"
title={`Monthly override. Template default: ${fmt(row.expected_amount)}`}
{editingExpected ? (
<input
type="number"
min="0" step="0.01"
value={expectedDraft}
autoFocus
onChange={e => setExpectedDraft(e.target.value)}
onBlur={handleSaveExpected}
onKeyDown={e => {
if (e.key === 'Enter') e.currentTarget.blur();
if (e.key === 'Escape') { setEditingExpected(false); }
}}
className="w-24 rounded border border-border bg-transparent px-1 py-0.5 text-right text-sm font-mono text-foreground outline-none focus:ring-[2px] focus:ring-ring/50"
/>
) : effectiveActual != null ? (
<button
type="button"
onClick={() => { setExpectedDraft(String(effectiveActual)); setEditingExpected(true); }}
className="rounded px-1 py-0.5 text-amber-500 transition-colors hover:bg-accent"
title={`Monthly override — click to edit. Template default: ${fmt(row.expected_amount)}`}
>
{fmt(effectiveActual)}
</span>
</button>
) : (
<div className="flex flex-col items-end gap-0.5">
<span className="text-muted-foreground">{fmt(row.expected_amount)}</span>
<button
type="button"
onClick={() => { setExpectedDraft(String(row.expected_amount)); setEditingExpected(true); }}
className="rounded px-1 py-0.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
title="Click to edit expected amount"
>
{fmt(row.expected_amount)}
</button>
{row.amount_suggestion?.suggestion != null &&
Math.abs(row.amount_suggestion.suggestion - row.expected_amount) / row.expected_amount > 0.05 && (
<button