import { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { CreditCard, TrendingUp, TrendingDown, Building2, RefreshCw, AlertCircle, Edit2, Check, X, Bell, MessageSquare, Send, Trash2, User, Filter, Cloud, } 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 { Label } from "./ui/label"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"; import AccountsSkeleton from "./AccountsSkeleton"; import NotificationFiltersDrawer from "./NotificationFiltersDrawer"; import DiscordConfigDrawer from "./DiscordConfigDrawer"; import TelegramConfigDrawer from "./TelegramConfigDrawer"; import AddBankAccountDrawer from "./AddBankAccountDrawer"; import S3BackupConfigDrawer from "./S3BackupConfigDrawer"; import type { Account, Balance, NotificationSettings, NotificationService, BackupSettings, } 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 Settings() { const [editingAccountId, setEditingAccountId] = useState(null); const [editingName, setEditingName] = useState(""); const [failedImages, setFailedImages] = useState>(new Set()); const queryClient = useQueryClient(); // Account queries const { data: accounts, isLoading: accountsLoading, error: accountsError, refetch: refetchAccounts, } = useQuery({ queryKey: ["accounts"], queryFn: apiClient.getAccounts, }); const { data: balances } = useQuery({ queryKey: ["balances"], queryFn: () => apiClient.getBalances(), }); // Notification queries const { data: notificationSettings, isLoading: settingsLoading, error: settingsError, refetch: refetchSettings, } = useQuery({ queryKey: ["notificationSettings"], queryFn: apiClient.getNotificationSettings, }); const { data: services, isLoading: servicesLoading, error: servicesError, refetch: refetchServices, } = useQuery({ queryKey: ["notificationServices"], queryFn: apiClient.getNotificationServices, }); const { data: bankConnections } = useQuery({ queryKey: ["bankConnections"], queryFn: apiClient.getBankConnectionsStatus, }); // Backup queries const { data: backupSettings, isLoading: backupLoading, error: backupError, refetch: refetchBackup, } = useQuery({ queryKey: ["backupSettings"], queryFn: apiClient.getBackupSettings, }); // Account mutations 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); }, }); // Notification mutations const deleteServiceMutation = useMutation({ mutationFn: apiClient.deleteNotificationService, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["notificationSettings"] }); queryClient.invalidateQueries({ queryKey: ["notificationServices"] }); }, }); // Bank connection mutations const deleteBankConnectionMutation = useMutation({ mutationFn: apiClient.deleteBankConnection, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["accounts"] }); queryClient.invalidateQueries({ queryKey: ["bankConnections"] }); queryClient.invalidateQueries({ queryKey: ["balances"] }); }, }); // Account handlers const handleEditStart = (account: Account) => { setEditingAccountId(account.id); 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(""); }; // Notification handlers const handleDeleteService = (serviceName: string) => { if ( confirm( `Are you sure you want to delete the ${serviceName} notification service?`, ) ) { deleteServiceMutation.mutate(serviceName.toLowerCase()); } }; const isLoading = accountsLoading || settingsLoading || servicesLoading || backupLoading; const hasError = accountsError || settingsError || servicesError || backupError; if (isLoading) { return ; } if (hasError) { return ( Failed to load settings

Unable to connect to the Leggen API. Please check your configuration and ensure the API server is running.

); } return (
Accounts Notifications Backup {/* Account Management Section */} Account Management Manage your connected bank accounts and customize their display names {!accounts || accounts.length === 0 ? (

No accounts found

Connect your first bank account to get started with Leggen.

) : (
{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 (
{/* Mobile layout - stack vertically */}
{account.logo && !failedImages.has(account.id) ? ( {`${account.institution_id} { console.warn( `Failed to load bank logo for ${account.institution_id}: ${account.logo}`, ); setFailedImages( (prev) => new Set([...prev, account.id]), ); }} /> ) : ( )}
{editingAccountId === account.id ? (
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 />

{account.institution_id}

) : (

{account.display_name || account.name || "Unnamed Account"}

{account.institution_id}

{account.iban && (

IBAN: {account.iban}

)}
)}
{/* Balance and date section */}
{/* Date and status indicator - left on mobile, bottom on desktop */}
{/* Tooltip */}
{getStatusIndicator(account.status).tooltip}

Updated{" "} {formatDate( account.last_accessed || account.created, )}

{/* Balance - right on mobile, top on desktop */}
{isPositive ? ( ) : ( )}

{formatCurrency(balance, currency)}

); })}
)}
{/* Bank Connections Status */}
Bank Connections Status of all bank connection requests and their authorization state
{!bankConnections || bankConnections.length === 0 ? (

No bank connections found

Bank connection requests will appear here after you connect accounts.

) : (
{bankConnections.map((connection) => { const statusColor = connection.status.toLowerCase() === "ln" ? "bg-green-500" : connection.status.toLowerCase() === "cr" ? "bg-amber-500" : connection.status.toLowerCase() === "ex" ? "bg-red-500" : "bg-muted-foreground"; return (

{connection.bank_name}

{connection.status_display} •{" "} {connection.accounts_count} account {connection.accounts_count !== 1 ? "s" : ""}

ID: {connection.requisition_id}

Created {formatDate(connection.created_at)}

); })}
)} {/* Notification Services */} Notification Services Manage your notification services {!services || services.length === 0 ? (

No notification services configured

Configure notification services in your backend to receive alerts.

) : (
{services.map((service) => (
{service.name.toLowerCase().includes("discord") ? ( ) : service.name .toLowerCase() .includes("telegram") ? ( ) : ( )}

{service.name}

{service.enabled && service.configured ? "Active" : service.enabled ? "Needs Configuration" : "Disabled"}
{service.name.toLowerCase().includes("discord") ? ( ) : service.name .toLowerCase() .includes("telegram") ? ( ) : null}
))}
)} {/* Notification Filters */}
Notification Filters
{notificationSettings?.filters ? (
{notificationSettings.filters.case_insensitive .length > 0 ? ( notificationSettings.filters.case_insensitive.map( (filter, index) => ( {filter} ), ) ) : (

None

)}
{notificationSettings.filters.case_sensitive && notificationSettings.filters.case_sensitive.length > 0 ? ( notificationSettings.filters.case_sensitive.map( (filter, index) => ( {filter} ), ) ) : (

None

)}

Filters determine which transaction descriptions will trigger notifications. Add terms to exclude transactions containing those words.

) : (

No notification filters configured

Set up filters to control which transactions trigger notifications.

)}
{/* S3 Backup Configuration */} S3 Backup Configuration Configure automatic database backups to Amazon S3 or S3-compatible storage {!backupSettings?.s3 ? (

No S3 backup configured

Set up S3 backup to automatically backup your database to the cloud.

) : (

S3 Backup

{backupSettings.s3.enabled ? 'Enabled' : 'Disabled'}

Bucket: {backupSettings.s3.bucket_name}

Region: {backupSettings.s3.region}

{backupSettings.s3.endpoint_url && (

Endpoint: {backupSettings.s3.endpoint_url}

)}
Backup Information

Database backups are stored in the "leggen_backups/" folder in your S3 bucket. Backups include the complete SQLite database file.

)}
); }