fix(transactions): editable transaction type, fee/interest counted as spend, fees category
- TypeBadge is now clickable — opens inline select to change debit/credit/fee/interest/etc. - PATCH /api/transactions/[id] now accepts transaction_type, updates transactions table directly - Analytics monthly query includes fee and interest types as spend (not just debit) - fee and interest amounts show red in transaction list (same as debit) - Add fees category to taxonomy
This commit is contained in:
@@ -15,30 +15,62 @@ function formatDate(d: string) {
|
||||
});
|
||||
}
|
||||
|
||||
const SPEND_TYPES = new Set(["debit", "fee", "interest"]);
|
||||
|
||||
function formatAmount(amount: number, type: string) {
|
||||
const formatted = new Intl.NumberFormat("en-AU", {
|
||||
style: "currency",
|
||||
currency: "AUD",
|
||||
}).format(amount);
|
||||
return type === "debit" ? formatted : `+${formatted}`;
|
||||
return SPEND_TYPES.has(type) ? formatted : `+${formatted}`;
|
||||
}
|
||||
|
||||
const TYPE_COLORS: Record<string, string> = {
|
||||
debit: "bg-red-900/30 text-red-400",
|
||||
credit: "bg-green-900/30 text-green-400",
|
||||
payment: "bg-blue-900/30 text-blue-400",
|
||||
refund: "bg-emerald-900/30 text-emerald-400",
|
||||
fee: "bg-yellow-900/30 text-yellow-400",
|
||||
interest: "bg-orange-900/30 text-orange-400",
|
||||
transfer: "bg-zinc-800 text-zinc-400",
|
||||
};
|
||||
|
||||
const TYPE_OPTIONS = [
|
||||
"debit", "credit", "payment", "refund", "fee", "interest", "transfer",
|
||||
].map((t) => ({ value: t, label: t }));
|
||||
|
||||
function TypeBadge({ type }: { type: string }) {
|
||||
const colors: Record<string, string> = {
|
||||
debit: "bg-red-900/30 text-red-400",
|
||||
credit: "bg-green-900/30 text-green-400",
|
||||
payment: "bg-blue-900/30 text-blue-400",
|
||||
refund: "bg-emerald-900/30 text-emerald-400",
|
||||
fee: "bg-yellow-900/30 text-yellow-400",
|
||||
interest: "bg-orange-900/30 text-orange-400",
|
||||
};
|
||||
return (
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${colors[type] || "bg-zinc-800 text-zinc-400"}`}>
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${TYPE_COLORS[type] || "bg-zinc-800 text-zinc-400"}`}>
|
||||
{type}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function EditableTypeBadge({ type, onSave }: { type: string; onSave: (t: string) => void }) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
if (!editing) {
|
||||
return (
|
||||
<button onClick={() => setEditing(true)} title="Click to change type">
|
||||
<TypeBadge type={type} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<select
|
||||
autoFocus
|
||||
defaultValue={type}
|
||||
onBlur={(e) => { onSave(e.target.value); setEditing(false); }}
|
||||
onChange={(e) => { onSave(e.target.value); setEditing(false); }}
|
||||
className="bg-zinc-800 border border-zinc-600 rounded px-1 py-0.5 text-xs"
|
||||
>
|
||||
{TYPE_OPTIONS.map((o) => (
|
||||
<option key={o.value} value={o.value}>{o.label}</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
function InlineEdit({
|
||||
value,
|
||||
onSave,
|
||||
@@ -386,11 +418,16 @@ function TransactionsContent() {
|
||||
</div>
|
||||
</td>
|
||||
<td className={`p-2 text-right whitespace-nowrap font-mono ${
|
||||
t.transaction_type === "debit" ? "text-red-400" : "text-green-400"
|
||||
SPEND_TYPES.has(t.transaction_type) ? "text-red-400" : "text-green-400"
|
||||
}`}>
|
||||
{formatAmount(t.amount, t.transaction_type)}
|
||||
</td>
|
||||
<td className="p-2"><TypeBadge type={t.transaction_type} /></td>
|
||||
<td className="p-2">
|
||||
<EditableTypeBadge
|
||||
type={t.transaction_type}
|
||||
onSave={(val) => updateTxn.mutate({ id: t.id, transaction_type: val })}
|
||||
/>
|
||||
</td>
|
||||
<td className="p-2 max-w-[140px]">
|
||||
<div className="relative">
|
||||
<InlineEdit
|
||||
|
||||
Reference in New Issue
Block a user