-
-
- Enable maintenance mode to temporarily disable access to the platform
+
+
+
+
+
+ Activating this will redirect all non-admin traffic to the
+ maintenance landing page.
{!isEnabled && (
-
-
+
+
setMessage(e.target.value)}
+ className="h-11 rounded-none border-gray-200 text-sm font-medium focus-visible:ring-gray-900 shadow-none"
/>
-
- This message will be displayed to users when maintenance mode is enabled
-
)}
{isEnabled && status?.message && (
-
-
-
{status.message}
+
+
+
+ {status.message}
+
)}
+
+ {isLoading && (
+
+ Verifying system status...
+
+ )}
- )
+ );
}
diff --git a/src/pages/admin/payments/payment-requests.tsx b/src/pages/admin/payments/payment-requests.tsx
new file mode 100644
index 0000000..640cbfc
--- /dev/null
+++ b/src/pages/admin/payments/payment-requests.tsx
@@ -0,0 +1,196 @@
+import { useState } from "react";
+import { useQuery } from "@tanstack/react-query";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+import { Search, ChevronLeft, ChevronRight, Filter } from "lucide-react";
+import { paymentService } from "@/services";
+import { cn } from "@/lib/utils";
+
+export default function PaymentRequestsPage() {
+ const [page, setPage] = useState(1);
+ const [search, setSearch] = useState("");
+
+ const { data: requestsData, isLoading: requestsLoading } = useQuery({
+ queryKey: ["admin", "payment-requests", page, search],
+ queryFn: () =>
+ paymentService.getPaymentRequests({
+ page,
+ limit: 10,
+ search: search || undefined,
+ }),
+ });
+
+ const formatCurrency = (amount: number | any) => {
+ const val = typeof amount === "number" ? amount : 0;
+ return new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: "USD",
+ }).format(val);
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case "PAID":
+ return "text-emerald-600 bg-emerald-50";
+ case "SENT":
+ return "text-blue-600 bg-blue-50";
+ case "OPENED":
+ return "text-amber-600 bg-amber-50";
+ case "EXPIRED":
+ case "CANCELLED":
+ return "text-red-600 bg-red-50";
+ default:
+ return "text-gray-600 bg-gray-50";
+ }
+ };
+
+ return (
+
+
+
+
+ Payment Requests
+
+
+ Manage outbound customer requests.
+
+
+ {/* View only access: New Request button removed */}
+
+
+
+
+
+ Request Queue
+
+
+
+
+ setSearch(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+ |
+ Request #
+ |
+
+ Customer
+ |
+
+ Amount
+ |
+
+ Due Date
+ |
+
+ Status
+ |
+
+
+
+ {requestsLoading ? (
+
+ |
+ Loading requests...
+ |
+
+ ) : requestsData?.data && requestsData.data.length > 0 ? (
+ requestsData.data.map((request) => (
+
+ |
+ {request.paymentRequestNumber}
+ |
+
+
+
+ {request.customerName}
+
+
+ {request.customerEmail}
+
+
+ |
+
+ {formatCurrency(request.amount)}
+ |
+
+ {new Date(request.dueDate).toLocaleDateString()}
+ |
+
+
+ {request.status}
+
+ |
+
+ ))
+ ) : (
+
+ |
+ No records found.
+ |
+
+ )}
+
+
+
+
+ {requestsData?.meta && (
+
+
+ Page {requestsData.meta.page} of {requestsData.meta.totalPages}
+
+
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/pages/admin/payments/payments-list.tsx b/src/pages/admin/payments/payments-list.tsx
new file mode 100644
index 0000000..d70c5af
--- /dev/null
+++ b/src/pages/admin/payments/payments-list.tsx
@@ -0,0 +1,162 @@
+import { useState } from "react";
+import { useQuery } from "@tanstack/react-query";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+import {
+ Search,
+ ChevronLeft,
+ ChevronRight,
+ Download,
+ Flag,
+} from "lucide-react";
+import { paymentService } from "@/services";
+
+export default function PaymentsListPage() {
+ const [page, setPage] = useState(1);
+
+ const { data: paymentsData, isLoading: paymentsLoading } = useQuery({
+ queryKey: ["admin", "payments", page],
+ queryFn: () => paymentService.getPayments({ page, limit: 10 }),
+ });
+
+ const formatCurrency = (amount: number | any) => {
+ const val = typeof amount === "number" ? amount : 0;
+ return new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: "USD",
+ }).format(val);
+ };
+
+ return (
+
+
+
+
+ Payments
+
+
History of settled transactions.
+
+ {/* View only access: Export button removed */}
+
+
+
+
+
+ Transaction History
+
+
+
+
+
+
+
+
+
+
+
+ |
+ Transaction ID
+ |
+
+ Sender
+ |
+
+ Method
+ |
+
+ Amount
+ |
+
+ Date
+ |
+
+ Status
+ |
+
+
+
+ {paymentsLoading ? (
+
+ |
+ Loading payments...
+ |
+
+ ) : paymentsData?.data && paymentsData.data.length > 0 ? (
+ paymentsData.data.map((payment) => (
+
+ |
+ {payment.transactionId}
+ |
+
+ {payment.senderName || "Unknown"}
+ |
+
+ {payment.paymentMethod}
+ |
+
+ {formatCurrency(payment.amount)}
+ |
+
+ {new Date(payment.paymentDate).toLocaleDateString()}
+ |
+
+ {payment.isFlagged && (
+
+ Flagged
+
+ )}
+ |
+
+ ))
+ ) : (
+
+ |
+ No records found.
+ |
+
+ )}
+
+
+
+
+ {paymentsData?.meta && (
+
+
+ Page {paymentsData.meta.page} of {paymentsData.meta.totalPages}
+
+
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/pages/admin/security/api-keys.tsx b/src/pages/admin/security/api-keys.tsx
index 3eed39e..fd8c0a0 100644
--- a/src/pages/admin/security/api-keys.tsx
+++ b/src/pages/admin/security/api-keys.tsx
@@ -1,102 +1,158 @@
-import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
-import { Button } from "@/components/ui/button"
-import { Badge } from "@/components/ui/badge"
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table"
-import { Ban } from "lucide-react"
-import { securityService, type ApiKey } from "@/services"
-import { toast } from "sonner"
-import { format } from "date-fns"
-import type { ApiError } from "@/types/error.types"
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Ban, Key, Calendar, User, Zap } from "lucide-react";
+import { securityService, type ApiKey } from "@/services";
+import { toast } from "sonner";
+import { format } from "date-fns";
+import type { ApiError } from "@/types/error.types";
+import { cn } from "@/lib/utils";
export default function ApiKeysPage() {
- const queryClient = useQueryClient()
+ const queryClient = useQueryClient();
const { data: apiKeys, isLoading } = useQuery({
- queryKey: ['admin', 'security', 'api-keys'],
+ queryKey: ["admin", "security", "api-keys"],
queryFn: () => securityService.getAllApiKeys(),
- })
+ });
const revokeMutation = useMutation({
mutationFn: (id: string) => securityService.revokeApiKey(id),
onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['admin', 'security', 'api-keys'] })
- toast.success("API key revoked successfully")
+ queryClient.invalidateQueries({
+ queryKey: ["admin", "security", "api-keys"],
+ });
+ toast.success("API access credential revoked");
},
onError: (error) => {
- const apiError = error as ApiError
- toast.error(apiError.response?.data?.message || "Failed to revoke API key")
+ const apiError = error as ApiError;
+ toast.error(
+ apiError.response?.data?.message || "Failed to revoke access",
+ );
},
- })
+ });
return (
-
-
API Keys
+
+
+
+
+ API Gateway
+
+
+ Management of system access credentials and authentication tokens.
+
+
+
-
-
- All API Keys
+
+
+
+ Credential Registry
+
+
-
- {isLoading ? (
- Loading API keys...
- ) : (
- <>
-
-
-
- Name
- User
- Last Used
- Status
- Actions
-
-
-
- {apiKeys?.map((key: ApiKey) => (
-
- {key.name}
- {key.userId || 'N/A'}
-
- {key.lastUsed ? format(new Date(key.lastUsed), 'MMM dd, yyyy') : 'Never'}
-
-
-
- {key.isActive ? 'Active' : 'Revoked'}
-
-
-
+
+
+
+
+
+ |
+ Key Identifier
+ |
+
+ Operator
+ |
+
+ Last Activity
+ |
+
+ Access Status
+ |
+
+ Actions
+ |
+
+
+
+ {isLoading ? (
+
+ |
+ Retrieving secure credentials...
+ |
+
+ ) : apiKeys && apiKeys.length > 0 ? (
+ apiKeys.map((key: ApiKey) => (
+
+ |
+
+
+
+ {key.name}
+
+
+ |
+
+
+
+ {key.userId || "N/A"}
+
+ |
+
+
+
+ {key.lastUsed
+ ? format(new Date(key.lastUsed), "MMM dd, yyyy")
+ : "Inactive"}
+
+ |
+
+
+ {key.isActive ? "Authorized" : "Deactivated"}
+
+ |
+
{key.isActive && (
)}
-
-
- ))}
-
- |
- {apiKeys?.length === 0 && (
-
- No API keys found
-
- )}
- >
- )}
+
+
+ ))
+ ) : (
+
+ |
+ No API access credentials defined.
+ |
+
+ )}
+
+
+
- )
+ );
}
-
diff --git a/src/pages/admin/security/failed-logins.tsx b/src/pages/admin/security/failed-logins.tsx
index c86364d..36a4742 100644
--- a/src/pages/admin/security/failed-logins.tsx
+++ b/src/pages/admin/security/failed-logins.tsx
@@ -1,105 +1,177 @@
-import { useState } from "react"
-import { useQuery } from "@tanstack/react-query"
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
-import { Input } from "@/components/ui/input"
-import { Badge } from "@/components/ui/badge"
-import { Button } from "@/components/ui/button"
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table"
-import { Search, Ban } from "lucide-react"
-import { securityService, type FailedLogin } from "@/services"
-import { format } from "date-fns"
+import { useState } from "react";
+import { useQuery } from "@tanstack/react-query";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+import { Search, Ban, ChevronLeft, ChevronRight, Filter } from "lucide-react";
+import { securityService, type FailedLogin } from "@/services";
+import { format } from "date-fns";
export default function FailedLoginsPage() {
- const [page] = useState(1)
- const [limit] = useState(50)
- const [search, setSearch] = useState("")
+ const [page, setPage] = useState(1);
+ const [limit] = useState(15);
+ const [search, setSearch] = useState("");
const { data: failedLogins, isLoading } = useQuery({
- queryKey: ['admin', 'security', 'failed-logins', page, limit, search],
+ queryKey: ["admin", "security", "failed-logins", page, limit, search],
queryFn: async () => {
- const params: Record
= { page, limit }
- if (search) params.email = search
- return await securityService.getFailedLogins(params)
+ const params: Record = { page, limit };
+ if (search) params.email = search;
+ return await securityService.getFailedLogins(params);
},
- })
+ });
return (
-
-
Failed Login Attempts
+
+
+
+
+ Access Violations
+
+
+ Audit trail for authentication failures and potential threats.
+
+
+
-
-
-
-
Failed Logins
-
-
+
+
+
+ Violation Ledger
+
+
+
+
setSearch(e.target.value)}
/>
+
-
- {isLoading ? (
- Loading failed logins...
- ) : (
- <>
-
-
-
- Email
- IP Address
- User Agent
- Reason
- Attempted At
- Blocked
- Actions
-
-
-
- {failedLogins?.data?.map((login: FailedLogin) => (
-
- {login.email}
- {login.ipAddress}
- {login.ipAddress}
- {login.reason || 'N/A'}
-
- {format(new Date(login.timestamp), 'MMM dd, yyyy HH:mm')}
-
-
-
- N/A
-
-
-
-
+
+ {failedLogins && (
+
+
+ Total Violations: {failedLogins.total || 0}
+
+
+ setPage((p) => Math.max(1, p - 1))}
+ disabled={page === 1}
+ >
+
+
+ setPage((p) => p + 1)}
+ disabled={
+ !failedLogins?.data || failedLogins.data.length < limit
+ }
+ >
+
+
+
+
+ )}
- )
+ );
}
-
diff --git a/src/pages/admin/security/index.tsx b/src/pages/admin/security/index.tsx
index 73d3c12..328641b 100644
--- a/src/pages/admin/security/index.tsx
+++ b/src/pages/admin/security/index.tsx
@@ -1,86 +1,89 @@
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
-import { Shield, AlertTriangle, Key, Gauge, Users } from "lucide-react"
-import { useNavigate } from "react-router-dom"
+import { Card, CardContent, CardHeader } from "@/components/ui/card";
+import {
+ Shield,
+ AlertTriangle,
+ Key,
+ Gauge,
+ Users,
+ ChevronRight,
+} from "lucide-react";
+import { useNavigate } from "react-router-dom";
+import { cn } from "@/lib/utils";
export default function SecurityPage() {
- const navigate = useNavigate()
+ const navigate = useNavigate();
return (
Security
-
navigate('/admin/security/failed-logins')}>
-
-
-
- Failed Logins
-
-
-
-
- View and manage failed login attempts
-
-
-
-
-
navigate('/admin/security/suspicious')}>
-
-
-
- Suspicious Activity
-
-
-
-
- Monitor suspicious IPs and emails
-
-
-
-
-
navigate('/admin/security/api-keys')}>
-
-
-
- API Keys
-
-
-
-
- Manage API keys and tokens
-
-
-
-
-
navigate('/admin/security/rate-limits')}>
-
-
-
- Rate Limits
-
-
-
-
- View rate limit violations
-
-
-
-
-
navigate('/admin/security/sessions')}>
-
-
-
- Active Sessions
-
-
-
-
- Manage active user sessions
-
-
-
+ {[
+ {
+ label: "Failed Logins",
+ description: "View and manage failed login attempts",
+ icon: AlertTriangle,
+ path: "/admin/security/failed-logins",
+ color: "text-rose-600",
+ },
+ {
+ label: "Suspicious Activity",
+ description: "Monitor suspicious IPs and emails",
+ icon: Shield,
+ path: "/admin/security/suspicious",
+ color: "text-amber-600",
+ },
+ {
+ label: "API Keys",
+ description: "Manage API keys and tokens",
+ icon: Key,
+ path: "/admin/security/api-keys",
+ color: "text-blue-600",
+ },
+ {
+ label: "Rate Limits",
+ description: "View rate limit violations",
+ icon: Gauge,
+ path: "/admin/security/rate-limits",
+ color: "text-purple-600",
+ },
+ {
+ label: "Active Sessions",
+ description: "Manage active user sessions",
+ icon: Users,
+ path: "/admin/security/sessions",
+ color: "text-emerald-600",
+ },
+ ].map((item) => (
+
navigate(item.path)}
+ >
+
+
+
+ {item.label}
+
+
+
+
+
+
+
+ {item.label}
+
+
+ {item.description}
+
+
+
+
+
+ ))}
- )
+ );
}
-
diff --git a/src/pages/admin/security/rate-limits.tsx b/src/pages/admin/security/rate-limits.tsx
index 081000d..314c3f6 100644
--- a/src/pages/admin/security/rate-limits.tsx
+++ b/src/pages/admin/security/rate-limits.tsx
@@ -1,65 +1,136 @@
-import { useQuery } from "@tanstack/react-query"
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table"
-import { securityService } from "@/services"
-import type { RateLimitViolation } from "@/types/security.types"
+import { useQuery } from "@tanstack/react-query";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Gauge, Clock, Activity, AlertTriangle } from "lucide-react";
+import { securityService } from "@/services";
+import type { RateLimitViolation } from "@/types/security.types";
export default function RateLimitsPage() {
const { data: violations, isLoading } = useQuery({
- queryKey: ['admin', 'security', 'rate-limits'],
+ queryKey: ["admin", "security", "rate-limits"],
queryFn: () => securityService.getRateLimitViolations(7),
- })
+ });
return (
-
-
Rate Limit Violations
+
+
+
+
+ Traffic Control
+
+
+ Audit of rate limit violations and anomalous request frequencies.
+
+
+
-
-
- Recent Violations (Last 7 Days)
+
+
+
+ Violation Registry
+
+
-
- {isLoading ? (
- Loading violations...
- ) : (
- <>
-
-
-
- User
- IP Address
- Requests
- Period
-
-
-
- {violations?.map((violation: RateLimitViolation) => (
-
- {violation.userId || 'N/A'}
- {violation.ipAddress}
- {violation.requests}
- {violation.period}
-
- ))}
-
-
- {violations?.length === 0 && (
-
- No rate limit violations found
-
- )}
- >
- )}
+
+
+
+
+
+ |
+ Protocol Identifier
+ |
+
+ Network Origin
+ |
+
+ Velocity
+ |
+
+ Reference Period
+ |
+
+ Severity
+ |
+
+
+
+ {isLoading ? (
+
+ |
+ Analyzing traffic patterns...
+ |
+
+ ) : violations && violations.length > 0 ? (
+ violations.map((violation: RateLimitViolation) => (
+
+ |
+
+
+
+ {violation.userId || "PUBLIC_TRANSIT"}
+
+
+ |
+
+
+ {violation.ipAddress}
+
+ |
+
+
+
+ {violation.requests}{" "}
+
+ REQ
+
+
+ |
+
+
+ {violation.period}
+
+ |
+
+
+ Warning
+
+ |
+
+ ))
+ ) : (
+
+ |
+ Traffic volume within nominal limits.
+ |
+
+ )}
+
+
+
-
- )
-}
+
+
+
+
+ Adaptive Throttling Active
+
+
+ System is currently monitoring high-velocity traffic. Automatic
+ blocking protocols will engage if violation frequency exceeds 5% of
+ total ingress.
+
+
+
+
+ );
+}
diff --git a/src/pages/admin/security/sessions.tsx b/src/pages/admin/security/sessions.tsx
index eaea60f..6318a44 100644
--- a/src/pages/admin/security/sessions.tsx
+++ b/src/pages/admin/security/sessions.tsx
@@ -1,75 +1,132 @@
-import { useQuery } from "@tanstack/react-query"
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
-import { Button } from "@/components/ui/button"
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table"
-import { LogOut } from "lucide-react"
-import { securityService, type ActiveSession } from "@/services"
-import { format } from "date-fns"
+import { useQuery } from "@tanstack/react-query";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { LogOut, Monitor, MapPin, Clock } from "lucide-react";
+import { securityService, type ActiveSession } from "@/services";
+import { format } from "date-fns";
export default function SessionsPage() {
const { data: sessions, isLoading } = useQuery({
- queryKey: ['admin', 'security', 'sessions'],
+ queryKey: ["admin", "security", "sessions"],
queryFn: () => securityService.getActiveSessions(),
- })
+ });
return (
-
-
Active Sessions
+
+
+
+
+ Active Sessions
+
+
+ Live oversight of authenticated system access.
+
+
+
-
-
- All Active Sessions
+
+
+
+ Access Registry
+
+
+ {sessions?.length || 0} Authenticated Sessions
+
-
- {isLoading ? (
- Loading sessions...
- ) : (
- <>
-
-
-
- User
- IP Address
- User Agent
- Last Activity
- Actions
-
-
-
- {sessions?.map((session: ActiveSession) => (
-
- {session.userId || 'N/A'}
- {session.ipAddress}
- {session.userAgent}
-
- {format(new Date(session.lastActivity), 'MMM dd, yyyy HH:mm')}
-
-
-
-
+
+
+
+
+
+ |
+ Operator Identity
+ |
+
+ Endpoint
+ |
+
+ Environment
+ |
+
+ Activity Status
+ |
+
+ Control
+ |
+
+
+
+ {isLoading ? (
+
+ |
+ Interrogating active sessions...
+ |
+
+ ) : sessions && sessions.length > 0 ? (
+ sessions.map((session: ActiveSession) => (
+
+ |
+
+
+ {session.userId || "N/A"}
+
+
+ Internal Reference
+
+
+ |
+
+
+
+ {session.ipAddress}
+
+ |
+
+
+
+
+ {session.userAgent}
+
+
+ |
+
+
+
+ {format(new Date(session.lastActivity), "HH:mm:ss")}
+
+ |
+
+
+ Revoke
-
-
- ))}
-
- |
- {sessions?.length === 0 && (
-
- No active sessions found
-
- )}
- >
- )}
+
+
+ ))
+ ) : (
+
+ |
+ No active authenticated sessions.
+ |
+
+ )}
+
+
+
- )
+ );
}
-
diff --git a/src/pages/admin/security/suspicious.tsx b/src/pages/admin/security/suspicious.tsx
index 0f066ce..218932f 100644
--- a/src/pages/admin/security/suspicious.tsx
+++ b/src/pages/admin/security/suspicious.tsx
@@ -1,88 +1,148 @@
-import { useQuery } from "@tanstack/react-query"
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
-import { Button } from "@/components/ui/button"
-import { Shield, Ban } from "lucide-react"
-import { securityService } from "@/services"
-import type { SuspiciousIP, SuspiciousEmail } from "@/types/security.types"
+import { useQuery } from "@tanstack/react-query";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Shield, Ban, Mail, Globe, AlertTriangle } from "lucide-react";
+import { securityService } from "@/services";
+import type { SuspiciousIP, SuspiciousEmail } from "@/types/security.types";
export default function SuspiciousActivityPage() {
const { data: suspicious, isLoading } = useQuery({
- queryKey: ['admin', 'security', 'suspicious'],
+ queryKey: ["admin", "security", "suspicious"],
queryFn: () => securityService.getSuspiciousActivity(),
- })
+ });
return (
-
-
Suspicious Activity
+
+
+
+
+ Anomalous Activity
+
+
+ High-risk identifiers flagged for potential system abuse.
+
+
+
-
-
-
-
-
- Suspicious IP Addresses
-
+
+
+
+
+
+
+ Suspicious Network Ingress
+
+
+
+ Shield Active
+
-
+
{isLoading ? (
- Loading...
+
+ Interrogating global threats...
+
) : (suspicious?.suspiciousIPs?.length ?? 0) > 0 ? (
-
- {suspicious?.suspiciousIPs?.map((ip: SuspiciousIP, index: number) => (
-
-
-
{ip.ipAddress}
-
{ip.attempts} attempts
+
+ {suspicious?.suspiciousIPs?.map(
+ (ip: SuspiciousIP, index: number) => (
+
+
+
+ {ip.ipAddress}
+
+
+ {ip.attempts} Flagged Interactions
+
+
+
+ Block IP
+
-
-
- Block
-
-
- ))}
+ ),
+ )}
) : (
-
- No suspicious IPs found
+
+ No high-risk network sources.
)}
-
-
-
-
- Suspicious Emails
-
+
+
+
+
+
+ Flagged Credentials
+
+
+
+ Monitoring
+
-
+
{isLoading ? (
- Loading...
+
+ Screening identity registry...
+
) : (suspicious?.suspiciousEmails?.length ?? 0) > 0 ? (
-
- {suspicious?.suspiciousEmails?.map((email: SuspiciousEmail, index: number) => (
-
-
-
{email.email}
-
{email.attempts} attempts
+
+ {suspicious?.suspiciousEmails?.map(
+ (email: SuspiciousEmail, index: number) => (
+
+
+
+ {email.email}
+
+
+ {email.attempts} Security Triggers
+
+
+
+ Block Domain
+
-
-
- Block
-
-
- ))}
+ ),
+ )}
) : (
-
- No suspicious emails found
+
+ No suspicious identity triggers.
)}
-
- )
-}
+
+
+
+
+ Protocol Awareness
+
+
+ Flagged items above are generated based on heuristic analysis of
+ failed signatures, geofence violations, and credential stuffing
+ patterns. Actions taken here apply globally to the ingress proxy.
+
+
+
+
+ );
+}
diff --git a/src/pages/admin/settings/index.tsx b/src/pages/admin/settings/index.tsx
index f84e469..b996d21 100644
--- a/src/pages/admin/settings/index.tsx
+++ b/src/pages/admin/settings/index.tsx
@@ -1,211 +1,143 @@
-import { useState } from "react"
-import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import { Label } from "@/components/ui/label"
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
-import { Switch } from "@/components/ui/switch"
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog"
-import { Plus } from "lucide-react"
-import { settingsService, type Setting } from "@/services"
-import { toast } from "sonner"
-import type { ApiError } from "@/types/error.types"
+import { useState } from "react";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { settingsService, type Setting } from "@/services";
+import { toast } from "sonner";
+import type { ApiError } from "@/types/error.types";
+import { cn } from "@/lib/utils";
export default function SettingsPage() {
- const queryClient = useQueryClient()
- const [selectedCategory, setSelectedCategory] = useState("GENERAL")
- const [createDialogOpen, setCreateDialogOpen] = useState(false)
- const [newSetting, setNewSetting] = useState({
- key: "",
- value: "",
- description: "",
- isPublic: false,
- })
+ const queryClient = useQueryClient();
+ const [selectedCategory, setSelectedCategory] = useState("GENERAL");
const { data: settings, isLoading } = useQuery({
- queryKey: ['admin', 'settings', selectedCategory],
+ queryKey: ["admin", "settings", selectedCategory],
queryFn: () => settingsService.getSettings(selectedCategory),
- })
+ });
const updateSettingMutation = useMutation({
- mutationFn: ({ key, value }: { key: string; value: string }) =>
+ mutationFn: ({ key, value }: { key: string; value: string }) =>
settingsService.updateSetting(key, { value }),
onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['admin', 'settings'] })
- toast.success("Setting updated successfully")
+ queryClient.invalidateQueries({ queryKey: ["admin", "settings"] });
+ toast.success("Setting updated successfully");
},
onError: (error) => {
- const apiError = error as ApiError
- toast.error(apiError.response?.data?.message || "Failed to update setting")
+ const apiError = error as ApiError;
+ toast.error(
+ apiError.response?.data?.message || "Failed to update setting",
+ );
},
- })
-
- const createSettingMutation = useMutation({
- mutationFn: (data: {
- key: string
- value: string
- category: string
- description?: string
- isPublic?: boolean
- }) => settingsService.createSetting(data),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['admin', 'settings'] })
- toast.success("Setting created successfully")
- setCreateDialogOpen(false)
- setNewSetting({ key: "", value: "", description: "", isPublic: false })
- },
- onError: (error) => {
- const apiError = error as ApiError
- toast.error(apiError.response?.data?.message || "Failed to create setting")
- },
- })
+ });
const handleSave = (key: string, value: string) => {
- updateSettingMutation.mutate({ key, value })
- }
+ updateSettingMutation.mutate({ key, value });
+ };
- const handleCreate = () => {
- if (!newSetting.key || !newSetting.value) {
- toast.error("Key and value are required")
- return
- }
- createSettingMutation.mutate({
- key: newSetting.key,
- value: newSetting.value,
- category: selectedCategory,
- description: newSetting.description || undefined,
- isPublic: newSetting.isPublic,
- })
- }
+ const categories = [
+ "GENERAL",
+ "EMAIL",
+ "STORAGE",
+ "SECURITY",
+ "API",
+ "FEATURES",
+ ];
return (
-
-
-
System Settings
-
setCreateDialogOpen(true)}>
-
- Create Setting
-
+
+
+
+
+ System Settings
+
+
+ Configure global application parameters.
+
+
+
+ {/* View only access: Create Setting button removed */}
+
-
-
- General
- Email
- Storage
- Security
- API
- Features
+
+
+ {categories.map((cat) => (
+
+ {cat}
+
+ ))}
-
-
-
- {selectedCategory} Settings
+
+
+
+
+ {selectedCategory} Configuration
+
-
+
{isLoading ? (
- Loading settings...
+
+ Fetching system variables...
+
) : settings && settings.length > 0 ? (
settings.map((setting: Setting) => (
-
-
-
+
+
+
+ {setting.description && (
+
+ {setting.description}
+
+ )}
+
+
{
if (e.target.value !== setting.value) {
- handleSave(setting.key, e.target.value)
+ handleSave(setting.key, e.target.value);
}
}}
/>
- {setting.description && (
-
{setting.description}
- )}
))
) : (
-
- No settings found for this category
+
+ No variables defined for this category.
)}
-
- {/* Create Setting Dialog */}
-
- )
+ );
}
-
diff --git a/src/pages/admin/users/index.tsx b/src/pages/admin/users/index.tsx
index 997e904..3b875c3 100644
--- a/src/pages/admin/users/index.tsx
+++ b/src/pages/admin/users/index.tsx
@@ -1,409 +1,213 @@
-import { useState } from "react"
-import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
-import { useNavigate } from "react-router-dom"
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import { Badge } from "@/components/ui/badge"
-import { Label } from "@/components/ui/label"
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table"
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select"
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog"
-import { Search, Download, Eye, UserPlus, Trash2, Key, Upload } from "lucide-react"
-import { userService } from "@/services"
-import { toast } from "sonner"
-import { format } from "date-fns"
-import type { ApiError } from "@/types/error.types"
-
-interface User {
- id: string
- email: string
- firstName: string
- lastName: string
- role: string
- isActive: boolean
- createdAt: string
-}
+import { useState } from "react";
+import { useQuery } from "@tanstack/react-query";
+import { useNavigate } from "react-router-dom";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Badge } from "@/components/ui/badge";
+import { Search, Eye, ChevronLeft, ChevronRight, Filter } from "lucide-react";
+import { userService } from "@/services";
+import { format } from "date-fns";
+import { cn } from "@/lib/utils";
export default function UsersPage() {
- const navigate = useNavigate()
- const queryClient = useQueryClient()
- const [page, setPage] = useState(1)
- const [limit] = useState(20)
- const [search, setSearch] = useState("")
- const [roleFilter, setRoleFilter] = useState
("all")
- const [statusFilter, setStatusFilter] = useState("all")
- const [selectedUser, setSelectedUser] = useState(null)
- const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
- const [resetPasswordDialogOpen, setResetPasswordDialogOpen] = useState(false)
- const [importDialogOpen, setImportDialogOpen] = useState(false)
- const [importFile, setImportFile] = useState(null)
+ const navigate = useNavigate();
+ const [page, setPage] = useState(1);
+ const [limit] = useState(15);
+ const [search, setSearch] = useState("");
+ const [roleFilter, setRoleFilter] = useState("all");
const { data: usersData, isLoading } = useQuery({
- queryKey: ['admin', 'users', page, limit, search, roleFilter, statusFilter],
+ queryKey: ["admin", "users", page, limit, search, roleFilter],
queryFn: async () => {
- const params: Record = { page, limit }
- if (search) params.search = search
- if (roleFilter !== 'all') params.role = roleFilter
- if (statusFilter !== 'all') params.isActive = statusFilter === 'active'
- return await userService.getUsers(params)
+ const params: Record = { page, limit };
+ if (search) params.search = search;
+ if (roleFilter !== "all") params.role = roleFilter;
+ return await userService.getUsers(params);
},
- })
+ });
- const deleteUserMutation = useMutation({
- mutationFn: ({ id, hard }: { id: string; hard: boolean }) =>
- userService.deleteUser(id, hard),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['admin', 'users'] })
- toast.success("User deleted successfully")
- setDeleteDialogOpen(false)
- },
- onError: (error) => {
- const apiError = error as ApiError
- toast.error(apiError.response?.data?.message || "Failed to delete user")
- },
- })
-
- const resetPasswordMutation = useMutation({
- mutationFn: (id: string) => userService.resetPassword(id),
- onSuccess: (data) => {
- toast.success(`Password reset. Temporary password: ${data.temporaryPassword}`)
- setResetPasswordDialogOpen(false)
- },
- onError: (error) => {
- const apiError = error as ApiError
- toast.error(apiError.response?.data?.message || "Failed to reset password")
- },
- })
-
- const importUsersMutation = useMutation({
- mutationFn: (file: File) => userService.importUsers(file),
- onSuccess: (data) => {
- queryClient.invalidateQueries({ queryKey: ['admin', 'users'] })
- toast.success(`Imported ${data.imported} users. ${data.failed} failed.`)
- setImportDialogOpen(false)
- setImportFile(null)
- },
- onError: (error) => {
- const apiError = error as ApiError
- toast.error(apiError.response?.data?.message || "Failed to import users")
- },
- })
-
- const handleExport = async () => {
- try {
- const blob = await userService.exportUsers('csv')
- const url = window.URL.createObjectURL(blob)
- const a = document.createElement('a')
- a.href = url
- a.download = `users-${new Date().toISOString()}.csv`
- a.click()
- toast.success("Users exported successfully")
- } catch (error) {
- const apiError = error as ApiError
- toast.error(apiError.response?.data?.message || "Failed to export users")
- }
- }
-
- const handleDelete = () => {
- if (selectedUser) {
- deleteUserMutation.mutate({ id: selectedUser.id, hard: false })
- }
- }
-
- const handleResetPassword = () => {
- if (selectedUser) {
- resetPasswordMutation.mutate(selectedUser.id)
- }
- }
-
- const handleImport = () => {
- if (importFile) {
- importUsersMutation.mutate(importFile)
- }
- }
-
- const handleFileChange = (e: React.ChangeEvent) => {
- const file = e.target.files?.[0]
- if (file) {
- setImportFile(file)
- }
- }
-
- const getRoleBadgeVariant = (role: string) => {
+ const getRoleBadgeColor = (role: string) => {
switch (role) {
- case 'ADMIN':
- return 'destructive'
- case 'USER':
- return 'default'
- case 'VIEWER':
- return 'secondary'
+ case "ADMIN":
+ return "text-rose-600 bg-rose-50 border-rose-100";
+ case "USER":
+ return "text-blue-600 bg-blue-50 border-blue-100";
+ case "VIEWER":
+ return "text-emerald-600 bg-emerald-50 border-emerald-100";
default:
- return 'outline'
+ return "text-gray-600 bg-gray-50 border-gray-100";
}
- }
+ };
return (
-
-
-
Users Management
-
-
setImportDialogOpen(true)}>
-
- Import Users
-
-
-
- Add User
-
+
+
+
+
+ Users
+
+
+ Manage system access and permissions.
+
+
+
+ {/* View only access: Add User and Import buttons removed */}
-
-
-
-
All Users
-
-
-
- setSearch(e.target.value)}
- />
-
-
-
-
-
- Export
+
+
+
+ User Directory
+
+
+
+
+ setSearch(e.target.value)}
+ />
+
+
+ Filter
+
+
+
+
+
+
+
+
+ |
+ User
+ |
+
+ Role
+ |
+
+ Status
+ |
+
+ Created
+ |
+
+ Actions
+ |
+
+
+
+ {isLoading ? (
+
+ |
+ Retrieving user data...
+ |
+
+ ) : usersData?.data && usersData.data.length > 0 ? (
+ usersData.data.map((user: any) => (
+
+ |
+
+
+ {user.firstName} {user.lastName}
+
+
+ {user.email}
+
+
+ |
+
+
+ {user.role}
+
+ |
+
+
+ {user.isActive ? "Active" : "Inactive"}
+
+ |
+
+ {format(new Date(user.createdAt), "MMM dd, yyyy")}
+ |
+
+ navigate(`/admin/users/${user.id}`)}
+ >
+
+
+ |
+
+ ))
+ ) : (
+
+ |
+ No users found.
+ |
+
+ )}
+
+
+
+
+ {usersData && (
+
+
+ Showing {(page - 1) * limit + 1} to{" "}
+ {Math.min(page * limit, usersData.total)} of {usersData.total}
+
+
+ setPage((p) => Math.max(1, p - 1))}
+ disabled={page === 1}
+ >
+
+
+ setPage((p) => p + 1)}
+ disabled={page * limit >= usersData.total}
+ >
+
-
-
- {isLoading ? (
- Loading users...
- ) : (
- <>
-
-
-
- Email
- Name
- Role
- Status
- Created At
- Actions
-
-
-
- {usersData?.data?.map((user: User) => (
-
- {user.email}
- {user.firstName} {user.lastName}
-
-
- {user.role}
-
-
-
-
- {user.isActive ? 'Active' : 'Inactive'}
-
-
-
- {format(new Date(user.createdAt), 'MMM dd, yyyy')}
-
-
-
- navigate(`/admin/users/${user.id}`)}
- >
-
-
- {
- setSelectedUser(user)
- setResetPasswordDialogOpen(true)
- }}
- >
-
-
- {
- setSelectedUser(user)
- setDeleteDialogOpen(true)
- }}
- >
-
-
-
-
-
- ))}
-
-
- {usersData?.data?.length === 0 && (
-
- No users found
-
- )}
- {usersData && usersData.total > limit && (
-
-
- Showing {(page - 1) * limit + 1} to {Math.min(page * limit, usersData.total)} of {usersData.total} users
-
-
- setPage(p => Math.max(1, p - 1))}
- disabled={page === 1}
- >
- Previous
-
- setPage(p => p + 1)}
- disabled={page * limit >= usersData.total}
- >
- Next
-
-
-
- )}
- >
- )}
-
+ )}
-
- {/* Delete Dialog */}
-
-
- {/* Reset Password Dialog */}
-
-
- {/* Import Users Dialog */}
-
- )
+ );
}
-
diff --git a/src/pages/notifications/index.tsx b/src/pages/notifications/index.tsx
index 2d0e591..39eb255 100644
--- a/src/pages/notifications/index.tsx
+++ b/src/pages/notifications/index.tsx
@@ -1,9 +1,9 @@
-import { useState, useMemo } from "react"
-import { useQuery } from "@tanstack/react-query"
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import { Badge } from "@/components/ui/badge"
+import { useState, useMemo } from "react";
+import { useQuery } from "@tanstack/react-query";
+import { Card, CardContent, CardHeader } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Badge } from "@/components/ui/badge";
import {
Table,
TableBody,
@@ -11,180 +11,166 @@ import {
TableHead,
TableHeader,
TableRow,
-} from "@/components/ui/table"
-import { Search, Download, Eye, CheckCheck, Bell, Loader2 } from "lucide-react"
-import { notificationService } from "@/services/notification.service"
-import { toast } from "sonner"
+} from "@/components/ui/table";
+import {
+ Search,
+ Eye,
+ CheckCheck,
+ Loader2,
+ Calendar,
+ Mail,
+ Tag,
+} from "lucide-react";
+import { notificationService } from "@/services/notification.service";
+import { toast } from "sonner";
+import { format } from "date-fns";
+import { cn } from "@/lib/utils";
export default function NotificationsPage() {
- const [searchQuery, setSearchQuery] = useState("")
- const [typeFilter, setTypeFilter] = useState("")
- const [statusFilter, setStatusFilter] = useState("")
+ const [searchQuery, setSearchQuery] = useState("");
+ const [typeFilter, setTypeFilter] = useState("");
+ const [statusFilter, setStatusFilter] = useState("");
- const { data: notifications, isLoading, refetch } = useQuery({
- queryKey: ['notifications'],
+ const {
+ data: notifications,
+ isLoading,
+ refetch,
+ } = useQuery({
+ queryKey: ["notifications"],
queryFn: () => notificationService.getNotifications(),
- })
+ });
const { data: unreadCount } = useQuery({
- queryKey: ['notifications', 'unread-count'],
+ queryKey: ["notifications", "unread-count"],
queryFn: () => notificationService.getUnreadCount(),
- })
+ });
// Client-side filtering
const filteredNotifications = useMemo(() => {
- if (!notifications) return []
-
+ if (!notifications) return [];
+
return notifications.filter((notification) => {
// Type filter
- if (typeFilter && notification.type !== typeFilter) return false
-
+ if (typeFilter && notification.type !== typeFilter) return false;
+
// Status filter
- if (statusFilter === 'read' && !notification.isRead) return false
- if (statusFilter === 'unread' && notification.isRead) return false
-
+ if (statusFilter === "read" && !notification.isRead) return false;
+ if (statusFilter === "unread" && notification.isRead) return false;
+
// Search filter
if (searchQuery) {
- const query = searchQuery.toLowerCase()
+ const query = searchQuery.toLowerCase();
return (
notification.title.toLowerCase().includes(query) ||
notification.message.toLowerCase().includes(query) ||
notification.recipient.toLowerCase().includes(query)
- )
- }
-
- return true
- })
- }, [notifications, typeFilter, statusFilter, searchQuery])
-
- const handleExport = () => {
- try {
- if (!filteredNotifications || filteredNotifications.length === 0) {
- toast.error("No notifications to export")
- return
+ );
}
- const csvData = [
- ['Notification ID', 'Title', 'Message', 'Type', 'Recipient', 'Status', 'Created Date', 'Read Date'],
- ...filteredNotifications.map(n => [
- n.id,
- n.title,
- n.message,
- n.type,
- n.recipient,
- n.isRead ? 'Read' : 'Unread',
- new Date(n.createdAt).toLocaleString(),
- n.readAt ? new Date(n.readAt).toLocaleString() : '-'
- ])
- ]
-
- const csvContent = csvData.map(row =>
- row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(',')
- ).join('\n')
-
- const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
- const url = window.URL.createObjectURL(blob)
- const link = document.createElement('a')
- link.href = url
- link.download = `notifications-${new Date().toISOString().split('T')[0]}.csv`
- document.body.appendChild(link)
- link.click()
- document.body.removeChild(link)
- window.URL.revokeObjectURL(url)
-
- toast.success("Notifications exported successfully!")
- } catch (error) {
- toast.error("Failed to export notifications")
- console.error('Export error:', error)
- }
- }
+ return true;
+ });
+ }, [notifications, typeFilter, statusFilter, searchQuery]);
const handleMarkAsRead = async (id: string) => {
try {
- await notificationService.markAsRead(id)
- toast.success("Notification marked as read")
- refetch()
+ await notificationService.markAsRead(id);
+ toast.success("Notification marked as read");
+ refetch();
} catch (error) {
- toast.error("Failed to mark notification as read")
- console.error('Mark as read error:', error)
+ toast.error("Failed to mark notification as read");
+ console.error("Mark as read error:", error);
}
- }
+ };
const handleMarkAllAsRead = async () => {
try {
- await notificationService.markAllAsRead()
- toast.success("All notifications marked as read")
- refetch()
+ await notificationService.markAllAsRead();
+ toast.success("All notifications marked as read");
+ refetch();
} catch (error) {
- toast.error("Failed to mark all as read")
- console.error('Mark all as read error:', error)
+ toast.error("Failed to mark all as read");
+ console.error("Mark all as read error:", error);
}
- }
-
- const formatDate = (dateString: string) => {
- return new Date(dateString).toLocaleDateString('en-US', {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- })
- }
-
- const formatDateTime = (dateString?: string) => {
- if (!dateString) return '-'
- return new Date(dateString).toLocaleString('en-US', {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit',
- })
- }
+ };
const getStatusBadge = (isRead: boolean) => {
- return isRead ? 'bg-gray-500' : 'bg-orange-500'
- }
+ return isRead ? (
+
+ Read
+
+ ) : (
+
+ Unread
+
+ );
+ };
+
+ const getTypeIcon = (type: string) => {
+ switch (type.toLowerCase()) {
+ case "system":
+ return
;
+ case "alert":
+ return
;
+ case "invoice":
+ return
;
+ default:
+ return
;
+ }
+ };
return (
-
+
-
Notifications
- {unreadCount !== undefined && unreadCount > 0 && (
-
- You have {unreadCount} unread notification{unreadCount !== 1 ? 's' : ''}
+
Notifications
+ {unreadCount !== undefined && unreadCount > 0 ? (
+
+ You have{" "}
+ {unreadCount}{" "}
+ unread notification{unreadCount !== 1 ? "s" : ""}
+
+ ) : (
+
+ All messages been processed.
)}
{unreadCount !== undefined && unreadCount > 0 && (
-
+
Mark All as Read
)}
-
-
- Settings
-
-
-
-
-
All Notifications
-
-
+
+
+
+
+
setSearchQuery(e.target.value)}
/>
-
-
-
-
- Export
-
-
+
{isLoading ? (
-
-
+
+
) : filteredNotifications && filteredNotifications.length > 0 ? (
-
-
- Notification ID
- Title
- Message
- Type
- Status
- Created Date
- Read Date
- Action
+
+
+
+ ID Reference
+
+
+ Message Intelligence
+
+
+ Classification
+
+
+ State
+
+
+ Timeline
+
+
+ Actions
+
{filteredNotifications.map((notification) => (
-
- {notification.id}
- {notification.title}
- {notification.message}
-
- {notification.type}
+
+
+
+ {notification.id.substring(0, 12)}
+
-
-
- {notification.isRead ? 'Read' : 'Unread'}
-
+
+
+
+ {notification.title}
+
+
+ {notification.message}
+
+
- {formatDate(notification.createdAt)}
- {formatDateTime(notification.readAt)}
-
+
+
+ {getTypeIcon(notification.type)}
+ {notification.type}
+
+
+
+ {getStatusBadge(notification.isRead)}
+
+
+
+
+ {format(
+ new Date(notification.createdAt),
+ "MMM dd, HH:mm",
+ )}
+
+
+
{!notification.isRead && (
- handleMarkAsRead(notification.id)}
- title="Mark as read"
>
@@ -263,15 +288,14 @@ export default function NotificationsPage() {
) : (
-
- {searchQuery || typeFilter || statusFilter
- ? 'No notifications match your filters'
- : 'No notifications found'
- }
+
+ {searchQuery || typeFilter || statusFilter
+ ? "No matching telemetry records found"
+ : "No notification stream detected"}
)}
- )
+ );
}
diff --git a/src/services/api/client.ts b/src/services/api/client.ts
index 5559da5..c42adb6 100644
--- a/src/services/api/client.ts
+++ b/src/services/api/client.ts
@@ -1,87 +1,92 @@
-import axios, { type AxiosInstance, type AxiosError, type InternalAxiosRequestConfig } from 'axios'
+import axios, {
+ type AxiosInstance,
+ type AxiosError,
+ type InternalAxiosRequestConfig,
+} from "axios";
-const API_BASE_URL = import.meta.env.VITE_BACKEND_API_URL || 'http://localhost:3001/api/v1'
+const API_BASE_URL =
+ import.meta.env.VITE_BACKEND_API_URL || "http://localhost:3001";
interface RetryableAxiosRequestConfig extends InternalAxiosRequestConfig {
- _retry?: boolean
+ _retry?: boolean;
}
// Create axios instance with default config
const apiClient: AxiosInstance = axios.create({
baseURL: API_BASE_URL,
headers: {
- 'Content-Type': 'application/json',
+ "Content-Type": "application/json",
},
withCredentials: true, // Send cookies with requests
timeout: 30000, // 30 second timeout
paramsSerializer: {
serialize: (params) => {
// Custom serializer to preserve number types
- const searchParams = new URLSearchParams()
+ const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
- searchParams.append(key, String(value))
+ searchParams.append(key, String(value));
}
- })
- return searchParams.toString()
- }
- }
-})
+ });
+ return searchParams.toString();
+ },
+ },
+});
// Request interceptor - Add auth token
apiClient.interceptors.request.use(
(config) => {
// Add token from localStorage as fallback (cookies are preferred)
- const token = localStorage.getItem('access_token')
+ const token = localStorage.getItem("access_token");
if (token) {
- config.headers.Authorization = `Bearer ${token}`
+ config.headers.Authorization = `Bearer ${token}`;
}
- return config
+ return config;
},
(error) => {
- return Promise.reject(error)
- }
-)
+ return Promise.reject(error);
+ },
+);
// Response interceptor - Handle errors and token refresh
apiClient.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
- const originalRequest = error.config as RetryableAxiosRequestConfig
+ const originalRequest = error.config as RetryableAxiosRequestConfig;
// Handle 401 Unauthorized - Try to refresh token
if (error.response?.status === 401 && !originalRequest._retry) {
- originalRequest._retry = true
+ originalRequest._retry = true;
try {
// Try to refresh token
- const refreshToken = localStorage.getItem('refresh_token')
+ const refreshToken = localStorage.getItem("refresh_token");
if (refreshToken) {
const response = await axios.post(
`${API_BASE_URL}/auth/refresh`,
{ refreshToken },
- { withCredentials: true }
- )
+ { withCredentials: true },
+ );
- const { accessToken } = response.data
- localStorage.setItem('access_token', accessToken)
+ const { accessToken } = response.data;
+ localStorage.setItem("access_token", accessToken);
// Retry original request with new token
- originalRequest.headers.Authorization = `Bearer ${accessToken}`
- return apiClient(originalRequest)
+ originalRequest.headers.Authorization = `Bearer ${accessToken}`;
+ return apiClient(originalRequest);
}
} catch (refreshError) {
// Refresh failed - logout user
- localStorage.removeItem('access_token')
- localStorage.removeItem('refresh_token')
- localStorage.removeItem('user')
- window.location.href = '/login'
- return Promise.reject(refreshError)
+ localStorage.removeItem("access_token");
+ localStorage.removeItem("refresh_token");
+ localStorage.removeItem("user");
+ window.location.href = "/login";
+ return Promise.reject(refreshError);
}
}
- return Promise.reject(error)
- }
-)
+ return Promise.reject(error);
+ },
+);
-export default apiClient
+export default apiClient;
diff --git a/src/services/dashboard.service.ts b/src/services/dashboard.service.ts
index 023b631..29ca2b3 100644
--- a/src/services/dashboard.service.ts
+++ b/src/services/dashboard.service.ts
@@ -1,21 +1,57 @@
-import apiClient from './api/client'
-import type { ActivityLog } from '@/types/activity.types'
+import apiClient from "./api/client";
+import type { ActivityLog } from "@/types/activity.types";
export interface UserDashboardStats {
- totalInvoices: number
- totalTransactions: number
- totalRevenue: number
- pendingInvoices: number
- growthPercentage?: number
- recentActivity?: ActivityLog[]
+ totalInvoices: number;
+ totalTransactions: number;
+ totalRevenue: number;
+ pendingInvoices: number;
+ growthPercentage?: number;
+ recentActivity?: ActivityLog[];
+}
+
+export interface DashboardMetrics {
+ totalInvoices: number;
+ totalRevenue: number;
+ totalPayments: number;
+ pendingInvoices: number;
+ overdueInvoices: number;
+ paidInvoices: number;
+}
+
+export interface ScannedInvoice {
+ id: string;
+ invoiceNumber: string;
+ customerName: string;
+ amount: number;
+ issueDate: string;
+ scannedData: Record
;
+}
+
+export interface SalesPurchaseComparison {
+ sales: {
+ status: string;
+ count: number;
+ total: number;
+ }[];
+ purchases: {
+ status: string;
+ count: number;
+ total: number;
+ }[];
+}
+
+export interface InvoiceStatusBreakdown {
+ status: string;
+ count: number;
}
export interface UserProfile {
- id: string
- email: string
- firstName: string
- lastName: string
- role: string
+ id: string;
+ email: string;
+ firstName: string;
+ lastName: string;
+ role: string;
}
class DashboardService {
@@ -23,37 +59,76 @@ class DashboardService {
* Get current user profile
*/
async getUserProfile(): Promise {
- const response = await apiClient.get('/user/profile')
- return response.data
+ const response = await apiClient.get("/user/profile");
+ return response.data;
}
/**
* Get user dashboard statistics
*/
async getUserStats(): Promise {
- const response = await apiClient.get('/user/stats')
- return response.data
+ const response = await apiClient.get("/user/stats");
+ return response.data;
}
/**
* Get user recent activity
*/
async getRecentActivity(limit: number = 10): Promise {
- const response = await apiClient.get('/user/activity', {
+ const response = await apiClient.get("/user/activity", {
params: { limit },
- })
- return response.data
+ });
+ return response.data;
}
/**
* Export user dashboard data
*/
async exportData(): Promise {
- const response = await apiClient.get('/user/export', {
- responseType: 'blob',
- })
- return response.data
+ const response = await apiClient.get("/user/export", {
+ responseType: "blob",
+ });
+ return response.data;
+ }
+
+ /**
+ * Get main dashboard metrics
+ */
+ async getMetrics(): Promise {
+ const response =
+ await apiClient.get("/dashboard/metrics");
+ return response.data;
+ }
+
+ /**
+ * Get scanned invoices pending verification
+ */
+ async getScannedInvoices(): Promise {
+ const response = await apiClient.get(
+ "/dashboard/scanned-invoices",
+ );
+ return response.data;
+ }
+
+ /**
+ * Get sales vs purchase comparison
+ */
+ async getSalesPurchaseComparison(): Promise {
+ const response = await apiClient.get(
+ "/dashboard/sales-purchase",
+ );
+ return response.data;
+ }
+
+ /**
+ * Get invoice status breakdown
+ */
+ async getInvoiceStatusBreakdown(): Promise {
+ const response = await apiClient.get(
+ "/dashboard/invoice-status",
+ );
+ return response.data;
}
}
-export const dashboardService = new DashboardService()
+export const dashboardService = new DashboardService();
diff --git a/src/services/index.ts b/src/services/index.ts
index c6991e7..32df9b2 100644
--- a/src/services/index.ts
+++ b/src/services/index.ts
@@ -1,23 +1,64 @@
// Export all services from a single entry point
-export { authService } from './auth.service'
-export { userService } from './user.service'
-export { analyticsService } from './analytics.service'
-export { securityService } from './security.service'
-export { systemService } from './system.service'
-export { announcementService } from './announcement.service'
-export { auditService } from './audit.service'
-export { settingsService } from './settings.service'
-export { dashboardService } from './dashboard.service'
-export { notificationService } from './notification.service'
+export { authService } from "./auth.service";
+export { userService } from "./user.service";
+export { analyticsService } from "./analytics.service";
+export { securityService } from "./security.service";
+export { systemService } from "./system.service";
+export { announcementService } from "./announcement.service";
+export { auditService } from "./audit.service";
+export { settingsService } from "./settings.service";
+export { dashboardService } from "./dashboard.service";
+export { notificationService } from "./notification.service";
+export { paymentService } from "./payment.service";
+export { invoiceService } from "./invoice.service";
// Export types
-export type { LoginRequest, LoginResponse } from './auth.service'
-export type { User, GetUsersParams, PaginatedResponse } from './user.service'
-export type { OverviewStats, UserGrowthData, RevenueData } from './analytics.service'
-export type { SuspiciousActivity, ActiveSession, FailedLogin, ApiKey } from './security.service'
-export type { HealthStatus, SystemInfo, MaintenanceStatus } from './system.service'
-export type { Announcement, CreateAnnouncementData, UpdateAnnouncementData } from './announcement.service'
-export type { AuditLog, GetAuditLogsParams, AuditStats } from './audit.service'
-export type { Setting, CreateSettingData, UpdateSettingData } from './settings.service'
-export type { UserDashboardStats, UserProfile } from './dashboard.service'
-export type { Notification, NotificationSettings } from './notification.service'
+export type { LoginRequest, LoginResponse } from "./auth.service";
+export type { User, GetUsersParams, PaginatedResponse } from "./user.service";
+export type {
+ OverviewStats,
+ UserGrowthData,
+ RevenueData,
+} from "./analytics.service";
+export type {
+ SuspiciousActivity,
+ ActiveSession,
+ FailedLogin,
+ ApiKey,
+} from "./security.service";
+export type {
+ HealthStatus,
+ SystemInfo,
+ MaintenanceStatus,
+} from "./system.service";
+export type {
+ Announcement,
+ CreateAnnouncementData,
+ UpdateAnnouncementData,
+} from "./announcement.service";
+export type { AuditLog, GetAuditLogsParams, AuditStats } from "./audit.service";
+export type {
+ Setting,
+ CreateSettingData,
+ UpdateSettingData,
+} from "./settings.service";
+export type { UserDashboardStats, UserProfile } from "./dashboard.service";
+export type {
+ Notification,
+ NotificationSettings,
+} from "./notification.service";
+export type {
+ Payment,
+ PaymentRequest,
+ PaymentFilters,
+ PaymentRequestFilters,
+} from "./payment.service";
+export type {
+ Invoice,
+ Proforma,
+ ProformaRequest,
+ ProformaRequestItem,
+ InvoiceFilters,
+ ProformaFilters,
+ ProformaRequestFilters,
+} from "./invoice.service";
diff --git a/src/services/invoice.service.ts b/src/services/invoice.service.ts
new file mode 100644
index 0000000..8548643
--- /dev/null
+++ b/src/services/invoice.service.ts
@@ -0,0 +1,178 @@
+import apiClient from "./api/client";
+
+export interface PaginatedResponse {
+ data: T[];
+ meta: {
+ total: number;
+ page: number;
+ limit: number;
+ totalPages: number;
+ hasNextPage: boolean;
+ hasPreviousPage: boolean;
+ };
+}
+
+export interface InvoiceItem {
+ id: string;
+ description: string;
+ quantity: number;
+ unitPrice: number;
+ total: number;
+}
+
+export interface Invoice {
+ id: string;
+ invoiceNumber: string;
+ customerName: string;
+ customerEmail: string;
+ customerPhone: string;
+ amount: number;
+ currency: string;
+ type: "SALES" | "PURCHASE";
+ status: "DRAFT" | "PENDING" | "PAID" | "OVERDUE" | "CANCELLED";
+ issueDate: string;
+ dueDate: string;
+ paidDate?: string;
+ description: string;
+ notes: string;
+ taxAmount: number;
+ discountAmount: number;
+ isScanned: boolean;
+ scannedData?: any;
+ userId: string;
+ items: InvoiceItem[];
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface Proforma {
+ id: string;
+ proformaNumber: string;
+ customerName: string;
+ customerEmail: string;
+ customerPhone: string;
+ amount: number;
+ currency: string;
+ issueDate: string;
+ dueDate: string;
+ description: string;
+ notes: string;
+ taxAmount: number;
+ discountAmount: number;
+ pdfPath: string;
+ userId: string;
+ items: InvoiceItem[];
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface ProformaRequestItem {
+ id: string;
+ itemName: string;
+ quantity: number;
+ unitOfMeasure: string;
+ createdAt: string;
+}
+
+export interface ProformaRequest {
+ id: string;
+ requesterId: string;
+ title: string;
+ description: string;
+ category: "EQUIPMENT" | "SERVICE" | "MIXED";
+ status:
+ | "DRAFT"
+ | "OPEN"
+ | "UNDER_REVIEW"
+ | "REVISION_REQUESTED"
+ | "CLOSED"
+ | "CANCELLED";
+ submissionDeadline: string;
+ allowRevisions: boolean;
+ paymentTerms: string;
+ taxIncluded: boolean;
+ items: ProformaRequestItem[];
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface InvoiceFilters {
+ page?: number;
+ limit?: number;
+ status?: string;
+ type?: string;
+ startDate?: string;
+ endDate?: string;
+ customerName?: string;
+ invoiceNumber?: string;
+ search?: string;
+}
+
+export interface ProformaFilters {
+ page?: number;
+ limit?: number;
+ startDate?: string;
+ endDate?: string;
+ customerName?: string;
+ proformaNumber?: string;
+ search?: string;
+}
+
+export interface ProformaRequestFilters {
+ page?: number;
+ limit?: number;
+ status?: string;
+ category?: string;
+ search?: string;
+ deadlineFrom?: string;
+ deadlineTo?: string;
+}
+
+class InvoiceService {
+ /**
+ * Get all invoices
+ */
+ async getInvoices(
+ filters: InvoiceFilters = {},
+ ): Promise> {
+ const response = await apiClient.get>(
+ "/invoices",
+ {
+ params: filters,
+ },
+ );
+ return response.data;
+ }
+
+ /**
+ * Get all proforma invoices
+ */
+ async getProformas(
+ filters: ProformaFilters = {},
+ ): Promise> {
+ const response = await apiClient.get>(
+ "/proforma",
+ {
+ params: filters,
+ },
+ );
+ return response.data;
+ }
+
+ /**
+ * Get all proforma requests (admin view)
+ */
+ async getProformaRequests(
+ filters: ProformaRequestFilters = {},
+ ): Promise> {
+ const response = await apiClient.get>(
+ "/admin/proforma-requests",
+ {
+ params: filters,
+ },
+ );
+ return response.data;
+ }
+}
+
+export const invoiceService = new InvoiceService();
diff --git a/src/services/payment.service.ts b/src/services/payment.service.ts
new file mode 100644
index 0000000..946c7cf
--- /dev/null
+++ b/src/services/payment.service.ts
@@ -0,0 +1,118 @@
+import apiClient from "./api/client";
+
+export interface Payment {
+ id: string;
+ transactionId: string;
+ amount: number;
+ currency: string;
+ paymentDate: string;
+ paymentMethod: string;
+ notes: string;
+ isFlagged: boolean;
+ flagReason: string;
+ flagNotes: string;
+ receiptPath: string;
+ senderName: string;
+ senderId: string;
+ receiverName: string;
+ receiverId: string;
+ userId: string;
+ invoiceId: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface PaymentRequest {
+ id: string;
+ paymentRequestNumber: string;
+ customerName: string;
+ customerEmail: string;
+ customerPhone: string;
+ amount: number;
+ currency: string;
+ issueDate: string;
+ dueDate: string;
+ description: string;
+ notes: string;
+ taxAmount: number;
+ discountAmount: number;
+ openedCount: number;
+ copiedAccountCount: number;
+ status: "DRAFT" | "SENT" | "OPENED" | "PAID" | "EXPIRED" | "CANCELLED";
+ paymentId: string;
+ accounts: any[];
+ pdfPath: string;
+ userId: string;
+ items: {
+ id: string;
+ description: string;
+ quantity: number;
+ unitPrice: number;
+ total: number;
+ }[];
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface PaginatedResponse {
+ data: T[];
+ meta: {
+ total: number;
+ page: number;
+ limit: number;
+ totalPages: number;
+ hasNextPage: boolean;
+ hasPreviousPage: boolean;
+ };
+}
+
+export interface PaymentFilters {
+ page?: number;
+ limit?: number;
+ invoiceId?: string;
+}
+
+export interface PaymentRequestFilters {
+ page?: number;
+ limit?: number;
+ startDate?: string;
+ endDate?: string;
+ customerName?: string;
+ paymentRequestNumber?: string;
+ status?: string;
+ search?: string;
+}
+
+class PaymentService {
+ /**
+ * Get all payments
+ */
+ async getPayments(
+ filters: PaymentFilters = {},
+ ): Promise> {
+ const response = await apiClient.get>(
+ "/payments",
+ {
+ params: filters,
+ },
+ );
+ return response.data;
+ }
+
+ /**
+ * Get payment requests
+ */
+ async getPaymentRequests(
+ filters: PaymentRequestFilters = {},
+ ): Promise> {
+ const response = await apiClient.get>(
+ "/payment-requests",
+ {
+ params: filters,
+ },
+ );
+ return response.data;
+ }
+}
+
+export const paymentService = new PaymentService();