From abf39abe74b75d8cb980109fbcbdd940066cc90b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elisi=C3=A1rio=20Couto?= Date: Tue, 9 Sep 2025 17:13:48 +0100 Subject: [PATCH] feat: add notifications view and update branding - Add complete notifications management view with: - Service status display (Discord, Telegram) - Test notification functionality - Service management (delete/disable) - Filter settings display (case sensitive/insensitive) - Update API types to match current backend structure - Fix NotificationFilters type (remove deprecated fields) - Update page title from 'Vite + React + TS' to 'Leggen' - Replace Vite favicon with custom Leggen favicon - Add notifications tab to main navigation - Ensure full API compatibility with current backend --- frontend/index.html | 4 +- frontend/public/favicon.svg | 4 + frontend/src/components/Dashboard.tsx | 8 +- frontend/src/components/Notifications.tsx | 290 ++++++++++++++++++++++ frontend/src/lib/api.ts | 32 ++- frontend/src/lib/utils.ts | 6 +- frontend/src/types/api.ts | 39 +++ 7 files changed, 375 insertions(+), 8 deletions(-) create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/src/components/Notifications.tsx diff --git a/frontend/index.html b/frontend/index.html index e4b78ea..652cdda 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,9 +2,9 @@ - + - Vite + React + TS + Leggen
diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..f3d69ab --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index 7f60889..afc32f7 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -10,16 +10,18 @@ import { List, BarChart3, Wifi, - WifiOff + WifiOff, + Bell } from 'lucide-react'; import { apiClient } from '../lib/api'; import AccountsOverview from './AccountsOverview'; import TransactionsList from './TransactionsList'; +import Notifications from './Notifications'; import ErrorBoundary from './ErrorBoundary'; import { cn } from '../lib/utils'; import type { Account } from '../types/api'; -type TabType = 'overview' | 'transactions' | 'analytics'; +type TabType = 'overview' | 'transactions' | 'analytics' | 'notifications'; export default function Dashboard() { const [activeTab, setActiveTab] = useState('overview'); @@ -47,6 +49,7 @@ export default function Dashboard() { { name: 'Overview', icon: Home, id: 'overview' as TabType }, { name: 'Transactions', icon: List, id: 'transactions' as TabType }, { name: 'Analytics', icon: BarChart3, id: 'analytics' as TabType }, + { name: 'Notifications', icon: Bell, id: 'notifications' as TabType }, ]; const totalBalance = accounts?.reduce((sum, account) => { @@ -176,6 +179,7 @@ export default function Dashboard() {

Analytics dashboard coming soon...

)} + {activeTab === 'notifications' && } diff --git a/frontend/src/components/Notifications.tsx b/frontend/src/components/Notifications.tsx new file mode 100644 index 0000000..8282544 --- /dev/null +++ b/frontend/src/components/Notifications.tsx @@ -0,0 +1,290 @@ +import { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { + Bell, + MessageSquare, + Send, + Trash2, + RefreshCw, + AlertCircle, + CheckCircle, + Settings, + TestTube +} from 'lucide-react'; +import { apiClient } from '../lib/api'; +import LoadingSpinner from './LoadingSpinner'; +import type { NotificationSettings, NotificationService } from '../types/api'; + +export default function Notifications() { + const [testService, setTestService] = useState(''); + const [testMessage, setTestMessage] = useState('Test notification from Leggen'); + const queryClient = useQueryClient(); + + const { + data: settings, + 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 testMutation = useMutation({ + mutationFn: apiClient.testNotification, + onSuccess: () => { + // Could show a success toast here + console.log('Test notification sent successfully'); + }, + onError: (error) => { + console.error('Failed to send test notification:', error); + }, + }); + + const deleteServiceMutation = useMutation({ + mutationFn: apiClient.deleteNotificationService, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['notificationSettings'] }); + queryClient.invalidateQueries({ queryKey: ['notificationServices'] }); + }, + }); + + if (settingsLoading || servicesLoading) { + return ( +
+ +
+ ); + } + + if (settingsError || servicesError) { + return ( +
+
+
+ +

Failed to load notifications

+

+ Unable to connect to the Leggen API. Make sure the server is running on localhost:8000. +

+ +
+
+
+ ); + } + + const handleTestNotification = () => { + if (!testService) return; + + testMutation.mutate({ + service: testService, + message: testMessage, + }); + }; + + const handleDeleteService = (serviceName: string) => { + if (confirm(`Are you sure you want to delete the ${serviceName} notification service?`)) { + deleteServiceMutation.mutate(serviceName); + } + }; + + return ( +
+ {/* Test Notification Section */} +
+
+ +

Test Notifications

+
+ +
+
+ + +
+ +
+ + setTestMessage(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + placeholder="Test message..." + /> +
+
+ +
+ +
+
+ + {/* 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.enabled ? 'Enabled' : 'Disabled'} + + + {service.configured ? 'Configured' : 'Not Configured'} + +
+
+
+ +
+ +
+
+
+ ))} +
+ )} +
+ + {/* Notification Settings */} +
+
+ +

Notification Settings

+
+ + {settings && ( +
+
+

Filters

+
+
+
+ +

+ {settings.filters.case_insensitive.length > 0 + ? settings.filters.case_insensitive.join(', ') + : 'None' + } +

+
+
+ +

+ {settings.filters.case_sensitive && settings.filters.case_sensitive.length > 0 + ? settings.filters.case_sensitive.join(', ') + : 'None' + } +

+
+
+
+
+ +
+

Configure notification settings through your backend API to customize filters and service configurations.

+
+
+ )} +
+
+ ); +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index c19ad5f..2004f29 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,5 +1,5 @@ import axios from 'axios'; -import type { Account, Transaction, Balance, ApiResponse } from '../types/api'; +import type { Account, Transaction, Balance, ApiResponse, NotificationSettings, NotificationTest, NotificationService, NotificationServicesResponse } from '../types/api'; const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000/api/v1'; @@ -62,6 +62,36 @@ export const apiClient = { const response = await api.get>(`/transactions/${id}`); return response.data.data; }, + + // Get notification settings + getNotificationSettings: async (): Promise => { + const response = await api.get>('/notifications/settings'); + return response.data.data; + }, + + // Update notification settings + updateNotificationSettings: async (settings: NotificationSettings): Promise => { + const response = await api.put>('/notifications/settings', settings); + return response.data.data; + }, + + // Test notification + testNotification: async (test: NotificationTest): Promise => { + await api.post('/notifications/test', test); + }, + + // Get notification services + getNotificationServices: async (): Promise => { + const response = await api.get>('/notifications/services'); + // Convert object to array format + const servicesData = response.data.data; + return Object.values(servicesData); + }, + + // Delete notification service + deleteNotificationService: async (service: string): Promise => { + await api.delete(`/notifications/settings/${service}`); + }, }; export default apiClient; diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index b9d7638..5976740 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -13,9 +13,9 @@ export function formatCurrency(amount: number, currency: string = 'EUR'): string style: 'currency', currency: validCurrency, }).format(amount); - } catch (error) { - // Fallback if currency is still invalid - console.warn(`Invalid currency code: ${currency}, falling back to EUR`); + } catch { + // Fallback if currency is still invalid + console.warn(`Invalid currency code: ${currency}, falling back to EUR`); return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'EUR', diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index 3791d58..67b8993 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -86,3 +86,42 @@ export interface PaginatedResponse { per_page: number; total_pages: number; } + +// Notification types +export interface DiscordConfig { + webhook: string; + enabled: boolean; +} + +export interface TelegramConfig { + token: string; + chat_id: number; + enabled: boolean; +} + +export interface NotificationFilters { + case_insensitive: string[]; + case_sensitive?: string[]; +} + +export interface NotificationSettings { + discord?: DiscordConfig; + telegram?: TelegramConfig; + filters: NotificationFilters; +} + +export interface NotificationTest { + service: string; + message?: string; +} + +export interface NotificationService { + name: string; + enabled: boolean; + configured: boolean; + active?: boolean; +} + +export interface NotificationServicesResponse { + [serviceName: string]: NotificationService; +}