Add transaction comments UI

This commit is contained in:
MOHAN 2026-07-16 23:43:23 +05:30
parent 5ed97c2a90
commit 1dbca1bcd4
2 changed files with 132 additions and 1 deletions

View File

@ -0,0 +1,16 @@
import { NextRequest } from "next/server";
import { proxyRequest } from "@/lib/backend";
export async function GET(
req: NextRequest,
{ params }: { params: { id: string } }
) {
return proxyRequest(req, `transactions/${params.id}/comments`);
}
export async function POST(
req: NextRequest,
{ params }: { params: { id: string } }
) {
return proxyRequest(req, `transactions/${params.id}/comments`);
}

View File

@ -22,9 +22,21 @@ type TransactionRow = {
};
status?: string;
hidden?: boolean;
commentCount?: number;
date: string;
};
type TransactionComment = {
id: string;
body: string;
createdAt: string;
updatedAt: string;
author: {
displayName: string;
email?: string | null;
};
};
type Account = {
viewRef: string;
institutionName: string;
@ -125,6 +137,10 @@ export default function TransactionsPage() {
splitYoursPercent: "50",
});
const [editingRef, setEditingRef] = useState<string | null>(null);
const [commentRef, setCommentRef] = useState<string | null>(null);
const [comments, setComments] = useState<TransactionComment[]>([]);
const [commentDraft, setCommentDraft] = useState("");
const [commentStatus, setCommentStatus] = useState("");
const [editForm, setEditForm] = useState({
category: "",
note: "",
@ -400,6 +416,37 @@ export default function TransactionsPage() {
});
};
const openComments = async (row: TransactionRow) => {
setCommentRef(row.viewRef);
setComments([]);
setCommentDraft("");
setCommentStatus("Loading comments...");
const res = await apiFetch<TransactionComment[]>(`/api/transactions/${row.viewRef}/comments`);
if (res.error) {
setCommentStatus(res.error.message ?? "Unable to load comments.");
return;
}
setComments(res.data ?? []);
setCommentStatus("");
};
const createComment = async () => {
if (!commentRef || !commentDraft.trim()) return;
setCommentStatus("Posting comment...");
const res = await apiFetch<TransactionComment>(`/api/transactions/${commentRef}/comments`, {
method: "POST",
body: JSON.stringify({ body: commentDraft }),
});
if (res.error) {
setCommentStatus(res.error.message ?? "Unable to post comment.");
return;
}
if (res.data) setComments((prev) => [...prev, res.data]);
setCommentDraft("");
setCommentStatus("");
await load();
};
const saveEdit = async () => {
if (!editingRef) return;
setStatus("Saving edits...");
@ -423,6 +470,7 @@ export default function TransactionsPage() {
const inputCls = "mt-2 w-full rounded-md border border-border bg-background/50 px-3 py-2 text-sm text-foreground focus:border-primary focus:ring-primary focus:outline-none";
const labelCls = "text-xs font-semibold text-muted-foreground uppercase tracking-wider";
const selectedCommentRow = rows.find((row) => row.viewRef === commentRef) ?? null;
return (
<AppShell title="Transactions" subtitle="View, sync, and categorize your transactions.">
@ -780,6 +828,68 @@ export default function TransactionsPage() {
{status && (
<div className="px-6 py-3 bg-secondary/30 border-b border-border text-sm text-muted-foreground">{status}</div>
)}
{selectedCommentRow && (
<div className="border-b border-border bg-secondary/10 p-5">
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Transaction chat</p>
<h2 className="mt-1 text-base font-bold text-foreground">{selectedCommentRow.description ?? selectedCommentRow.name ?? "Transaction"}</h2>
<p className="mt-1 text-xs text-muted-foreground">
{new Date(selectedCommentRow.date).toLocaleDateString()} · {formatAmount(selectedCommentRow.amount).display}
</p>
</div>
<button
type="button"
onClick={() => {
setCommentRef(null);
setComments([]);
setCommentDraft("");
setCommentStatus("");
}}
className="rounded border border-border px-3 py-1 text-xs font-semibold text-foreground hover:bg-secondary"
>
Close
</button>
</div>
<div className="mt-4 grid gap-4 lg:grid-cols-[1fr_320px]">
<div className="max-h-64 space-y-3 overflow-y-auto rounded-lg border border-border bg-background/40 p-3">
{comments.map((comment) => (
<div key={comment.id} className="rounded-lg border border-border bg-background p-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<p className="text-sm font-semibold text-foreground">{comment.author.displayName}</p>
<p className="text-xs text-muted-foreground">{new Date(comment.createdAt).toLocaleString()}</p>
</div>
<p className="mt-2 whitespace-pre-wrap text-sm leading-6 text-muted-foreground">{comment.body}</p>
</div>
))}
{!comments.length && !commentStatus && (
<p className="text-sm text-muted-foreground">No comments yet.</p>
)}
{commentStatus && <p className="text-sm text-muted-foreground">{commentStatus}</p>}
</div>
<div>
<textarea
value={commentDraft}
onChange={(event) => setCommentDraft(event.target.value)}
maxLength={1000}
placeholder="Add a note for this transaction"
className="h-32 w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground focus:border-primary focus:outline-none"
/>
<div className="mt-2 flex items-center justify-between gap-3">
<span className="text-xs text-muted-foreground">{commentDraft.length}/1000</span>
<button
type="button"
onClick={createComment}
disabled={!commentDraft.trim()}
className="rounded bg-primary px-4 py-2 text-xs font-bold text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
>
Post comment
</button>
</div>
</div>
</div>
</div>
)}
<div className="overflow-x-auto">
<table className="w-full text-left text-xs text-muted-foreground">
<thead className="text-[0.65rem] uppercase tracking-[0.18em] font-semibold bg-secondary/20">
@ -897,7 +1007,12 @@ export default function TransactionsPage() {
{formatAmount(row.amount).display}
</td>
<td className="px-4 py-3 text-right">
<button onClick={() => startEdit(row)} className="text-xs text-primary hover:underline">Edit</button>
<div className="flex justify-end gap-3">
<button onClick={() => openComments(row)} className="text-xs text-primary hover:underline">
Comments{row.commentCount ? ` (${row.commentCount})` : ""}
</button>
<button onClick={() => startEdit(row)} className="text-xs text-primary hover:underline">Edit</button>
</div>
</td>
</tr>
)