feat(frontend): Rename notifications page to System Status and add sync operations section

Co-authored-by: elisiariocouto <818914+elisiariocouto@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-09-21 23:05:12 +00:00
committed by Elisiário Couto
parent 61f9592095
commit 3f2ff21eac
4 changed files with 148 additions and 6 deletions

View File

@@ -3,7 +3,7 @@ import { Link, useLocation } from "@tanstack/react-router";
import { import {
List, List,
BarChart3, BarChart3,
Bell, Activity,
Settings, Settings,
Building2, Building2,
TrendingUp, TrendingUp,
@@ -33,7 +33,7 @@ import {
const navigation = [ const navigation = [
{ name: "Overview", icon: List, to: "/" }, { name: "Overview", icon: List, to: "/" },
{ name: "Analytics", icon: BarChart3, to: "/analytics" }, { name: "Analytics", icon: BarChart3, to: "/analytics" },
{ name: "Notifications", icon: Bell, to: "/notifications" }, { name: "System Status", icon: Activity, to: "/notifications" },
{ name: "Settings", icon: Settings, to: "/settings" }, { name: "Settings", icon: Settings, to: "/settings" },
]; ];

View File

@@ -10,6 +10,10 @@ import {
CheckCircle, CheckCircle,
Settings, Settings,
TestTube, TestTube,
Activity,
Clock,
TrendingUp,
User,
} from "lucide-react"; } from "lucide-react";
import { apiClient } from "../lib/api"; import { apiClient } from "../lib/api";
import NotificationsSkeleton from "./NotificationsSkeleton"; import NotificationsSkeleton from "./NotificationsSkeleton";
@@ -32,7 +36,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "./ui/select"; } from "./ui/select";
import type { NotificationSettings, NotificationService } from "../types/api"; import type { NotificationSettings, NotificationService, SyncOperationsResponse } from "../types/api";
export default function Notifications() { export default function Notifications() {
const [testService, setTestService] = useState(""); const [testService, setTestService] = useState("");
@@ -61,6 +65,16 @@ export default function Notifications() {
queryFn: apiClient.getNotificationServices, queryFn: apiClient.getNotificationServices,
}); });
const {
data: syncOperations,
isLoading: syncOperationsLoading,
error: syncOperationsError,
refetch: refetchSyncOperations,
} = useQuery<SyncOperationsResponse>({
queryKey: ["syncOperations"],
queryFn: () => apiClient.getSyncOperations(10, 0), // Get latest 10 operations
});
const testMutation = useMutation({ const testMutation = useMutation({
mutationFn: apiClient.testNotification, mutationFn: apiClient.testNotification,
onSuccess: () => { onSuccess: () => {
@@ -80,15 +94,15 @@ export default function Notifications() {
}, },
}); });
if (settingsLoading || servicesLoading) { if (settingsLoading || servicesLoading || syncOperationsLoading) {
return <NotificationsSkeleton />; return <NotificationsSkeleton />;
} }
if (settingsError || servicesError) { if (settingsError || servicesError || syncOperationsError) {
return ( return (
<Alert variant="destructive"> <Alert variant="destructive">
<AlertCircle className="h-4 w-4" /> <AlertCircle className="h-4 w-4" />
<AlertTitle>Failed to load notifications</AlertTitle> <AlertTitle>Failed to load system data</AlertTitle>
<AlertDescription className="space-y-3"> <AlertDescription className="space-y-3">
<p> <p>
Unable to connect to the Leggen API. Please check your configuration Unable to connect to the Leggen API. Please check your configuration
@@ -98,6 +112,7 @@ export default function Notifications() {
onClick={() => { onClick={() => {
refetchSettings(); refetchSettings();
refetchServices(); refetchServices();
refetchSyncOperations();
}} }}
variant="outline" variant="outline"
size="sm" size="sm"
@@ -131,6 +146,100 @@ export default function Notifications() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Sync Operations Section */}
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Activity className="h-5 w-5 text-primary" />
<span>Sync Operations</span>
</CardTitle>
<CardDescription>Recent synchronization activities</CardDescription>
</CardHeader>
<CardContent>
{!syncOperations || syncOperations.operations.length === 0 ? (
<div className="text-center py-6">
<Activity className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
No sync operations yet
</h3>
<p className="text-muted-foreground">
Sync operations will appear here once you start syncing your accounts.
</p>
</div>
) : (
<div className="space-y-4">
{syncOperations.operations.slice(0, 5).map((operation) => {
const startedAt = new Date(operation.started_at);
const isRunning = !operation.completed_at;
const duration = operation.duration_seconds
? `${Math.round(operation.duration_seconds)}s`
: '';
return (
<div
key={operation.id}
className="flex items-center justify-between p-4 border rounded-lg hover:bg-accent transition-colors"
>
<div className="flex items-center space-x-4">
<div className={`p-2 rounded-full ${
isRunning
? 'bg-blue-100 text-blue-600'
: operation.success
? 'bg-green-100 text-green-600'
: 'bg-red-100 text-red-600'
}`}>
{isRunning ? (
<RefreshCw className="h-4 w-4 animate-spin" />
) : operation.success ? (
<CheckCircle className="h-4 w-4" />
) : (
<AlertCircle className="h-4 w-4" />
)}
</div>
<div>
<div className="flex items-center space-x-2">
<h4 className="text-sm font-medium text-foreground">
{isRunning ? 'Sync Running' : operation.success ? 'Sync Completed' : 'Sync Failed'}
</h4>
<Badge variant="outline" className="text-xs">
{operation.trigger_type}
</Badge>
</div>
<div className="flex items-center space-x-4 mt-1 text-xs text-muted-foreground">
<span className="flex items-center space-x-1">
<Clock className="h-3 w-3" />
<span>{startedAt.toLocaleDateString()} {startedAt.toLocaleTimeString()}</span>
</span>
{duration && (
<span>Duration: {duration}</span>
)}
</div>
</div>
</div>
<div className="text-right text-sm text-muted-foreground">
<div className="flex items-center space-x-2">
<User className="h-3 w-3" />
<span>{operation.accounts_processed} accounts</span>
</div>
<div className="flex items-center space-x-2 mt-1">
<TrendingUp className="h-3 w-3" />
<span>{operation.transactions_added} new transactions</span>
</div>
{operation.errors.length > 0 && (
<div className="flex items-center space-x-2 mt-1 text-red-600">
<AlertCircle className="h-3 w-3" />
<span>{operation.errors.length} errors</span>
</div>
)}
</div>
</div>
);
})}
</div>
)}
</CardContent>
</Card>
{/* Test Notification Section */} {/* Test Notification Section */}
<Card> <Card>
<CardHeader> <CardHeader>

View File

@@ -12,6 +12,7 @@ import type {
HealthData, HealthData,
AccountUpdate, AccountUpdate,
TransactionStats, TransactionStats,
SyncOperationsResponse,
} from "../types/api"; } from "../types/api";
// Use VITE_API_URL for development, relative URLs for production // Use VITE_API_URL for development, relative URLs for production
@@ -219,6 +220,17 @@ export const apiClient = {
>(`/transactions/monthly-stats?${queryParams.toString()}`); >(`/transactions/monthly-stats?${queryParams.toString()}`);
return response.data.data; return response.data.data;
}, },
// Get sync operations history
getSyncOperations: async (
limit: number = 50,
offset: number = 0,
): Promise<SyncOperationsResponse> => {
const response = await api.get<ApiResponse<SyncOperationsResponse>>(
`/sync/operations?limit=${limit}&offset=${offset}`,
);
return response.data.data;
},
}; };
export default apiClient; export default apiClient;

View File

@@ -220,3 +220,24 @@ export interface TransactionStats {
average_transaction: number; average_transaction: number;
accounts_included: number; accounts_included: number;
} }
// Sync operations types
export interface SyncOperation {
id: number;
started_at: string;
completed_at?: string;
success?: boolean;
accounts_processed: number;
transactions_added: number;
transactions_updated: number;
balances_updated: number;
duration_seconds?: number;
errors: string[];
logs: string[];
trigger_type: 'manual' | 'scheduled' | 'api';
}
export interface SyncOperationsResponse {
operations: SyncOperation[];
count: number;
}