inline editing
This commit is contained in:
parent
33f1bfd3c2
commit
8122d07069
|
|
@ -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 (1–31)"
|
||||
/>
|
||||
) : (
|
||||
<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
|
||||
|
|
|
|||
Loading…
Reference in New Issue