mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-29 00:19:08 +00:00
Compare commits
8 Commits
2025.9.11
...
93137f9998
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93137f9998 | ||
|
|
9e68810239 | ||
|
|
54e4c5b43c | ||
|
|
bfb5a7ef76 | ||
|
|
95b3b93a8a | ||
|
|
9a2199873c | ||
|
|
82a12dadad | ||
|
|
33a7ad5ad2 |
@@ -1,4 +1,10 @@
|
||||
|
||||
## 2025.9.12 (2025/09/15)
|
||||
|
||||
|
||||
## 2025.9.12 (2025/09/15)
|
||||
|
||||
|
||||
## 2025.9.11 (2025/09/15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
36
frontend/package-lock.json
generated
36
frontend/package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
@@ -29,6 +30,7 @@
|
||||
"react-day-picker": "^9.10.0",
|
||||
"react-dom": "^19.1.1",
|
||||
"recharts": "^3.2.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
@@ -1637,6 +1639,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-progress": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz",
|
||||
"integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-select": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
|
||||
@@ -5765,6 +5791,16 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/sonner": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
|
||||
"integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.7.6",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
@@ -31,6 +32,7 @@
|
||||
"react-day-picker": "^9.10.0",
|
||||
"react-dom": "^19.1.1",
|
||||
"recharts": "^3.2.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
|
||||
@@ -37,23 +37,23 @@ const getStatusIndicator = (status: string) => {
|
||||
};
|
||||
case "pending":
|
||||
return {
|
||||
color: "bg-yellow-500",
|
||||
color: "bg-amber-500",
|
||||
tooltip: "Pending",
|
||||
};
|
||||
case "error":
|
||||
case "failed":
|
||||
return {
|
||||
color: "bg-red-500",
|
||||
color: "bg-destructive",
|
||||
tooltip: "Error",
|
||||
};
|
||||
case "inactive":
|
||||
return {
|
||||
color: "bg-gray-500",
|
||||
color: "bg-muted-foreground",
|
||||
tooltip: "Inactive",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
color: "bg-blue-500",
|
||||
color: "bg-primary",
|
||||
tooltip: status,
|
||||
};
|
||||
}
|
||||
@@ -81,8 +81,8 @@ export default function AccountsOverview() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const updateAccountMutation = useMutation({
|
||||
mutationFn: ({ id, name }: { id: string; name: string }) =>
|
||||
apiClient.updateAccount(id, { name }),
|
||||
mutationFn: ({ id, display_name }: { id: string; display_name: string }) =>
|
||||
apiClient.updateAccount(id, { display_name }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["accounts"] });
|
||||
setEditingAccountId(null);
|
||||
@@ -95,14 +95,15 @@ export default function AccountsOverview() {
|
||||
|
||||
const handleEditStart = (account: Account) => {
|
||||
setEditingAccountId(account.id);
|
||||
setEditingName(account.name || "");
|
||||
// Use display_name if available, otherwise fall back to name
|
||||
setEditingName(account.display_name || account.name || "");
|
||||
};
|
||||
|
||||
const handleEditSave = () => {
|
||||
if (editingAccountId && editingName.trim()) {
|
||||
updateAccountMutation.mutate({
|
||||
id: editingAccountId,
|
||||
name: editingName.trim(),
|
||||
display_name: editingName.trim(),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -200,8 +201,8 @@ export default function AccountsOverview() {
|
||||
{uniqueBanks}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-purple-100 dark:bg-purple-900/20 rounded-full">
|
||||
<Building2 className="h-6 w-6 text-purple-600" />
|
||||
<div className="p-3 bg-muted rounded-full">
|
||||
<Building2 className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -267,7 +268,7 @@ export default function AccountsOverview() {
|
||||
setEditingName(e.target.value)
|
||||
}
|
||||
className="flex-1 px-3 py-1 text-base sm:text-lg font-medium border border-input rounded-md bg-background focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring"
|
||||
placeholder="Account name"
|
||||
placeholder="Custom account name"
|
||||
name="search"
|
||||
autoComplete="off"
|
||||
onKeyDown={(e) => {
|
||||
@@ -303,7 +304,7 @@ export default function AccountsOverview() {
|
||||
<div>
|
||||
<div className="flex items-center space-x-2 min-w-0">
|
||||
<h4 className="text-base sm:text-lg font-medium text-foreground truncate">
|
||||
{account.name || "Unnamed Account"}
|
||||
{account.display_name || account.name || "Unnamed Account"}
|
||||
</h4>
|
||||
<button
|
||||
onClick={() => handleEditStart(account)}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Component } from "react";
|
||||
import type { ErrorInfo, ReactNode } from "react";
|
||||
import { AlertTriangle, RefreshCw } from "lucide-react";
|
||||
import { Card, CardContent } from "./ui/card";
|
||||
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
@@ -39,46 +42,49 @@ class ErrorBoundary extends Component<Props, State> {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-center text-center">
|
||||
<div>
|
||||
<AlertTriangle className="h-12 w-12 text-red-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
Something went wrong
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-4">
|
||||
An error occurred while rendering this component. Please try
|
||||
refreshing or check the console for more details.
|
||||
</p>
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-center text-center">
|
||||
<div>
|
||||
<AlertTriangle className="h-12 w-12 text-destructive mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||
Something went wrong
|
||||
</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
An error occurred while rendering this component. Please try
|
||||
refreshing or check the console for more details.
|
||||
</p>
|
||||
|
||||
{this.state.error && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-md p-3 mb-4 text-left">
|
||||
<p className="text-sm font-mono text-red-800">
|
||||
<strong>Error:</strong> {this.state.error.message}
|
||||
</p>
|
||||
{this.state.error.stack && (
|
||||
<details className="mt-2">
|
||||
<summary className="text-sm text-red-600 cursor-pointer">
|
||||
Stack trace
|
||||
</summary>
|
||||
<pre className="text-xs text-red-700 mt-1 whitespace-pre-wrap">
|
||||
{this.state.error.stack}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{this.state.error && (
|
||||
<Alert variant="destructive" className="mb-4 text-left">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>Error Details</AlertTitle>
|
||||
<AlertDescription className="space-y-2">
|
||||
<p className="text-sm font-mono">
|
||||
<strong>Error:</strong> {this.state.error.message}
|
||||
</p>
|
||||
{this.state.error.stack && (
|
||||
<details className="mt-2">
|
||||
<summary className="text-sm cursor-pointer">
|
||||
Stack trace
|
||||
</summary>
|
||||
<pre className="text-xs mt-1 whitespace-pre-wrap">
|
||||
{this.state.error.stack}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={this.handleReset}
|
||||
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" />
|
||||
Try Again
|
||||
</button>
|
||||
<Button onClick={this.handleReset}>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +1,32 @@
|
||||
import { Skeleton } from "./ui/skeleton";
|
||||
import { Card, CardContent } from "./ui/card";
|
||||
|
||||
export default function FiltersSkeleton() {
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow animate-pulse">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<Card>
|
||||
<div className="px-6 py-4 border-b border-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="h-6 bg-gray-200 rounded w-32"></div>
|
||||
<Skeleton className="h-6 w-32" />
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="h-8 bg-gray-200 rounded w-24"></div>
|
||||
<div className="h-8 bg-gray-200 rounded w-20"></div>
|
||||
<Skeleton className="h-8 w-24" />
|
||||
<Skeleton className="h-8 w-20" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
||||
<CardContent className="px-6 py-4 border-b border-border bg-muted/30">
|
||||
{/* Quick Date Filters Skeleton */}
|
||||
<div className="mb-6">
|
||||
<div className="h-4 bg-gray-200 rounded w-32 mb-3"></div>
|
||||
<Skeleton className="h-4 w-32 mb-3" />
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="h-10 bg-gray-200 rounded-lg w-24"></div>
|
||||
<div className="h-10 bg-gray-200 rounded-lg w-20"></div>
|
||||
<div className="h-10 bg-gray-200 rounded-lg w-28"></div>
|
||||
<Skeleton className="h-10 w-24 rounded-lg" />
|
||||
<Skeleton className="h-10 w-20 rounded-lg" />
|
||||
<Skeleton className="h-10 w-28 rounded-lg" />
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="h-10 bg-gray-200 rounded-lg w-24"></div>
|
||||
<div className="h-10 bg-gray-200 rounded-lg w-20"></div>
|
||||
<Skeleton className="h-10 w-24 rounded-lg" />
|
||||
<Skeleton className="h-10 w-20 rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -31,40 +34,40 @@ export default function FiltersSkeleton() {
|
||||
{/* Filter Fields Skeleton */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="sm:col-span-2 lg:col-span-1">
|
||||
<div className="h-4 bg-gray-200 rounded w-16 mb-1"></div>
|
||||
<div className="h-10 bg-gray-200 rounded"></div>
|
||||
<Skeleton className="h-4 w-16 mb-1" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="h-4 bg-gray-200 rounded w-16 mb-1"></div>
|
||||
<div className="h-10 bg-gray-200 rounded"></div>
|
||||
<Skeleton className="h-4 w-16 mb-1" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="h-4 bg-gray-200 rounded w-20 mb-1"></div>
|
||||
<div className="h-10 bg-gray-200 rounded"></div>
|
||||
<Skeleton className="h-4 w-20 mb-1" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="h-4 bg-gray-200 rounded w-16 mb-1"></div>
|
||||
<div className="h-10 bg-gray-200 rounded"></div>
|
||||
<Skeleton className="h-4 w-16 mb-1" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Amount Range Filters Skeleton */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4">
|
||||
<div>
|
||||
<div className="h-4 bg-gray-200 rounded w-20 mb-1"></div>
|
||||
<div className="h-10 bg-gray-200 rounded"></div>
|
||||
<Skeleton className="h-4 w-20 mb-1" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="h-4 bg-gray-200 rounded w-20 mb-1"></div>
|
||||
<div className="h-10 bg-gray-200 rounded"></div>
|
||||
<Skeleton className="h-4 w-20 mb-1" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
{/* Results Summary Skeleton */}
|
||||
<div className="px-6 py-3 bg-gray-50 border-b border-gray-200">
|
||||
<div className="h-4 bg-gray-200 rounded w-48"></div>
|
||||
</div>
|
||||
</div>
|
||||
<CardContent className="px-6 py-3 bg-muted/30 border-b border-border">
|
||||
<Skeleton className="h-4 w-48" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,15 +49,15 @@ export default function Header({ setSidebarOpen }: HeaderProps) {
|
||||
<div className="flex items-center space-x-1">
|
||||
{healthLoading ? (
|
||||
<>
|
||||
<Activity className="h-4 w-4 text-yellow-500 animate-pulse" />
|
||||
<Activity className="h-4 w-4 text-muted-foreground animate-pulse" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Checking...
|
||||
</span>
|
||||
</>
|
||||
) : healthError || healthStatus?.status !== "healthy" ? (
|
||||
<>
|
||||
<WifiOff className="h-4 w-4 text-red-500" />
|
||||
<span className="text-sm text-red-500">Disconnected</span>
|
||||
<WifiOff className="h-4 w-4 text-destructive" />
|
||||
<span className="text-sm text-destructive">Disconnected</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
interface LoadingSpinnerProps {
|
||||
message?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function LoadingSpinner({
|
||||
message = "Loading...",
|
||||
className,
|
||||
}: LoadingSpinnerProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className={cn("flex items-center justify-center p-8", className)}>
|
||||
<div className="text-center">
|
||||
<RefreshCw className="h-8 w-8 animate-spin text-blue-600 mx-auto mb-2" />
|
||||
<p className="text-gray-600 text-sm">{message}</p>
|
||||
<RefreshCw className="h-8 w-8 animate-spin text-primary mx-auto mb-2" />
|
||||
<p className="text-muted-foreground text-sm">{message}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -24,6 +24,7 @@ import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
import { Label } from "./ui/label";
|
||||
import { Badge } from "./ui/badge";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -233,12 +234,8 @@ export default function Notifications() {
|
||||
{service.name}
|
||||
</h4>
|
||||
<div className="flex items-center space-x-2 mt-1">
|
||||
<span
|
||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
service.enabled
|
||||
? "bg-green-100 text-green-800"
|
||||
: "bg-red-100 text-red-800"
|
||||
}`}
|
||||
<Badge
|
||||
variant={service.enabled ? "default" : "destructive"}
|
||||
>
|
||||
{service.enabled ? (
|
||||
<CheckCircle className="h-3 w-3 mr-1" />
|
||||
@@ -246,18 +243,14 @@ export default function Notifications() {
|
||||
<AlertCircle className="h-3 w-3 mr-1" />
|
||||
)}
|
||||
{service.enabled ? "Enabled" : "Disabled"}
|
||||
</span>
|
||||
<span
|
||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
service.configured
|
||||
? "bg-blue-100 text-blue-800"
|
||||
: "bg-yellow-100 text-yellow-800"
|
||||
}`}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant={service.configured ? "secondary" : "outline"}
|
||||
>
|
||||
{service.configured
|
||||
? "Configured"
|
||||
: "Not Configured"}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { Skeleton } from "./ui/skeleton";
|
||||
import { Card } from "./ui/card";
|
||||
|
||||
interface TransactionSkeletonProps {
|
||||
rows?: number;
|
||||
view?: "table" | "mobile";
|
||||
@@ -11,93 +14,89 @@ export default function TransactionSkeleton({
|
||||
|
||||
if (view === "mobile") {
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow divide-y divide-gray-200">
|
||||
<Card className="divide-y divide-border">
|
||||
{skeletonRows.map((_, index) => (
|
||||
<div key={index} className="p-4 animate-pulse">
|
||||
<div key={index} className="p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="p-2 rounded-full bg-gray-200 flex-shrink-0">
|
||||
<div className="h-4 w-4 bg-gray-300 rounded"></div>
|
||||
</div>
|
||||
<Skeleton className="h-10 w-10 rounded-full flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0 space-y-2">
|
||||
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<div className="space-y-1">
|
||||
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
|
||||
<div className="h-3 bg-gray-200 rounded w-2/3"></div>
|
||||
<div className="h-3 bg-gray-200 rounded w-1/3"></div>
|
||||
<Skeleton className="h-3 w-1/2" />
|
||||
<Skeleton className="h-3 w-2/3" />
|
||||
<Skeleton className="h-3 w-1/3" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right ml-3 flex-shrink-0 space-y-2">
|
||||
<div className="h-6 bg-gray-200 rounded w-20"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-16 ml-auto"></div>
|
||||
<div className="h-6 bg-gray-200 rounded w-12 ml-auto"></div>
|
||||
<Skeleton className="h-6 w-20" />
|
||||
<Skeleton className="h-4 w-16 ml-auto" />
|
||||
<Skeleton className="h-6 w-12 ml-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
||||
<Card className="overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<table className="min-w-full divide-y divide-border">
|
||||
<thead className="bg-muted/50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left">
|
||||
<div className="h-4 bg-gray-200 rounded w-20 animate-pulse"></div>
|
||||
<Skeleton className="h-4 w-20" />
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left">
|
||||
<div className="h-4 bg-gray-200 rounded w-16 animate-pulse"></div>
|
||||
<Skeleton className="h-4 w-16" />
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left">
|
||||
<div className="h-4 bg-gray-200 rounded w-12 animate-pulse"></div>
|
||||
<Skeleton className="h-4 w-12" />
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left">
|
||||
<div className="h-4 bg-gray-200 rounded w-8 animate-pulse"></div>
|
||||
<Skeleton className="h-4 w-8" />
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
<tbody className="bg-card divide-y divide-border">
|
||||
{skeletonRows.map((_, index) => (
|
||||
<tr key={index} className="animate-pulse">
|
||||
<tr key={index}>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="p-2 rounded-full bg-gray-200 flex-shrink-0">
|
||||
<div className="h-4 w-4 bg-gray-300 rounded"></div>
|
||||
</div>
|
||||
<Skeleton className="h-10 w-10 rounded-full flex-shrink-0" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<div className="space-y-1">
|
||||
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
|
||||
<div className="h-3 bg-gray-200 rounded w-2/3"></div>
|
||||
<Skeleton className="h-3 w-1/2" />
|
||||
<Skeleton className="h-3 w-2/3" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="text-right">
|
||||
<div className="h-6 bg-gray-200 rounded w-24 ml-auto mb-1"></div>
|
||||
<Skeleton className="h-6 w-24 ml-auto mb-1" />
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="space-y-1">
|
||||
<div className="h-4 bg-gray-200 rounded w-20"></div>
|
||||
<div className="h-3 bg-gray-200 rounded w-16"></div>
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-3 w-16" />
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="h-6 bg-gray-200 rounded w-12"></div>
|
||||
<Skeleton className="h-6 w-12" />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export function AccountCombobox({
|
||||
);
|
||||
|
||||
const formatAccountName = (account: Account) => {
|
||||
const displayName = account.name || "Unnamed Account";
|
||||
const displayName = account.display_name || account.name || "Unnamed Account";
|
||||
return `${displayName} (${account.institution_id})`;
|
||||
};
|
||||
|
||||
@@ -89,7 +89,7 @@ export function AccountCombobox({
|
||||
{accounts.map((account) => (
|
||||
<CommandItem
|
||||
key={account.id}
|
||||
value={`${account.name} ${account.institution_id}`}
|
||||
value={`${account.display_name || account.name} ${account.institution_id}`}
|
||||
onSelect={() => {
|
||||
onAccountChange(account.id);
|
||||
setOpen(false);
|
||||
@@ -105,7 +105,7 @@ export function AccountCombobox({
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">
|
||||
{account.name || "Unnamed Account"}
|
||||
{account.display_name || account.name || "Unnamed Account"}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{account.institution_id}
|
||||
|
||||
26
frontend/src/components/ui/progress.tsx
Normal file
26
frontend/src/components/ui/progress.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as React from "react";
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative h-2 w-full overflow-hidden rounded-full bg-secondary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
));
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName;
|
||||
|
||||
export { Progress };
|
||||
138
frontend/src/components/ui/sheet.tsx
Normal file
138
frontend/src/components/ui/sheet.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import * as React from "react";
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Sheet = SheetPrimitive.Root;
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger;
|
||||
|
||||
const SheetClose = SheetPrimitive.Close;
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal;
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = "right", className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
));
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetHeader.displayName = "SheetHeader";
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetFooter.displayName = "SheetFooter";
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
};
|
||||
15
frontend/src/components/ui/skeleton.tsx
Normal file
15
frontend/src/components/ui/skeleton.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton };
|
||||
27
frontend/src/components/ui/sonner.tsx
Normal file
27
frontend/src/components/ui/sonner.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { Toaster as Sonner } from "sonner";
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>;
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
return (
|
||||
<Sonner
|
||||
className="toaster group"
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Toaster };
|
||||
@@ -41,8 +41,8 @@ export const apiClient = {
|
||||
updateAccount: async (
|
||||
id: string,
|
||||
updates: AccountUpdate,
|
||||
): Promise<{ id: string; name?: string }> => {
|
||||
const response = await api.put<ApiResponse<{ id: string; name?: string }>>(
|
||||
): Promise<{ id: string; display_name?: string }> => {
|
||||
const response = await api.put<ApiResponse<{ id: string; display_name?: string }>>(
|
||||
`/accounts/${id}`,
|
||||
updates,
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface Account {
|
||||
status: string;
|
||||
iban?: string;
|
||||
name?: string;
|
||||
display_name?: string;
|
||||
currency?: string;
|
||||
created: string;
|
||||
last_accessed?: string;
|
||||
@@ -18,7 +19,7 @@ export interface Account {
|
||||
}
|
||||
|
||||
export interface AccountUpdate {
|
||||
name?: string;
|
||||
display_name?: string;
|
||||
}
|
||||
|
||||
export interface RawTransactionData {
|
||||
|
||||
@@ -24,6 +24,7 @@ class AccountDetails(BaseModel):
|
||||
status: str
|
||||
iban: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
display_name: Optional[str] = None
|
||||
currency: Optional[str] = None
|
||||
created: datetime
|
||||
last_accessed: Optional[datetime] = None
|
||||
@@ -36,7 +37,7 @@ class AccountDetails(BaseModel):
|
||||
class AccountUpdate(BaseModel):
|
||||
"""Account update model"""
|
||||
|
||||
name: Optional[str] = None
|
||||
display_name: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
json_encoders = {datetime: lambda v: v.isoformat() if v else None}
|
||||
|
||||
@@ -53,6 +53,7 @@ async def get_all_accounts() -> APIResponse:
|
||||
status=db_account["status"],
|
||||
iban=db_account.get("iban"),
|
||||
name=db_account.get("name"),
|
||||
display_name=db_account.get("display_name"),
|
||||
currency=db_account.get("currency"),
|
||||
created=db_account["created"],
|
||||
last_accessed=db_account.get("last_accessed"),
|
||||
@@ -112,6 +113,7 @@ async def get_account_details(account_id: str) -> APIResponse:
|
||||
status=db_account["status"],
|
||||
iban=db_account.get("iban"),
|
||||
name=db_account.get("name"),
|
||||
display_name=db_account.get("display_name"),
|
||||
currency=db_account.get("currency"),
|
||||
created=db_account["created"],
|
||||
last_accessed=db_account.get("last_accessed"),
|
||||
@@ -324,7 +326,7 @@ async def get_account_transactions(
|
||||
async def update_account_details(
|
||||
account_id: str, update_data: AccountUpdate
|
||||
) -> APIResponse:
|
||||
"""Update account details (currently only name)"""
|
||||
"""Update account details (currently only display_name)"""
|
||||
try:
|
||||
# Get current account details
|
||||
current_account = await database_service.get_account_details_from_db(account_id)
|
||||
@@ -336,16 +338,16 @@ async def update_account_details(
|
||||
|
||||
# Prepare updated account data
|
||||
updated_account_data = current_account.copy()
|
||||
if update_data.name is not None:
|
||||
updated_account_data["name"] = update_data.name
|
||||
if update_data.display_name is not None:
|
||||
updated_account_data["display_name"] = update_data.display_name
|
||||
|
||||
# Persist updated account details
|
||||
await database_service.persist_account_details(updated_account_data)
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data={"id": account_id, "name": update_data.name},
|
||||
message=f"Account {account_id} name updated successfully",
|
||||
data={"id": account_id, "display_name": update_data.display_name},
|
||||
message=f"Account {account_id} display name updated successfully",
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
|
||||
@@ -215,6 +215,7 @@ class DatabaseService:
|
||||
await self._migrate_balance_timestamps_if_needed()
|
||||
await self._migrate_null_transaction_ids_if_needed()
|
||||
await self._migrate_to_composite_key_if_needed()
|
||||
await self._migrate_add_display_name_if_needed()
|
||||
|
||||
async def _migrate_balance_timestamps_if_needed(self):
|
||||
"""Check and migrate balance timestamps if needed"""
|
||||
@@ -632,6 +633,79 @@ class DatabaseService:
|
||||
logger.error(f"Composite key migration failed: {e}")
|
||||
raise
|
||||
|
||||
async def _migrate_add_display_name_if_needed(self):
|
||||
"""Check and add display_name column to accounts table if needed"""
|
||||
try:
|
||||
if await self._check_display_name_migration_needed():
|
||||
logger.info("Display name column migration needed, starting...")
|
||||
await self._migrate_add_display_name()
|
||||
logger.info("Display name column migration completed")
|
||||
else:
|
||||
logger.info("Display name column already exists")
|
||||
except Exception as e:
|
||||
logger.error(f"Display name column migration failed: {e}")
|
||||
raise
|
||||
|
||||
async def _check_display_name_migration_needed(self) -> bool:
|
||||
"""Check if display_name column needs to be added to accounts table"""
|
||||
db_path = path_manager.get_database_path()
|
||||
if not db_path.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if accounts table exists
|
||||
cursor.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='accounts'"
|
||||
)
|
||||
if not cursor.fetchone():
|
||||
conn.close()
|
||||
return False
|
||||
|
||||
# Check if display_name column exists
|
||||
cursor.execute("PRAGMA table_info(accounts)")
|
||||
columns = cursor.fetchall()
|
||||
|
||||
# Check if display_name column exists
|
||||
has_display_name = any(col[1] == "display_name" for col in columns)
|
||||
|
||||
conn.close()
|
||||
return not has_display_name
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to check display_name migration status: {e}")
|
||||
return False
|
||||
|
||||
async def _migrate_add_display_name(self):
|
||||
"""Add display_name column to accounts table"""
|
||||
db_path = path_manager.get_database_path()
|
||||
if not db_path.exists():
|
||||
logger.warning("Database file not found, skipping migration")
|
||||
return
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
cursor = conn.cursor()
|
||||
|
||||
logger.info("Adding display_name column to accounts table...")
|
||||
|
||||
# Add the display_name column
|
||||
cursor.execute("""
|
||||
ALTER TABLE accounts
|
||||
ADD COLUMN display_name TEXT
|
||||
""")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
logger.info("Display name column migration completed successfully")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Display name column migration failed: {e}")
|
||||
raise
|
||||
|
||||
def _unix_to_datetime_string(self, unix_timestamp: float) -> str:
|
||||
"""Convert Unix timestamp to datetime string"""
|
||||
dt = datetime.fromtimestamp(unix_timestamp)
|
||||
@@ -1045,7 +1119,8 @@ class DatabaseService:
|
||||
currency TEXT,
|
||||
created DATETIME,
|
||||
last_accessed DATETIME,
|
||||
last_updated DATETIME
|
||||
last_updated DATETIME,
|
||||
display_name TEXT
|
||||
)"""
|
||||
)
|
||||
|
||||
@@ -1060,6 +1135,16 @@ class DatabaseService:
|
||||
)
|
||||
|
||||
try:
|
||||
# First, check if account exists and preserve display_name
|
||||
cursor.execute(
|
||||
"SELECT display_name FROM accounts WHERE id = ?", (account_data["id"],)
|
||||
)
|
||||
existing_row = cursor.fetchone()
|
||||
existing_display_name = existing_row[0] if existing_row else None
|
||||
|
||||
# Use existing display_name if not provided in account_data
|
||||
display_name = account_data.get("display_name", existing_display_name)
|
||||
|
||||
# Insert or replace account data
|
||||
cursor.execute(
|
||||
"""INSERT OR REPLACE INTO accounts (
|
||||
@@ -1071,8 +1156,9 @@ class DatabaseService:
|
||||
currency,
|
||||
created,
|
||||
last_accessed,
|
||||
last_updated
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
last_updated,
|
||||
display_name
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
(
|
||||
account_data["id"],
|
||||
account_data["institution_id"],
|
||||
@@ -1083,6 +1169,7 @@ class DatabaseService:
|
||||
account_data["created"],
|
||||
account_data.get("last_accessed"),
|
||||
account_data.get("last_updated", account_data["created"]),
|
||||
display_name,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "leggen"
|
||||
version = "2025.9.11"
|
||||
version = "2025.9.12"
|
||||
description = "An Open Banking CLI"
|
||||
authors = [{ name = "Elisiário Couto", email = "elisiario@couto.io" }]
|
||||
requires-python = "~=3.13.0"
|
||||
|
||||
@@ -106,7 +106,8 @@ class SampleDataGenerator:
|
||||
currency TEXT,
|
||||
created DATETIME,
|
||||
last_accessed DATETIME,
|
||||
last_updated DATETIME
|
||||
last_updated DATETIME,
|
||||
display_name TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
@@ -373,8 +374,8 @@ class SampleDataGenerator:
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT OR REPLACE INTO accounts
|
||||
(id, institution_id, status, iban, name, currency, created, last_accessed, last_updated)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
(id, institution_id, status, iban, name, currency, created, last_accessed, last_updated, display_name)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
account["id"],
|
||||
@@ -386,6 +387,7 @@ class SampleDataGenerator:
|
||||
account["created"],
|
||||
account["last_accessed"],
|
||||
account["last_updated"],
|
||||
None, # display_name is initially None for sample data
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ class TestAccountsAPI:
|
||||
"institution_id": "REVOLUT_REVOLT21",
|
||||
"status": "READY",
|
||||
"iban": "LT313250081177977789",
|
||||
"name": "Personal Account",
|
||||
"display_name": None,
|
||||
"created": "2024-02-13T23:56:00Z",
|
||||
"last_accessed": "2025-09-01T09:30:00Z",
|
||||
}
|
||||
@@ -80,6 +82,8 @@ class TestAccountsAPI:
|
||||
"institution_id": "REVOLUT_REVOLT21",
|
||||
"status": "READY",
|
||||
"iban": "LT313250081177977789",
|
||||
"name": "Personal Account",
|
||||
"display_name": None,
|
||||
"created": "2024-02-13T23:56:00Z",
|
||||
"last_accessed": "2025-09-01T09:30:00Z",
|
||||
}
|
||||
@@ -283,3 +287,58 @@ class TestAccountsAPI:
|
||||
response = api_client.get("/api/v1/accounts/nonexistent")
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_update_account_display_name_success(
|
||||
self, api_client, mock_config, mock_auth_token, mock_db_path
|
||||
):
|
||||
"""Test successful update of account display name."""
|
||||
mock_account = {
|
||||
"id": "test-account-123",
|
||||
"institution_id": "REVOLUT_REVOLT21",
|
||||
"status": "READY",
|
||||
"iban": "LT313250081177977789",
|
||||
"name": "Personal Account",
|
||||
"display_name": None,
|
||||
"created": "2024-02-13T23:56:00Z",
|
||||
"last_accessed": "2025-09-01T09:30:00Z",
|
||||
}
|
||||
|
||||
with (
|
||||
patch("leggen.utils.config.config", mock_config),
|
||||
patch(
|
||||
"leggen.api.routes.accounts.database_service.get_account_details_from_db",
|
||||
return_value=mock_account,
|
||||
),
|
||||
patch(
|
||||
"leggen.api.routes.accounts.database_service.persist_account_details",
|
||||
return_value=None,
|
||||
),
|
||||
):
|
||||
response = api_client.put(
|
||||
"/api/v1/accounts/test-account-123",
|
||||
json={"display_name": "My Custom Account Name"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["data"]["id"] == "test-account-123"
|
||||
assert data["data"]["display_name"] == "My Custom Account Name"
|
||||
|
||||
def test_update_account_not_found(
|
||||
self, api_client, mock_config, mock_auth_token, mock_db_path
|
||||
):
|
||||
"""Test updating non-existent account."""
|
||||
with (
|
||||
patch("leggen.utils.config.config", mock_config),
|
||||
patch(
|
||||
"leggen.api.routes.accounts.database_service.get_account_details_from_db",
|
||||
return_value=None,
|
||||
),
|
||||
):
|
||||
response = api_client.put(
|
||||
"/api/v1/accounts/nonexistent",
|
||||
json={"display_name": "New Name"},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
Reference in New Issue
Block a user