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 [nudgeAmount, setNudgeAmount] = useState(null);
|
||||||
const [, startTransition] = useTransition();
|
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.
|
// Effective amount threshold: optimistic override → monthly override → template default.
|
||||||
const effectiveActual = optimisticActual !== undefined ? optimisticActual : row.actual_amount;
|
const effectiveActual = optimisticActual !== undefined ? optimisticActual : row.actual_amount;
|
||||||
const threshold = effectiveActual != null ? effectiveActual : row.expected_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() {
|
async function handleConfirmSuggestion() {
|
||||||
setSuggestionLoading(true);
|
setSuggestionLoading(true);
|
||||||
try {
|
try {
|
||||||
|
|
@ -1444,21 +1493,68 @@ function Row({ row, year, month, refresh, index, onEditBill }) {
|
||||||
|
|
||||||
{/* Due */}
|
{/* Due */}
|
||||||
<TableCell className="w-[10%] py-3 text-sm font-mono text-muted-foreground">
|
<TableCell className="w-[10%] py-3 text-sm font-mono text-muted-foreground">
|
||||||
{fmtDate(row.due_date)}
|
{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>
|
</TableCell>
|
||||||
|
|
||||||
{/* Expected / Actual — shows actual_amount in amber when it overrides the template */}
|
{/* Expected / Actual — shows actual_amount in amber when it overrides the template */}
|
||||||
<TableCell className="w-[10%] py-3 text-right font-mono text-sm">
|
<TableCell className="w-[10%] py-3 text-right font-mono text-sm">
|
||||||
{effectiveActual != null ? (
|
{editingExpected ? (
|
||||||
<span
|
<input
|
||||||
className="text-amber-500"
|
type="number"
|
||||||
title={`Monthly override. Template default: ${fmt(row.expected_amount)}`}
|
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)}
|
{fmt(effectiveActual)}
|
||||||
</span>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-end gap-0.5">
|
<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 &&
|
{row.amount_suggestion?.suggestion != null &&
|
||||||
Math.abs(row.amount_suggestion.suggestion - row.expected_amount) / row.expected_amount > 0.05 && (
|
Math.abs(row.amount_suggestion.suggestion - row.expected_amount) / row.expected_amount > 0.05 && (
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue