From edb631df36f6c22e3bcf598ccc38dc7290d71966 Mon Sep 17 00:00:00 2001 From: Yashwin Date: Thu, 16 Apr 2026 19:29:49 +0530 Subject: [PATCH] feat: implement failed emails management module with resend and deletion capabilities for superadmin and tenant dashboards --- src/components/layout/Sidebar.tsx | 5 + src/components/shared/FailedEmailsTable.tsx | 226 ++++++++++++++++++++ src/pages/superadmin/FailedEmails.tsx | 12 ++ src/pages/tenant/FailedEmails.tsx | 12 ++ src/routes/super-admin-routes.tsx | 5 + src/routes/tenant-admin-routes.tsx | 5 + src/services/failed-emails-service.ts | 47 ++++ 7 files changed, 312 insertions(+) create mode 100644 src/components/shared/FailedEmailsTable.tsx create mode 100644 src/pages/superadmin/FailedEmails.tsx create mode 100644 src/pages/tenant/FailedEmails.tsx create mode 100644 src/services/failed-emails-service.ts diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 3813f9a..004cc3d 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -75,6 +75,7 @@ const superAdminSystemMenu: MenuItem[] = [ isGroup: true, children: [ { label: "SMTP Config", path: "/settings/smtp" }, + { label: "Failed Emails", path: "/settings/failed-emails" }, ], }, ]; @@ -214,6 +215,10 @@ const tenantAdminSystemMenu: MenuItem[] = [ { label: "SMTP Settings", path: "/tenant/settings/smtp", + }, + { + label: "Failed Emails", + path: "/tenant/settings/failed-emails", } ], requiredPermission: { resource: "tenants" }, diff --git a/src/components/shared/FailedEmailsTable.tsx b/src/components/shared/FailedEmailsTable.tsx new file mode 100644 index 0000000..4c696b8 --- /dev/null +++ b/src/components/shared/FailedEmailsTable.tsx @@ -0,0 +1,226 @@ +import React, { useState, useEffect } from 'react'; +import { failedEmailsService, type FailedEmail } from '../../services/failed-emails-service'; +import { DataTable } from './DataTable'; +import { Modal } from './Modal'; +import { Button } from '@/components/ui/button'; +import { StatusBadge } from './StatusBadge'; +import { Eye, RefreshCw, Trash2, Loader2 } from 'lucide-react'; +import { format } from 'date-fns'; +import { toast } from 'sonner'; +import { Pagination } from './Pagination'; + +export const FailedEmailsTable: React.FC = () => { + const [emails, setEmails] = useState([]); + const [loading, setLoading] = useState(true); + const [total, setTotal] = useState(0); + const [currentPage, setCurrentPage] = useState(1); + const [limit, setLimit] = useState(50); + + const [selectedEmail, setSelectedEmail] = useState(null); + const [isModalVisible, setIsModalVisible] = useState(false); + const [isResendingAll, setIsResendingAll] = useState(false); + const [resendingId, setResendingId] = useState(null); + + const fetchEmails = async (page = 1, currentLimit = limit) => { + setLoading(true); + try { + const offset = (page - 1) * currentLimit; + const res = await failedEmailsService.getFailedEmails(currentLimit, offset); + setEmails(res.data || []); + setTotal(res.total || 0); + setCurrentPage(page); + } catch (error: any) { + toast.error('Error fetching failed emails', { + description: error.message + }); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchEmails(); + }, []); + + const handleResend = async (id: string) => { + setResendingId(id); + try { + await failedEmailsService.resendEmail(id); + toast.success('Email resent successfully.'); + fetchEmails(currentPage); + } catch (error: any) { + toast.error('Failed to resend email', { description: error.message }); + } finally { + setResendingId(null); + } + }; + + const handleResendAll = async () => { + setIsResendingAll(true); + try { + const res = await failedEmailsService.resendAll(); + toast.success(res.message); + fetchEmails(currentPage); + } catch (error: any) { + toast.error('Failed to resend all emails', { description: error.message }); + } finally { + setIsResendingAll(false); + } + }; + + const handleDelete = async (id: string) => { + try { + await failedEmailsService.deleteEmail(id); + toast.success('Email deleted successfully.'); + fetchEmails(currentPage); + } catch (error: any) { + toast.error('Failed to delete email', { description: error.message }); + } + }; + + const showEmailDetails = (email: FailedEmail) => { + setSelectedEmail(email); + setIsModalVisible(true); + }; + + const columns = [ + { + label: 'Date', + key: 'created_at', + render: (record: FailedEmail) => format(new Date(record.created_at), 'yyyy-MM-dd HH:mm:ss') + }, + { + label: 'To', + key: 'to_email', + render: (record: FailedEmail) => record.to_email + }, + { + label: 'Subject', + key: 'subject', + render: (record: FailedEmail) => record.subject + }, + { + label: 'Status', + key: 'status', + render: (record: FailedEmail) => ( + + {record.status} + + ) + }, + { + label: 'Actions', + key: 'actions', + render: (record: FailedEmail) => ( +
+ + {record.status === 'failed' && ( + + )} + +
+ ) + } + ]; + + return ( +
+
+

Failed Emails Log

+
+ + +
+
+ +
+ item.id} + isLoading={loading} + emptyMessage="No failed emails found" + /> + + {total > limit && ( + fetchEmails(page)} + onLimitChange={(newLimit) => { + setLimit(newLimit); + fetchEmails(1, newLimit); + }} + /> + )} +
+ + setIsModalVisible(false)} + maxWidth="xl" + > + {selectedEmail && ( +
+
+

To: {selectedEmail.to_email}

+

Subject: {selectedEmail.subject}

+

Error Message: {selectedEmail.error_message}

+
+
+ Body: +
+
+
+ )} + +
+ ); +}; diff --git a/src/pages/superadmin/FailedEmails.tsx b/src/pages/superadmin/FailedEmails.tsx new file mode 100644 index 0000000..a1c9f07 --- /dev/null +++ b/src/pages/superadmin/FailedEmails.tsx @@ -0,0 +1,12 @@ +import { FailedEmailsTable } from "@/components/shared/FailedEmailsTable"; +import { Layout } from "@/components/layout/Layout"; + +export default function FailedEmails() { + return ( + +
+ +
+
+ ); +} diff --git a/src/pages/tenant/FailedEmails.tsx b/src/pages/tenant/FailedEmails.tsx new file mode 100644 index 0000000..a1c9f07 --- /dev/null +++ b/src/pages/tenant/FailedEmails.tsx @@ -0,0 +1,12 @@ +import { FailedEmailsTable } from "@/components/shared/FailedEmailsTable"; +import { Layout } from "@/components/layout/Layout"; + +export default function FailedEmails() { + return ( + +
+ +
+
+ ); +} diff --git a/src/routes/super-admin-routes.tsx b/src/routes/super-admin-routes.tsx index abf3b6b..d470b0d 100644 --- a/src/routes/super-admin-routes.tsx +++ b/src/routes/super-admin-routes.tsx @@ -18,6 +18,7 @@ const AuditLogResourceTypes = lazy(() => import("@/pages/superadmin/AuditLogReso const NotificationMaster = lazy(() => import("@/pages/superadmin/NotificationMaster")); const NotificationTemplateMaster = lazy(() => import("@/pages/superadmin/NotificationTemplateMaster")); const SmtpConfig = lazy(() => import("@/pages/superadmin/SmtpConfig")); +const FailedEmails = lazy(() => import("@/pages/superadmin/FailedEmails")); // Loading fallback component const RouteLoader = (): ReactElement => ( @@ -100,4 +101,8 @@ export const superAdminRoutes: RouteConfig[] = [ path: "/settings/smtp", element: , }, + { + path: "/settings/failed-emails", + element: , + }, ]; diff --git a/src/routes/tenant-admin-routes.tsx b/src/routes/tenant-admin-routes.tsx index 07af6fb..65de163 100644 --- a/src/routes/tenant-admin-routes.tsx +++ b/src/routes/tenant-admin-routes.tsx @@ -35,6 +35,7 @@ const FilesList = lazy(() => import("@/pages/tenant/FilesList")); const FileView = lazy(() => import("@/pages/tenant/FileView")); const StorageDashboard = lazy(() => import("@/pages/tenant/StorageDashboard")); const SmtpSettings = lazy(() => import("@/pages/tenant/SmtpSettings")); +const FailedEmails = lazy(() => import("@/pages/tenant/FailedEmails")); // Loading fallback component const RouteLoader = (): ReactElement => ( @@ -161,4 +162,8 @@ export const tenantAdminRoutes: RouteConfig[] = [ path: "/tenant/settings/smtp", element: , }, + { + path: "/tenant/settings/failed-emails", + element: , + }, ]; diff --git a/src/services/failed-emails-service.ts b/src/services/failed-emails-service.ts new file mode 100644 index 0000000..013fb22 --- /dev/null +++ b/src/services/failed-emails-service.ts @@ -0,0 +1,47 @@ +import api from './api-client'; + +export interface FailedEmail { + id: string; + tenant_id: string | null; + user_id: string | null; + notification_id: string | null; + to_email: string; + subject: string; + body: string; + error_message: string; + status: string; + created_at: string; + updated_at: string; + first_name?: string; + last_name?: string; + user_email?: string; +} + +export interface FailedEmailsResponse { + data: FailedEmail[]; + total: number; +} + +class FailedEmailsService { + async getFailedEmails(limit = 50, offset = 0): Promise { + const response = await api.get('/failed-emails', { params: { limit, offset } }); + return response.data; + } + + async resendEmail(id: string): Promise<{ success: boolean; message: string }> { + const response = await api.post(`/failed-emails/${id}/resend`); + return response.data; + } + + async resendAll(): Promise<{ success: boolean; message: string }> { + const response = await api.post('/failed-emails/resend-all'); + return response.data; + } + + async deleteEmail(id: string): Promise<{ success: boolean; message: string }> { + const response = await api.delete(`/failed-emails/${id}`); + return response.data; + } +} + +export const failedEmailsService = new FailedEmailsService();