Compare commits

...

5 Commits

Author SHA1 Message Date
Elisiário Couto
b7d6cf8128 chore(ci): Bump version to 2025.9.16 2025-09-18 23:29:53 +01:00
copilot-swe-agent[bot]
6589c2dd66 fix(frontend): Add iOS safe area support for PWA sticky header
Co-authored-by: elisiariocouto <818914+elisiariocouto@users.noreply.github.com>
2025-09-18 23:28:49 +01:00
Elisiário Couto
571072f6ac chore(ci): Bump version to 2025.9.15 2025-09-18 23:03:01 +01:00
Elisiário Couto
be4f7f8cec refactor(frontend): Simplify filter bar UI and remove advanced filters popover.
🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-18 23:01:51 +01:00
Elisiário Couto
056c33b9c5 feat(frontend): Add settings page with account management functionality.
Added comprehensive settings page with account settings component, integrated with existing layout and routing
structure. Updated project documentation with frontend architecture details.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>")
2025-09-18 22:19:36 +01:00
22 changed files with 3312 additions and 555 deletions

View File

@@ -85,6 +85,29 @@ The command outputs instructions for setting the required environment variable t
- **Data fetching**: @tanstack/react-query with proper error handling - **Data fetching**: @tanstack/react-query with proper error handling
- **Components**: Functional components with hooks, proper TypeScript typing - **Components**: Functional components with hooks, proper TypeScript typing
## Frontend Structure
### Layout Architecture
- **Root Layout**: `frontend/src/routes/__root.tsx` - Contains main app structure with Sidebar and Header
- **Header/Navbar**: `frontend/src/components/Header.tsx` - Top navigation bar (sticky on mobile only)
- **Sidebar**: `frontend/src/components/Sidebar.tsx` - Left navigation sidebar
- **Routes**: `frontend/src/routes/` - TanStack Router file-based routing
### Key Components Location
- **UI Components**: `frontend/src/components/ui/` - Reusable UI primitives
- **Feature Components**: `frontend/src/components/` - Main app components
- **Pages**: `frontend/src/routes/` - Route components (index.tsx, transactions.tsx, etc.)
- **Hooks**: `frontend/src/hooks/` - Custom React hooks
- **API**: `frontend/src/lib/api.ts` - API client configuration
- **Context**: `frontend/src/contexts/` - React contexts (ThemeContext, etc.)
### Routing Structure
- `/` - Overview/Dashboard (TransactionsTable component)
- `/transactions` - Transactions page
- `/analytics` - Analytics page
- `/notifications` - Notifications page
- `/settings` - Settings page
### General ### General
- **Formatting**: ruff for Python, ESLint for TypeScript - **Formatting**: ruff for Python, ESLint for TypeScript
- **Commits**: Use conventional commits with optional scopes, run pre-commit hooks before pushing - **Commits**: Use conventional commits with optional scopes, run pre-commit hooks before pushing

View File

