Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 108 additions & 61 deletions frontend/src/app/transactions/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export default function TransactionsPage() {
</div>

{/* Filters */}
<div className="flex gap-3 mb-4">
<div className="flex flex-col sm:flex-row gap-3 mb-4">
<select
value={filter.tx_type}
onChange={(e) => setFilter({ ...filter, tx_type: e.target.value })}
Expand Down Expand Up @@ -124,7 +124,7 @@ export default function TransactionsPage() {

{/* Bulk Action Bar */}
{selectedIds.size > 0 && (
<div className="flex items-center gap-3 mb-4 px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl">
<div className="flex flex-col sm:flex-row sm:items-center gap-3 mb-4 px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl">
<span className="text-sm font-medium text-gray-700">
{selectedIds.size} transaction{selectedIds.size !== 1 ? "s" : ""} selected
</span>
Expand Down Expand Up @@ -167,71 +167,118 @@ export default function TransactionsPage() {
No transactions found. <a href="/upload" className="underline">Upload a statement</a> to get started.
</div>
) : (
<table className="w-full text-sm">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-4 py-3 w-10">
<input
type="checkbox"
checked={transactions.length > 0 && selectedIds.size === transactions.length}
onChange={toggleSelectAll}
className="rounded border-gray-300"
/>
</th>
<th className="text-left px-4 py-3 font-medium text-gray-600">Date</th>
<th className="text-left px-4 py-3 font-medium text-gray-600">Narration</th>
<th className="text-left px-4 py-3 font-medium text-gray-600">Category</th>
<th className="text-right px-4 py-3 font-medium text-gray-600">Amount</th>
</tr>
</thead>
<tbody>
{transactions.map((tx) => (
<tr key={tx.id} className={`border-b border-gray-100 hover:bg-gray-50 ${selectedIds.has(tx.id) ? "bg-gray-50" : ""}`}>
<td className="px-4 py-3">
<>
{/* Desktop/tablet: table */}
<table className="w-full text-sm hidden md:table">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-4 py-3 w-10">
<input
type="checkbox"
checked={selectedIds.has(tx.id)}
onChange={() => toggleSelect(tx.id)}
checked={transactions.length > 0 && selectedIds.size === transactions.length}
onChange={toggleSelectAll}
className="rounded border-gray-300"
/>
</td>
<td className="px-4 py-3 text-gray-600 whitespace-nowrap">{formatDate(tx.date)}</td>
<td className="px-4 py-3 text-gray-900 max-w-xs truncate" title={tx.narration}>{tx.narration}</td>
<td className="px-4 py-3">
{editingId === tx.id ? (
<select
defaultValue={tx.category}
onChange={(e) => handleCategoryChange(tx.id, e.target.value)}
onBlur={() => setEditingId(null)}
autoFocus
className="px-2 py-1 border border-gray-300 rounded text-xs bg-white"
>
{Object.entries(CATEGORIES).map(([key, label]) => (
<option key={key} value={key}>{label}</option>
))}
</select>
) : (
<button
onClick={() => setEditingId(tx.id)}
className={`px-2 py-1 rounded text-xs font-medium ${
tx.category_confirmed
? "bg-green-100 text-green-700"
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
}`}
>
{CATEGORIES[tx.category] || tx.category}
</button>
)}
</td>
<td className={`px-4 py-3 text-right font-medium whitespace-nowrap ${
tx.tx_type === "credit" ? "text-green-700" : "text-red-600"
}`}>
{tx.tx_type === "credit" ? "+" : "-"}{formatCurrency(tx.amount)}
</td>
</th>
<th className="text-left px-4 py-3 font-medium text-gray-600">Date</th>
<th className="text-left px-4 py-3 font-medium text-gray-600">Narration</th>
<th className="text-left px-4 py-3 font-medium text-gray-600">Category</th>
<th className="text-right px-4 py-3 font-medium text-gray-600">Amount</th>
</tr>
</thead>
<tbody>
{transactions.map((tx) => (
<tr key={tx.id} className={`border-b border-gray-100 hover:bg-gray-50 ${selectedIds.has(tx.id) ? "bg-gray-50" : ""}`}>
<td className="px-4 py-3">
<input
type="checkbox"
checked={selectedIds.has(tx.id)}
onChange={() => toggleSelect(tx.id)}
className="rounded border-gray-300"
/>
</td>
<td className="px-4 py-3 text-gray-600 whitespace-nowrap">{formatDate(tx.date)}</td>
<td className="px-4 py-3 text-gray-900 max-w-xs truncate" title={tx.narration}>{tx.narration}</td>
<td className="px-4 py-3">
{editingId === tx.id ? (
<select
defaultValue={tx.category}
onChange={(e) => handleCategoryChange(tx.id, e.target.value)}
onBlur={() => setEditingId(null)}
autoFocus
className="px-2 py-1 border border-gray-300 rounded text-xs bg-white"
>
{Object.entries(CATEGORIES).map(([key, label]) => (
<option key={key} value={key}>{label}</option>
))}
</select>
) : (
<button
onClick={() => setEditingId(tx.id)}
className={`px-2 py-1 rounded text-xs font-medium ${
tx.category_confirmed
? "bg-green-100 text-green-700"
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
}`}
>
{CATEGORIES[tx.category] || tx.category}
</button>
)}
</td>
<td className={`px-4 py-3 text-right font-medium whitespace-nowrap ${
tx.tx_type === "credit" ? "text-green-700" : "text-red-600"
}`}>
{tx.tx_type === "credit" ? "+" : "-"}{formatCurrency(tx.amount)}
</td>
</tr>
))}
</tbody>
</table>

{/* Mobile: card list */}
<div className="md:hidden divide-y divide-gray-100">
{transactions.map((tx) => (
<div key={tx.id} className="px-4 py-3">
<div className="flex items-start justify-between gap-2">
<p className="text-gray-900 text-sm break-words">{tx.narration}</p>
<span
className={`font-medium whitespace-nowrap shrink-0 ${
tx.tx_type === "credit" ? "text-green-700" : "text-red-600"
}`}
>
{tx.tx_type === "credit" ? "+" : "-"}{formatCurrency(tx.amount)}
</span>
</div>
<div className="flex items-center justify-between gap-2 mt-2">
<span className="text-xs text-gray-500">{formatDate(tx.date)}</span>
<button
onClick={() => setEditingId(tx.id)}
className={`px-2 py-1 rounded text-xs font-medium ${
tx.category_confirmed
? "bg-green-100 text-green-700"
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
}`}
>
{CATEGORIES[tx.category] || tx.category}
</button>
</div>
{editingId === tx.id && (
<select
defaultValue={tx.category}
onChange={(e) => handleCategoryChange(tx.id, e.target.value)}
onBlur={() => setEditingId(null)}
autoFocus
className="mt-2 w-full px-2 py-1.5 border border-gray-300 rounded text-xs bg-white"
>
{Object.entries(CATEGORIES).map(([key, label]) => (
<option key={key} value={key}>{label}</option>
))}
</select>
)}
</div>
))}
</tbody>
</table>
</div>
</>
)}
</div>
</div>
Expand Down