mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-14 01:32:19 +00:00
- Add AccountUpdate interface to TypeScript types
- Add updateAccount method to API client for PUT /api/v1/accounts/{id}
- Implement inline editing UI in AccountsOverview component
- Add edit/save/cancel buttons with proper state management
- Handle keyboard shortcuts (Enter to save, Escape to cancel)
- Add loading states and error handling for account updates
- Use React Query mutations for optimistic updates
- Refresh account data after successful updates
This enables users to edit account names directly in the Accounts view
using the new API endpoint that was added in the backend.
306 lines
12 KiB
TypeScript
306 lines
12 KiB
TypeScript
import { useState } from "react";
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import {
|
|
CreditCard,
|
|
TrendingUp,
|
|
TrendingDown,
|
|
Building2,
|
|
RefreshCw,
|
|
AlertCircle,
|
|
Edit2,
|
|
Check,
|
|
X,
|
|
} from "lucide-react";
|
|
import { apiClient } from "../lib/api";
|
|
import { formatCurrency, formatDate } from "../lib/utils";
|
|
import LoadingSpinner from "./LoadingSpinner";
|
|
import type { Account, Balance } from "../types/api";
|
|
|
|
export default function AccountsOverview() {
|
|
const {
|
|
data: accounts,
|
|
isLoading: accountsLoading,
|
|
error: accountsError,
|
|
refetch: refetchAccounts,
|
|
} = useQuery<Account[]>({
|
|
queryKey: ["accounts"],
|
|
queryFn: apiClient.getAccounts,
|
|
});
|
|
|
|
const { data: balances } = useQuery<Balance[]>({
|
|
queryKey: ["balances"],
|
|
queryFn: () => apiClient.getBalances(),
|
|
});
|
|
|
|
const [editingAccountId, setEditingAccountId] = useState<string | null>(null);
|
|
const [editingName, setEditingName] = useState("");
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
const updateAccountMutation = useMutation({
|
|
mutationFn: ({ id, name }: { id: string; name: string }) =>
|
|
apiClient.updateAccount(id, { name }),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["accounts"] });
|
|
setEditingAccountId(null);
|
|
setEditingName("");
|
|
},
|
|
onError: (error) => {
|
|
console.error("Failed to update account:", error);
|
|
},
|
|
});
|
|
|
|
const handleEditStart = (account: Account) => {
|
|
setEditingAccountId(account.id);
|
|
setEditingName(account.name || "");
|
|
};
|
|
|
|
const handleEditSave = () => {
|
|
if (editingAccountId && editingName.trim()) {
|
|
updateAccountMutation.mutate({
|
|
id: editingAccountId,
|
|
name: editingName.trim(),
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleEditCancel = () => {
|
|
setEditingAccountId(null);
|
|
setEditingName("");
|
|
};
|
|
|
|
if (accountsLoading) {
|
|
return (
|
|
<div className="bg-white rounded-lg shadow">
|
|
<LoadingSpinner message="Loading accounts..." />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (accountsError) {
|
|
return (
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<div className="flex items-center justify-center text-center">
|
|
<div>
|
|
<AlertCircle className="h-12 w-12 text-red-400 mx-auto mb-4" />
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
|
Failed to load accounts
|
|
</h3>
|
|
<p className="text-gray-600 mb-4">
|
|
Unable to connect to the Leggen API. Please check your
|
|
configuration and ensure the API server is running.
|
|
</p>
|
|
<button
|
|
onClick={() => refetchAccounts()}
|
|
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
|
>
|
|
<RefreshCw className="h-4 w-4 mr-2" />
|
|
Retry
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const totalBalance =
|
|
accounts?.reduce((sum, account) => {
|
|
// Get the first available balance from the balances array
|
|
const primaryBalance = account.balances?.[0]?.amount || 0;
|
|
return sum + primaryBalance;
|
|
}, 0) || 0;
|
|
const totalAccounts = accounts?.length || 0;
|
|
const uniqueBanks = new Set(accounts?.map((acc) => acc.institution_id) || [])
|
|
.size;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Summary Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-600">Total Balance</p>
|
|
<p className="text-2xl font-bold text-gray-900">
|
|
{formatCurrency(totalBalance)}
|
|
</p>
|
|
</div>
|
|
<div className="p-3 bg-green-100 rounded-full">
|
|
<TrendingUp className="h-6 w-6 text-green-600" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-600">
|
|
Total Accounts
|
|
</p>
|
|
<p className="text-2xl font-bold text-gray-900">
|
|
{totalAccounts}
|
|
</p>
|
|
</div>
|
|
<div className="p-3 bg-blue-100 rounded-full">
|
|
<CreditCard className="h-6 w-6 text-blue-600" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-600">
|
|
Connected Banks
|
|
</p>
|
|
<p className="text-2xl font-bold text-gray-900">{uniqueBanks}</p>
|
|
</div>
|
|
<div className="p-3 bg-purple-100 rounded-full">
|
|
<Building2 className="h-6 w-6 text-purple-600" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Accounts List */}
|
|
<div className="bg-white rounded-lg shadow">
|
|
<div className="px-6 py-4 border-b border-gray-200">
|
|
<h3 className="text-lg font-medium text-gray-900">Bank Accounts</h3>
|
|
<p className="text-sm text-gray-600">
|
|
Manage your connected bank accounts
|
|
</p>
|
|
</div>
|
|
|
|
{!accounts || accounts.length === 0 ? (
|
|
<div className="p-6 text-center">
|
|
<CreditCard className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
|
No accounts found
|
|
</h3>
|
|
<p className="text-gray-600">
|
|
Connect your first bank account to get started with Leggen.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="divide-y divide-gray-200">
|
|
{accounts.map((account) => {
|
|
// Get balance from account's balances array or fallback to balances query
|
|
const accountBalance = account.balances?.[0];
|
|
const fallbackBalance = balances?.find(
|
|
(b) => b.account_id === account.id,
|
|
);
|
|
const balance =
|
|
accountBalance?.amount || fallbackBalance?.balance_amount || 0;
|
|
const currency =
|
|
accountBalance?.currency ||
|
|
fallbackBalance?.currency ||
|
|
account.currency ||
|
|
"EUR";
|
|
const isPositive = balance >= 0;
|
|
|
|
return (
|
|
<div
|
|
key={account.id}
|
|
className="p-6 hover:bg-gray-50 transition-colors"
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-4">
|
|
<div className="p-3 bg-gray-100 rounded-full">
|
|
<Building2 className="h-6 w-6 text-gray-600" />
|
|
</div>
|
|
<div className="flex-1">
|
|
{editingAccountId === account.id ? (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center space-x-2">
|
|
<input
|
|
type="text"
|
|
value={editingName}
|
|
onChange={(e) => setEditingName(e.target.value)}
|
|
className="flex-1 px-3 py-1 text-lg font-medium border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
placeholder="Account name"
|
|
name="search"
|
|
autoComplete="off"
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") handleEditSave();
|
|
if (e.key === "Escape") handleEditCancel();
|
|
}}
|
|
autoFocus
|
|
/>
|
|
<button
|
|
onClick={handleEditSave}
|
|
disabled={!editingName.trim() || updateAccountMutation.isPending}
|
|
className="p-1 text-green-600 hover:text-green-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
title="Save changes"
|
|
>
|
|
<Check className="h-4 w-4" />
|
|
</button>
|
|
<button
|
|
onClick={handleEditCancel}
|
|
className="p-1 text-gray-600 hover:text-gray-700"
|
|
title="Cancel editing"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
<p className="text-sm text-gray-600">
|
|
{account.institution_id} • {account.status}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div>
|
|
<div className="flex items-center space-x-2">
|
|
<h4 className="text-lg font-medium text-gray-900">
|
|
{account.name || "Unnamed Account"}
|
|
</h4>
|
|
<button
|
|
onClick={() => handleEditStart(account)}
|
|
className="p-1 text-gray-400 hover:text-gray-600 transition-colors"
|
|
title="Edit account name"
|
|
>
|
|
<Edit2 className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
<p className="text-sm text-gray-600">
|
|
{account.institution_id} • {account.status}
|
|
</p>
|
|
{account.iban && (
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
IBAN: {account.iban}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-right">
|
|
<div className="flex items-center space-x-2">
|
|
{isPositive ? (
|
|
<TrendingUp className="h-4 w-4 text-green-500" />
|
|
) : (
|
|
<TrendingDown className="h-4 w-4 text-red-500" />
|
|
)}
|
|
<p
|
|
className={`text-lg font-semibold ${
|
|
isPositive ? "text-green-600" : "text-red-600"
|
|
}`}
|
|
>
|
|
{formatCurrency(balance, currency)}
|
|
</p>
|
|
</div>
|
|
<p className="text-sm text-gray-500">
|
|
Updated{" "}
|
|
{formatDate(account.last_accessed || account.created)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|