@@ -1,4 +1,46 @@
## 2025.9.16 (2025/09/18)
### Bug Fixes
- **frontend:** Add iOS safe area support for PWA sticky header ([6589c2dd](https://github.com/elisiariocouto/leggen/commit/6589c2dd666f8605cf6d1bf9ad7277734d4cd302))
## 2025.9.16 (2025/09/18)
### Bug Fixes
- **frontend:** Add iOS safe area support for PWA sticky header ([6589c2dd](https://github.com/elisiariocouto/leggen/commit/6589c2dd666f8605cf6d1bf9ad7277734d4cd302))
## 2025.9.15 (2025/09/18)
### Features
- **frontend:** Add settings page with account management functionality. ([056c33b9](https://github.com/elisiariocouto/leggen/commit/056c33b9c5cfbc2842cc2dd4ca8c4e3959a2be80))
### Refactor
- **frontend:** Simplify filter bar UI and remove advanced filters popover. ([be4f7f8c](https://github.com/elisiariocouto/leggen/commit/be4f7f8cecfe2564abdf0ce1be08497e5a6d7b68))
## 2025.9.15 (2025/09/18)
### Features
- **frontend:** Add settings page with account management functionality. ([056c33b9](https://github.com/elisiariocouto/leggen/commit/056c33b9c5cfbc2842cc2dd4ca8c4e3959a2be80))
### Refactor
- **frontend:** Simplify filter bar UI and remove advanced filters popover. ([be4f7f8c](https://github.com/elisiariocouto/leggen/commit/be4f7f8cecfe2564abdf0ce1be08497e5a6d7b68))
## 2025.9.14 (2025/09/18) ## 2025.9.14 (2025/09/18)
### Bug Fixes ### Bug Fixes

File diff suppressed because it is too large Load Diff

View File

@@ -48,6 +48,7 @@
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0", "globals": "^16.3.0",
"shadcn": "^3.3.1",
"sharp": "^0.34.3", "sharp": "^0.34.3",
"typescript": "~5.8.3", "typescript": "~5.8.3",
"typescript-eslint": "^8.39.1", "typescript-eslint": "^8.39.1",

View File

@@ -0,0 +1,347 @@
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
CreditCard,
TrendingUp,
TrendingDown,
Building2,
RefreshCw,
AlertCircle,
Edit2,
Check,
X,
Plus,
} from "lucide-react";
import { apiClient } from "../lib/api";
import { formatCurrency, formatDate } from "../lib/utils";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "./ui/card";
import { Button } from "./ui/button";
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
import AccountsSkeleton from "./AccountsSkeleton";
import type { Account, Balance } from "../types/api";
// Helper function to get status indicator color and styles
const getStatusIndicator = (status: string) => {
const statusLower = status.toLowerCase();
switch (statusLower) {
case "ready":
return {
color: "bg-green-500",
tooltip: "Ready",
};
case "pending":
return {
color: "bg-amber-500",
tooltip: "Pending",
};
case "error":
case "failed":
return {
color: "bg-destructive",
tooltip: "Error",
};
case "inactive":
return {
color: "bg-muted-foreground",
tooltip: "Inactive",
};
default:
return {
color: "bg-primary",
tooltip: status,
};
}
};
export default function AccountSettings() {
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, display_name }: { id: string; display_name: string }) =>
apiClient.updateAccount(id, { display_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);
// 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,
display_name: editingName.trim(),
});
}
};
const handleEditCancel = () => {
setEditingAccountId(null);
setEditingName("");
};
if (accountsLoading) {
return <AccountsSkeleton />;
}
if (accountsError) {
return (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Failed to load accounts</AlertTitle>
<AlertDescription className="space-y-3">
<p>
Unable to connect to the Leggen API. Please check your configuration
and ensure the API server is running.
</p>
<Button onClick={() => refetchAccounts()} variant="outline" size="sm">
<RefreshCw className="h-4 w-4 mr-2" />
Retry
</Button>
</AlertDescription>
</Alert>
);
}
return (
<div className="space-y-6">
{/* Account Management Section */}
<Card>
<CardHeader>
<CardTitle>Account Management</CardTitle>
<CardDescription>
Manage your connected bank accounts and customize their display names
</CardDescription>
</CardHeader>
{!accounts || accounts.length === 0 ? (
<CardContent className="p-6 text-center">
<CreditCard className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
No accounts found
</h3>
<p className="text-muted-foreground mb-4">
Connect your first bank account to get started with Leggen.
</p>
<Button disabled className="flex items-center space-x-2">
<Plus className="h-4 w-4" />
<span>Add Bank Account</span>
</Button>
<p className="text-xs text-muted-foreground mt-2">
Coming soon: Add new bank connections
</p>
</CardContent>
) : (
<CardContent className="p-0">
<div className="divide-y divide-border">
{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-4 sm:p-6 hover:bg-accent transition-colors"
>
{/* Mobile layout - stack vertically */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
<div className="flex items-start sm:items-center space-x-3 sm:space-x-4 min-w-0 flex-1">
<div className="flex-shrink-0 p-2 sm:p-3 bg-muted rounded-full">
<Building2 className="h-5 w-5 sm:h-6 sm:w-6 text-muted-foreground" />
</div>
<div className="flex-1 min-w-0">
{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-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="Custom 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-muted-foreground truncate">
{account.institution_id}
</p>
</div>
) : (
<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.display_name ||
account.name ||
"Unnamed Account"}
</h4>
<button
onClick={() => handleEditStart(account)}
className="flex-shrink-0 p-1 text-muted-foreground hover:text-foreground transition-colors"
title="Edit account name"
>
<Edit2 className="h-4 w-4" />
</button>
</div>
<p className="text-sm text-muted-foreground truncate">
{account.institution_id}
</p>
{account.iban && (
<p className="text-xs text-muted-foreground mt-1 font-mono break-all sm:break-normal">
IBAN: {account.iban}
</p>
)}
</div>
)}
</div>
</div>
{/* Balance and date section */}
<div className="flex items-center justify-between sm:flex-col sm:items-end sm:text-right flex-shrink-0">
{/* Mobile: date/status on left, balance on right */}
{/* Desktop: balance on top, date/status on bottom */}
{/* Date and status indicator - left on mobile, bottom on desktop */}
<div className="flex items-center space-x-2 order-1 sm:order-2">
<div
className={`w-3 h-3 rounded-full ${getStatusIndicator(account.status).color} relative group cursor-help`}
role="img"
aria-label={`Account status: ${getStatusIndicator(account.status).tooltip}`}
>
{/* Tooltip */}
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block bg-gray-900 text-white text-xs rounded py-1 px-2 whitespace-nowrap z-10">
{getStatusIndicator(account.status).tooltip}
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-2 border-transparent border-t-gray-900"></div>
</div>
</div>
<p className="text-xs sm:text-sm text-muted-foreground whitespace-nowrap">
Updated{" "}
{formatDate(
account.last_accessed || account.created,
)}
</p>
</div>
{/* Balance - right on mobile, top on desktop */}
<div className="flex items-center space-x-2 order-2 sm:order-1">
{isPositive ? (
<TrendingUp className="h-4 w-4 text-green-500" />
) : (
<TrendingDown className="h-4 w-4 text-red-500" />
)}
<p
className={`text-base sm:text-lg font-semibold ${
isPositive ? "text-green-600" : "text-red-600"
}`}
>
{formatCurrency(balance, currency)}
</p>
</div>
</div>
</div>
</div>
);
})}
</div>
</CardContent>
)}
</Card>
{/* Add Bank Section (Future Feature) */}
<Card>
<CardHeader>
<CardTitle>Add New Bank Account</CardTitle>
<CardDescription>
Connect additional bank accounts to track all your finances in one place
</CardDescription>
</CardHeader>
<CardContent className="p-6">
<div className="text-center space-y-4">
<div className="p-4 bg-muted rounded-lg">
<Plus className="h-8 w-8 text-muted-foreground mx-auto mb-2" />
<p className="text-sm text-muted-foreground">
Bank connection functionality is coming soon. Stay tuned for updates!
</p>
</div>
<Button disabled variant="outline">
<Plus className="h-4 w-4 mr-2" />
Connect Bank Account
</Button>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -9,6 +9,7 @@ const navigation = [
{ name: "Transactions", to: "/transactions" }, { name: "Transactions", to: "/transactions" },
{ name: "Analytics", to: "/analytics" }, { name: "Analytics", to: "/analytics" },
{ name: "Notifications", to: "/notifications" }, { name: "Notifications", to: "/notifications" },
{ name: "Settings", to: "/settings" },
]; ];
interface HeaderProps { interface HeaderProps {
@@ -32,7 +33,7 @@ export default function Header({ setSidebarOpen }: HeaderProps) {
}); });
return ( return (
<header className="bg-card shadow-sm border-b border-border"> <header className="lg:static sticky top-0 z-50 bg-card shadow-sm border-b border-border pt-safe-top">
<div className="flex items-center justify-between h-16 px-6"> <div className="flex items-center justify-between h-16 px-6">
<div className="flex items-center"> <div className="flex items-center">
<button <button

View File

@@ -1,24 +1,29 @@
import { Link, useLocation } from "@tanstack/react-router"; import { Link, useLocation } from "@tanstack/react-router";
import { import {
Home,
List, List,
BarChart3, BarChart3,
Bell, Bell,
TrendingUp, TrendingUp,
X, X,
ChevronDown,
ChevronUp,
Settings,
Building2,
} from "lucide-react"; } from "lucide-react";
import { Logo } from "./ui/logo"; import { Logo } from "./ui/logo";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { apiClient } from "../lib/api"; import { apiClient } from "../lib/api";
import { formatCurrency } from "../lib/utils"; import { formatCurrency } from "../lib/utils";
import { cn } from "../lib/utils"; import { cn } from "../lib/utils";
import { useState } from "react";
import type { Account } from "../types/api"; import type { Account } from "../types/api";
const navigation = [ const navigation = [
{ name: "Overview", icon: Home, to: "/" }, { name: "Overview", icon: List, to: "/" },
{ name: "Transactions", icon: List, to: "/transactions" },
{ name: "Analytics", icon: BarChart3, to: "/analytics" }, { name: "Analytics", icon: BarChart3, to: "/analytics" },
{ name: "Notifications", icon: Bell, to: "/notifications" }, { name: "Notifications", icon: Bell, to: "/notifications" },
{ name: "Settings", icon: Settings, to: "/settings" },
]; ];
interface SidebarProps { interface SidebarProps {
@@ -28,6 +33,7 @@ interface SidebarProps {
export default function Sidebar({ sidebarOpen, setSidebarOpen }: SidebarProps) { export default function Sidebar({ sidebarOpen, setSidebarOpen }: SidebarProps) {
const location = useLocation(); const location = useLocation();
const [accountsExpanded, setAccountsExpanded] = useState(false);
const { data: accounts } = useQuery<Account[]>({ const { data: accounts } = useQuery<Account[]>({
queryKey: ["accounts"], queryKey: ["accounts"],
@@ -85,21 +91,68 @@ export default function Sidebar({ sidebarOpen, setSidebarOpen }: SidebarProps) {
</div> </div>
</nav> </nav>
{/* Account Summary in Sidebar */} {/* Collapsible Account Summary in Sidebar */}
<div className="px-6 py-4 border-t border-border mt-auto"> <div className="px-6 pt-4 pb-6 border-t border-border mt-auto">
<div className="bg-muted rounded-lg p-4"> <div className="bg-muted rounded-lg">
<div className="flex items-center justify-between"> {/* Collapsible Header */}
<span className="text-sm font-medium text-muted-foreground"> <button
Total Balance onClick={() => setAccountsExpanded(!accountsExpanded)}
</span> className="w-full p-4 flex items-center justify-between hover:bg-muted/80 transition-colors rounded-lg"
<TrendingUp className="h-4 w-4 text-green-500" /> >
<div className="flex items-center justify-between w-full">
<div className="flex items-center space-x-2">
<span className="text-sm font-medium text-muted-foreground">
Total Balance
</span>
<TrendingUp className="h-4 w-4 text-green-500" />
</div>
{accountsExpanded ? (
<ChevronUp className="h-4 w-4 text-muted-foreground" />
) : (
<ChevronDown className="h-4 w-4 text-muted-foreground" />
)}
</div>
</button>
<div className="px-4 pb-2">
<p className="text-2xl font-bold text-foreground">
{formatCurrency(totalBalance)}
</p>
<p className="text-sm text-muted-foreground">
{accounts?.length || 0} accounts
</p>
</div> </div>
<p className="text-2xl font-bold text-foreground mt-1">
{formatCurrency(totalBalance)} {/* Expanded Account Details */}
</p> {accountsExpanded && accounts && accounts.length > 0 && (
<p className="text-sm text-muted-foreground mt-1"> <div className="border-t border-border/50 max-h-64 overflow-y-auto">
{accounts?.length || 0} accounts {accounts.map((account) => {
</p> const primaryBalance = account.balances?.[0]?.amount || 0;
const currency = account.balances?.[0]?.currency || account.currency || "EUR";
return (
<div
key={account.id}
className="p-3 border-b border-border/30 last:border-b-0 hover:bg-muted/50 transition-colors"
>
<div className="flex items-start space-x-2">
<div className="flex-shrink-0 p-1 bg-background rounded">
<Building2 className="h-3 w-3 text-muted-foreground" />
</div>
<div className="space-y-1 min-w-0 flex-1">
<p className="text-sm font-medium text-foreground truncate">
{account.display_name || account.name || "Unnamed Account"}
</p>
<p className="text-sm font-semibold text-foreground">
{formatCurrency(primaryBalance, currency)}
</p>
</div>
</div>
</div>
);
})}
</div>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -28,7 +28,7 @@ import FiltersSkeleton from "./FiltersSkeleton";
import RawTransactionModal from "./RawTransactionModal"; import RawTransactionModal from "./RawTransactionModal";
import { FilterBar, type FilterState } from "./filters"; import { FilterBar, type FilterState } from "./filters";
import { DataTablePagination } from "./ui/data-table-pagination"; import { DataTablePagination } from "./ui/data-table-pagination";
import { Card, CardContent } from "./ui/card"; import { Card } from "./ui/card";
import { Alert, AlertDescription, AlertTitle } from "./ui/alert"; import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import type { Account, Transaction, ApiResponse } from "../types/api"; import type { Account, Transaction, ApiResponse } from "../types/api";
@@ -40,8 +40,6 @@ export default function TransactionsTable() {
selectedAccount: "", selectedAccount: "",
startDate: "", startDate: "",
endDate: "", endDate: "",
minAmount: "",
maxAmount: "",
}); });
const [showRawModal, setShowRawModal] = useState(false); const [showRawModal, setShowRawModal] = useState(false);
@@ -73,8 +71,6 @@ export default function TransactionsTable() {
selectedAccount: "", selectedAccount: "",
startDate: "", startDate: "",
endDate: "", endDate: "",
minAmount: "",
maxAmount: "",
}); });
setColumnFilters([]); setColumnFilters([]);
setCurrentPage(1); setCurrentPage(1);
@@ -116,8 +112,6 @@ export default function TransactionsTable() {
currentPage, currentPage,
perPage, perPage,
debouncedSearchTerm, debouncedSearchTerm,
filterState.minAmount,
filterState.maxAmount,
], ],
queryFn: () => queryFn: () =>
apiClient.getTransactions({ apiClient.getTransactions({
@@ -128,12 +122,6 @@ export default function TransactionsTable() {
perPage: perPage, perPage: perPage,
search: debouncedSearchTerm || undefined, search: debouncedSearchTerm || undefined,
summaryOnly: false, summaryOnly: false,
minAmount: filterState.minAmount
? parseFloat(filterState.minAmount)
: undefined,
maxAmount: filterState.maxAmount
? parseFloat(filterState.maxAmount)
: undefined,
}), }),
}); });
@@ -157,8 +145,6 @@ export default function TransactionsTable() {
filterState.selectedAccount, filterState.selectedAccount,
filterState.startDate, filterState.startDate,
filterState.endDate, filterState.endDate,
filterState.minAmount,
filterState.maxAmount,
]); ]);
const handleViewRaw = (transaction: Transaction) => { const handleViewRaw = (transaction: Transaction) => {
@@ -175,9 +161,7 @@ export default function TransactionsTable() {
filterState.searchTerm || filterState.searchTerm ||
filterState.selectedAccount || filterState.selectedAccount ||
filterState.startDate || filterState.startDate ||
filterState.endDate || filterState.endDate;
filterState.minAmount ||
filterState.maxAmount;
// Define columns // Define columns
@@ -372,38 +356,6 @@ export default function TransactionsTable() {
isSearchLoading={isSearchLoading} isSearchLoading={isSearchLoading}
/> />
{/* Results Summary */}
<Card>
<CardContent className="px-6 py-3 bg-muted/30 border-b border-border">
<p className="text-sm text-muted-foreground">
Showing {transactions.length} transaction
{transactions.length !== 1 ? "s" : ""} (
{pagination ? (
<>
{(pagination.page - 1) * pagination.per_page + 1}-
{Math.min(
pagination.page * pagination.per_page,
pagination.total,
)}{" "}
of {pagination.total}
</>
) : (
"loading..."
)}
)
{filterState.selectedAccount && accounts && (
<span className="ml-1">
for{" "}
{
accounts.find((acc) => acc.id === filterState.selectedAccount)
?.name
}
</span>
)}
</p>
</CardContent>
</Card>
{/* Responsive Table/Cards */} {/* Responsive Table/Cards */}
<Card> <Card>
{/* Desktop Table View (hidden on mobile) */} {/* Desktop Table View (hidden on mobile) */}

View File

@@ -51,7 +51,7 @@ export function AccountCombobox({
variant="outline" variant="outline"
role="combobox" role="combobox"
aria-expanded={open} aria-expanded={open}
className="justify-between" className="w-full justify-between"
> >
<div className="flex items-center"> <div className="flex items-center">
<Building2 className="mr-2 h-4 w-4" /> <Building2 className="mr-2 h-4 w-4" />

View File

@@ -8,12 +8,14 @@ import type { Account } from "../../types/api";
export interface ActiveFilterChipsProps { export interface ActiveFilterChipsProps {
filterState: FilterState; filterState: FilterState;
onFilterChange: (key: keyof FilterState, value: string) => void; onFilterChange: (key: keyof FilterState, value: string) => void;
onClearFilters: () => void;
accounts?: Account[]; accounts?: Account[];
} }
export function ActiveFilterChips({ export function ActiveFilterChips({
filterState, filterState,
onFilterChange, onFilterChange,
onClearFilters,
accounts = [], accounts = [],
}: ActiveFilterChipsProps) { }: ActiveFilterChipsProps) {
const chips: Array<{ const chips: Array<{
@@ -68,30 +70,6 @@ export function ActiveFilterChips({
}); });
} }
// Amount range chips
if (filterState.minAmount || filterState.maxAmount) {
let amountLabel = "Amount: ";
const minAmount = filterState.minAmount
? parseFloat(filterState.minAmount)
: null;
const maxAmount = filterState.maxAmount
? parseFloat(filterState.maxAmount)
: null;
if (minAmount && maxAmount) {
amountLabel += `${minAmount} - €${maxAmount}`;
} else if (minAmount) {
amountLabel += `≥ €${minAmount}`;
} else if (maxAmount) {
amountLabel += `≤ €${maxAmount}`;
}
chips.push({
key: "minAmount", // We'll clear both min and max when removing this chip
label: amountLabel,
value: `${filterState.minAmount}-${filterState.maxAmount}`,
});
}
const handleRemoveChip = (key: keyof FilterState) => { const handleRemoveChip = (key: keyof FilterState) => {
switch (key) { switch (key) {
@@ -100,11 +78,6 @@ export function ActiveFilterChips({
onFilterChange("startDate", ""); onFilterChange("startDate", "");
onFilterChange("endDate", ""); onFilterChange("endDate", "");
break; break;
case "minAmount":
// Clear both min and max amount
onFilterChange("minAmount", "");
onFilterChange("maxAmount", "");
break;
default: default:
onFilterChange(key, ""); onFilterChange(key, "");
} }
@@ -135,6 +108,15 @@ export function ActiveFilterChips({
</Button> </Button>
</Badge> </Badge>
))} ))}
<Button
onClick={onClearFilters}
variant="outline"
size="sm"
className="text-muted-foreground ml-2"
>
<X className="h-4 w-4 mr-1" />
Clear All
</Button>
</div> </div>
); );
} }

View File

@@ -1,127 +0,0 @@
import { useState } from "react";
import { MoreHorizontal, Euro } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
export interface AdvancedFiltersPopoverProps {
minAmount: string;
maxAmount: string;
onMinAmountChange: (value: string) => void;
onMaxAmountChange: (value: string) => void;
}
export function AdvancedFiltersPopover({
minAmount,
maxAmount,
onMinAmountChange,
onMaxAmountChange,
}: AdvancedFiltersPopoverProps) {
const [open, setOpen] = useState(false);
const hasAdvancedFilters = minAmount || maxAmount;
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant={hasAdvancedFilters ? "default" : "outline"}
size="default"
className="relative"
>
<MoreHorizontal className="h-4 w-4 mr-2" />
More
{hasAdvancedFilters && (
<div className="absolute -top-1 -right-1 h-2 w-2 bg-blue-600 rounded-full" />
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-80" align="end">
<div className="space-y-4">
<div className="space-y-2">
<h4 className="font-medium leading-none">Advanced Filters</h4>
<p className="text-sm text-muted-foreground">
Additional filters for more precise results
</p>
</div>
<div className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
Amount Range
</label>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<label className="text-xs text-muted-foreground">
Minimum
</label>
<div className="relative">
<Euro className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
type="number"
placeholder="0.00"
value={minAmount}
onChange={(e) => onMinAmountChange(e.target.value)}
className="pl-8"
step="0.01"
min="0"
/>
</div>
</div>
<div className="space-y-1">
<label className="text-xs text-muted-foreground">
Maximum
</label>
<div className="relative">
<Euro className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
type="number"
placeholder="1000.00"
value={maxAmount}
onChange={(e) => onMaxAmountChange(e.target.value)}
className="pl-8"
step="0.01"
min="0"
/>
</div>
</div>
</div>
<p className="text-xs text-muted-foreground">
Leave empty for no limit
</p>
</div>
{/* Future: Add transaction status filter */}
<div className="pt-2 border-t">
<div className="text-xs text-muted-foreground">
More filters coming soon: transaction status, categories, and
more.
</div>
</div>
{/* Clear advanced filters */}
{hasAdvancedFilters && (
<div className="pt-2">
<Button
variant="outline"
size="sm"
onClick={() => {
onMinAmountChange("");
onMaxAmountChange("");
}}
className="w-full"
>
Clear Advanced Filters
</Button>
</div>
)}
</div>
</div>
</PopoverContent>
</Popover>
);
}

View File

@@ -6,6 +6,7 @@ import type { DateRange } from "react-day-picker";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar"; import { Calendar } from "@/components/ui/calendar";
import { Card, CardContent, CardFooter } from "@/components/ui/card";
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
@@ -26,33 +27,35 @@ interface DatePreset {
const datePresets: DatePreset[] = [ const datePresets: DatePreset[] = [
{ {
label: "Last 7 days", label: "Today",
getValue: () => { getValue: () => {
const endDate = new Date(); const today = new Date();
const startDate = new Date();
startDate.setDate(endDate.getDate() - 7);
return { return {
startDate: startDate.toISOString().split("T")[0], startDate: today.toISOString().split("T")[0],
endDate: endDate.toISOString().split("T")[0], endDate: today.toISOString().split("T")[0],
}; };
}, },
}, },
{ {
label: "This week", label: "Yesterday",
getValue: () => { getValue: () => {
const now = new Date(); const yesterday = new Date();
const dayOfWeek = now.getDay(); yesterday.setDate(yesterday.getDate() - 1);
const startOfWeek = new Date(now);
startOfWeek.setDate(
now.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1),
); // Monday as start
const endOfWeek = new Date(startOfWeek);
endOfWeek.setDate(startOfWeek.getDate() + 6);
return { return {
startDate: startOfWeek.toISOString().split("T")[0], startDate: yesterday.toISOString().split("T")[0],
endDate: endOfWeek.toISOString().split("T")[0], endDate: yesterday.toISOString().split("T")[0],
};
},
},
{
label: "Last 7 days",
getValue: () => {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(endDate.getDate() - 6);
return {
startDate: startDate.toISOString().split("T")[0],
endDate: endDate.toISOString().split("T")[0],
}; };
}, },
}, },
@@ -61,7 +64,7 @@ const datePresets: DatePreset[] = [
getValue: () => { getValue: () => {
const endDate = new Date(); const endDate = new Date();
const startDate = new Date(); const startDate = new Date();
startDate.setDate(endDate.getDate() - 30); startDate.setDate(endDate.getDate() - 29);
return { return {
startDate: startDate.toISOString().split("T")[0], startDate: startDate.toISOString().split("T")[0],
endDate: endDate.toISOString().split("T")[0], endDate: endDate.toISOString().split("T")[0],
@@ -81,19 +84,6 @@ const datePresets: DatePreset[] = [
}; };
}, },
}, },
{
label: "This year",
getValue: () => {
const now = new Date();
const startOfYear = new Date(now.getFullYear(), 0, 1);
const endOfYear = new Date(now.getFullYear(), 11, 31);
return {
startDate: startOfYear.toISOString().split("T")[0],
endDate: endOfYear.toISOString().split("T")[0],
};
},
},
]; ];
export function DateRangePicker({ export function DateRangePicker({
@@ -178,34 +168,30 @@ export function DateRangePicker({
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start"> <PopoverContent className="w-auto p-0" align="start">
<div className="flex"> <Card className="w-auto py-4">
{/* Presets */} <CardContent className="px-4">
<div className="border-r p-3 space-y-1"> <Calendar
<div className="text-sm font-medium text-gray-700 mb-2"> mode="range"
Quick select defaultMonth={dateRange?.from}
</div> selected={dateRange}
onSelect={handleDateRangeSelect}
className="bg-transparent p-0"
/>
</CardContent>
<CardFooter className="grid grid-cols-2 gap-1 border-t px-4 !pt-4">
{datePresets.map((preset) => ( {datePresets.map((preset) => (
<Button <Button
key={preset.label} key={preset.label}
variant="ghost" variant="outline"
size="sm" size="sm"
className="w-full justify-start text-sm" className="text-xs px-2 h-7"
onClick={() => handlePresetClick(preset)} onClick={() => handlePresetClick(preset)}
> >
{preset.label} {preset.label}
</Button> </Button>
))} ))}
</div> </CardFooter>
{/* Calendar */} </Card>
<Calendar
initialFocus
mode="range"
defaultMonth={dateRange?.from}
selected={dateRange}
onSelect={handleDateRangeSelect}
numberOfMonths={2}
/>
</div>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </div>

View File

@@ -1,11 +1,9 @@
import { Search, X } from "lucide-react"; import { Search } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { DateRangePicker } from "./DateRangePicker"; import { DateRangePicker } from "./DateRangePicker";
import { AccountCombobox } from "./AccountCombobox"; import { AccountCombobox } from "./AccountCombobox";
import { ActiveFilterChips } from "./ActiveFilterChips"; import { ActiveFilterChips } from "./ActiveFilterChips";
import { AdvancedFiltersPopover } from "./AdvancedFiltersPopover";
import type { Account } from "../../types/api"; import type { Account } from "../../types/api";
export interface FilterState { export interface FilterState {
@@ -13,8 +11,6 @@ export interface FilterState {
selectedAccount: string; selectedAccount: string;
startDate: string; startDate: string;
endDate: string; endDate: string;
minAmount: string;
maxAmount: string;
} }
export interface FilterBarProps { export interface FilterBarProps {
@@ -38,9 +34,7 @@ export function FilterBar({
filterState.searchTerm || filterState.searchTerm ||
filterState.selectedAccount || filterState.selectedAccount ||
filterState.startDate || filterState.startDate ||
filterState.endDate || filterState.endDate;
filterState.minAmount ||
filterState.maxAmount;
const handleDateRangeChange = (startDate: string, endDate: string) => { const handleDateRangeChange = (startDate: string, endDate: string) => {
onFilterChange("startDate", startDate); onFilterChange("startDate", startDate);
@@ -58,61 +52,85 @@ export function FilterBar({
</div> </div>
{/* Primary Filters Row */} {/* Primary Filters Row */}
<div className="flex flex-wrap items-center gap-3 mb-4"> <div className="space-y-4 mb-4">
{/* Search Input */} {/* Desktop Layout */}
<div className="relative flex-1 min-w-[240px]"> <div className="hidden lg:flex items-center justify-between gap-6">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" /> {/* Left Side: Main Filters */}
<Input <div className="flex items-center gap-3 flex-1">
placeholder="Search transactions..." {/* Search Input */}
value={filterState.searchTerm} <div className="relative w-[200px]">
onChange={(e) => onFilterChange("searchTerm", e.target.value)} <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
className="pl-9 pr-8 bg-background" <Input
/> placeholder="Search transactions..."
{isSearchLoading && ( value={filterState.searchTerm}
<div className="absolute right-3 top-1/2 transform -translate-y-1/2"> onChange={(e) => onFilterChange("searchTerm", e.target.value)}
<div className="animate-spin h-4 w-4 border-2 border-border border-t-primary rounded-full"></div> className="pl-9 pr-8 bg-background"
/>
{isSearchLoading && (
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
<div className="animate-spin h-4 w-4 border-2 border-border border-t-primary rounded-full"></div>
</div>
)}
</div> </div>
)}
{/* Account Selection */}
<AccountCombobox
accounts={accounts}
selectedAccount={filterState.selectedAccount}
onAccountChange={(accountId) =>
onFilterChange("selectedAccount", accountId)
}
className="w-[180px]"
/>
{/* Date Range Picker */}
<DateRangePicker
startDate={filterState.startDate}
endDate={filterState.endDate}
onDateRangeChange={handleDateRangeChange}
className="w-[220px]"
/>
</div>
</div> </div>
{/* Account Selection */} {/* Mobile Layout */}
<AccountCombobox <div className="lg:hidden space-y-3">
accounts={accounts} {/* First Row: Search Input (Full Width) */}
selectedAccount={filterState.selectedAccount} <div className="relative">
onAccountChange={(accountId) => <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
onFilterChange("selectedAccount", accountId) <Input
} placeholder="Search..."
className="w-[200px]" value={filterState.searchTerm}
/> onChange={(e) => onFilterChange("searchTerm", e.target.value)}
className="pl-9 pr-8 bg-background w-full"
/>
{isSearchLoading && (
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
<div className="animate-spin h-4 w-4 border-2 border-border border-t-primary rounded-full"></div>
</div>
)}
</div>
{/* Date Range Picker */} {/* Second Row: Account Selection (Full Width) */}
<DateRangePicker <AccountCombobox
startDate={filterState.startDate} accounts={accounts}
endDate={filterState.endDate} selectedAccount={filterState.selectedAccount}
onDateRangeChange={handleDateRangeChange} onAccountChange={(accountId) =>
className="w-[240px]" onFilterChange("selectedAccount", accountId)
/> }
className="w-full"
/>
{/* Advanced Filters Button */} {/* Third Row: Date Range */}
<AdvancedFiltersPopover <DateRangePicker
minAmount={filterState.minAmount} startDate={filterState.startDate}
maxAmount={filterState.maxAmount} endDate={filterState.endDate}
onMinAmountChange={(value) => onFilterChange("minAmount", value)} onDateRangeChange={handleDateRangeChange}
onMaxAmountChange={(value) => onFilterChange("maxAmount", value)} className="w-full"
/> />
{/* Clear Filters Button */} </div>
{hasActiveFilters && (
<Button
onClick={onClearFilters}
variant="outline"
size="sm"
className="text-muted-foreground"
>
<X className="h-4 w-4 mr-1" />
Clear All
</Button>
)}
</div> </div>
{/* Active Filter Chips */} {/* Active Filter Chips */}
@@ -120,6 +138,7 @@ export function FilterBar({
<ActiveFilterChips <ActiveFilterChips
filterState={filterState} filterState={filterState}
onFilterChange={onFilterChange} onFilterChange={onFilterChange}
onClearFilters={onClearFilters}
accounts={accounts} accounts={accounts}
/> />
)} )}

View File

@@ -2,5 +2,4 @@ export { FilterBar } from "./FilterBar";
export { DateRangePicker } from "./DateRangePicker"; export { DateRangePicker } from "./DateRangePicker";
export { AccountCombobox } from "./AccountCombobox"; export { AccountCombobox } from "./AccountCombobox";
export { ActiveFilterChips } from "./ActiveFilterChips"; export { ActiveFilterChips } from "./ActiveFilterChips";
export { AdvancedFiltersPopover } from "./AdvancedFiltersPopover";
export type { FilterState, FilterBarProps } from "./FilterBar"; export type { FilterState, FilterBarProps } from "./FilterBar";

View File

@@ -29,6 +29,12 @@
--chart-4: 43 74% 66%; --chart-4: 43 74% 66%;
--chart-5: 27 87% 67%; --chart-5: 27 87% 67%;
--radius: 0.5rem; --radius: 0.5rem;
/* iOS Safe Area Support for PWA */
--safe-area-inset-top: env(safe-area-inset-top, 0px);
--safe-area-inset-right: env(safe-area-inset-right, 0px);
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
--safe-area-inset-left: env(safe-area-inset-left, 0px);
} }
.dark { .dark {
--background: 222.2 84% 4.9%; --background: 222.2 84% 4.9%;

View File

@@ -10,6 +10,7 @@
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as TransactionsRouteImport } from './routes/transactions' import { Route as TransactionsRouteImport } from './routes/transactions'
import { Route as SettingsRouteImport } from './routes/settings'
import { Route as NotificationsRouteImport } from './routes/notifications' import { Route as NotificationsRouteImport } from './routes/notifications'
import { Route as AnalyticsRouteImport } from './routes/analytics' import { Route as AnalyticsRouteImport } from './routes/analytics'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
@@ -19,6 +20,11 @@ const TransactionsRoute = TransactionsRouteImport.update({
path: '/transactions', path: '/transactions',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const SettingsRoute = SettingsRouteImport.update({
id: '/settings',
path: '/settings',
getParentRoute: () => rootRouteImport,
} as any)
const NotificationsRoute = NotificationsRouteImport.update({ const NotificationsRoute = NotificationsRouteImport.update({
id: '/notifications', id: '/notifications',
path: '/notifications', path: '/notifications',
@@ -39,12 +45,14 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/analytics': typeof AnalyticsRoute '/analytics': typeof AnalyticsRoute
'/notifications': typeof NotificationsRoute '/notifications': typeof NotificationsRoute
'/settings': typeof SettingsRoute
'/transactions': typeof TransactionsRoute '/transactions': typeof TransactionsRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/analytics': typeof AnalyticsRoute '/analytics': typeof AnalyticsRoute
'/notifications': typeof NotificationsRoute '/notifications': typeof NotificationsRoute
'/settings': typeof SettingsRoute
'/transactions': typeof TransactionsRoute '/transactions': typeof TransactionsRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
@@ -52,20 +60,33 @@ export interface FileRoutesById {
'/': typeof IndexRoute '/': typeof IndexRoute
'/analytics': typeof AnalyticsRoute '/analytics': typeof AnalyticsRoute
'/notifications': typeof NotificationsRoute '/notifications': typeof NotificationsRoute
'/settings': typeof SettingsRoute
'/transactions': typeof TransactionsRoute '/transactions': typeof TransactionsRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/analytics' | '/notifications' | '/transactions' fullPaths:
| '/'
| '/analytics'
| '/notifications'
| '/settings'
| '/transactions'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: '/' | '/analytics' | '/notifications' | '/transactions' to: '/' | '/analytics' | '/notifications' | '/settings' | '/transactions'
id: '__root__' | '/' | '/analytics' | '/notifications' | '/transactions' id:
| '__root__'
| '/'
| '/analytics'
| '/notifications'
| '/settings'
| '/transactions'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
AnalyticsRoute: typeof AnalyticsRoute AnalyticsRoute: typeof AnalyticsRoute
NotificationsRoute: typeof NotificationsRoute NotificationsRoute: typeof NotificationsRoute
SettingsRoute: typeof SettingsRoute
TransactionsRoute: typeof TransactionsRoute TransactionsRoute: typeof TransactionsRoute
} }
@@ -78,6 +99,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof TransactionsRouteImport preLoaderRoute: typeof TransactionsRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/settings': {
id: '/settings'
path: '/settings'
fullPath: '/settings'
preLoaderRoute: typeof SettingsRouteImport
parentRoute: typeof rootRouteImport
}
'/notifications': { '/notifications': {
id: '/notifications' id: '/notifications'
path: '/notifications' path: '/notifications'
@@ -106,6 +134,7 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
AnalyticsRoute: AnalyticsRoute, AnalyticsRoute: AnalyticsRoute,
NotificationsRoute: NotificationsRoute, NotificationsRoute: NotificationsRoute,
SettingsRoute: SettingsRoute,
TransactionsRoute: TransactionsRoute, TransactionsRoute: TransactionsRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport

View File

@@ -34,9 +34,9 @@ function RootLayout() {
/> />
)} )}
<div className="flex flex-col flex-1"> <div className="flex flex-col flex-1 min-w-0">
<Header setSidebarOpen={setSidebarOpen} /> <Header setSidebarOpen={setSidebarOpen} />
<main className="flex-1 p-6"> <main className="flex-1 p-6 min-w-0">
<Outlet /> <Outlet />
</main> </main>
</div> </div>

View File

@@ -1,6 +1,11 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import AccountsOverview from "../components/AccountsOverview"; import TransactionsTable from "../components/TransactionsTable";
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/")({
component: AccountsOverview, component: TransactionsTable,
validateSearch: (search) => ({
accountId: search.accountId as string | undefined,
startDate: search.startDate as string | undefined,
endDate: search.endDate as string | undefined,
}),
}); });

View File

@@ -0,0 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import AccountSettings from "../components/AccountSettings";
export const Route = createFileRoute("/settings")({
component: AccountSettings,
});

View File

@@ -9,6 +9,12 @@ export default {
md: "calc(var(--radius) - 2px)", md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)", sm: "calc(var(--radius) - 4px)",
}, },
spacing: {
'safe-top': 'var(--safe-area-inset-top)',
'safe-bottom': 'var(--safe-area-inset-bottom)',
'safe-left': 'var(--safe-area-inset-left)',
'safe-right': 'var(--safe-area-inset-right)',
},
colors: { colors: {
background: "hsl(var(--background))", background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))", foreground: "hsl(var(--foreground))",

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "leggen" name = "leggen"
version = "2025.9.14" version = "2025.9.16"
description = "An Open Banking CLI" description = "An Open Banking CLI"
authors = [{ name = "Elisiário Couto", email = "elisiario@couto.io" }] authors = [{ name = "Elisiário Couto", email = "elisiario@couto.io" }]
requires-python = "~=3.13.0" requires-python = "~=3.13.0"

2
uv.lock generated
View File

@@ -220,7 +220,7 @@ wheels = [
[[package]] [[package]]
name = "leggen" name = "leggen"
version = "2025.9.14" version = "2025.9.16"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "apscheduler" }, { name = "apscheduler" },