refactor: update FailedEmails layout with headers and enhance table actions using dropdowns

This commit is contained in:
Yashwin 2026-05-18 12:06:32 +05:30
parent 6952a7c6f3
commit 793fa23c1b
5 changed files with 90 additions and 78 deletions

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { failedEmailsService, type FailedEmail } from '../../services/failed-emails-service'; import { failedEmailsService, type FailedEmail } from '../../services/failed-emails-service';
import { DataTable } from './DataTable'; import { DataTable, type Column } from './DataTable';
import { Modal } from './Modal'; import { Modal } from './Modal';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { StatusBadge } from './StatusBadge'; import { StatusBadge } from './StatusBadge';
@ -8,6 +8,8 @@ import { Eye, RefreshCw, Trash2, Loader2 } from 'lucide-react';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Pagination } from './Pagination'; import { Pagination } from './Pagination';
import { ActionDropdown } from './ActionDropdown';
import { PrimaryButton } from './PrimaryButton';
export const FailedEmailsTable: React.FC = () => { export const FailedEmailsTable: React.FC = () => {
const [emails, setEmails] = useState<FailedEmail[]>([]); const [emails, setEmails] = useState<FailedEmail[]>([]);
@ -84,7 +86,7 @@ export const FailedEmailsTable: React.FC = () => {
setIsModalVisible(true); setIsModalVisible(true);
}; };
const columns = [ const columns: Column<FailedEmail>[] = [
{ {
label: 'Date', label: 'Date',
key: 'created_at', key: 'created_at',
@ -112,70 +114,69 @@ export const FailedEmailsTable: React.FC = () => {
{ {
label: 'Actions', label: 'Actions',
key: 'actions', key: 'actions',
align: 'right',
render: (record: FailedEmail) => ( render: (record: FailedEmail) => (
<div className="flex items-center gap-2"> <div className="flex justify-end">
<Button <ActionDropdown
variant="ghost" actions={[
size="icon" {
onClick={() => showEmailDetails(record)} label: 'View Details',
title="View Details" onClick: () => showEmailDetails(record),
> icon: <Eye className="w-3.5 h-3.5 text-gray-500" />
<Eye className="w-4 h-4" /> },
</Button> ...(record.status === 'failed'
{record.status === 'failed' && ( ? [
<Button {
variant="outline" label: resendingId === record.id ? 'Resending...' : 'Resend Email',
size="icon" onClick: () => handleResend(record.id),
onClick={() => handleResend(record.id)} icon: resendingId === record.id ? (
title="Resend Email" <Loader2 className="w-3.5 h-3.5 animate-spin text-blue-600" />
disabled={resendingId === record.id}
>
{resendingId === record.id ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : ( ) : (
<RefreshCw className="w-4 h-4" /> <RefreshCw className="w-3.5 h-3.5 text-gray-500" />
)} )
</Button> }
)} ]
<Button : []),
variant="destructive" {
size="icon" label: 'Delete Email',
onClick={() => handleDelete(record.id)} onClick: () => handleDelete(record.id),
title="Delete Email" icon: <Trash2 className="w-3.5 h-3.5 text-red-600" />,
> variant: 'danger'
<Trash2 className="w-4 h-4" /> }
</Button> ]}
/>
</div> </div>
) )
} }
]; ];
return ( return (
<div className="p-4 sm:p-6 lg:p-8 bg-white min-h-[calc(100vh-64px)] overflow-hidden"> <div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden">
<div className="mb-6 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4"> {/* Toolbar / Actions Header */}
<h2 className="text-xl font-bold text-[#0f1724]">Failed Emails Log</h2> <div className="border-b border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3 flex flex-col sm:flex-row justify-end items-center gap-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 w-full sm:w-auto justify-end">
<Button variant="outline" onClick={() => fetchEmails(currentPage)}> <Button variant="outline" size="sm" onClick={() => fetchEmails(currentPage)}>
<RefreshCw className="mr-2 w-4 h-4" /> <RefreshCw className="mr-2 w-3.5 h-3.5" />
Refresh Refresh
</Button> </Button>
<Button <PrimaryButton
size="small"
onClick={handleResendAll} onClick={handleResendAll}
disabled={isResendingAll || emails.filter(e => e.status === 'failed').length === 0} disabled={isResendingAll || emails.filter(e => e.status === 'failed').length === 0}
> >
{isResendingAll ? ( {isResendingAll ? (
<> <>
<Loader2 className="mr-2 w-4 h-4 animate-spin" /> <Loader2 className="mr-2 w-3.5 h-3.5 animate-spin" />
Resending... Resending...
</> </>
) : ( ) : (
'Resend All Failed' 'Resend All Failed'
)} )}
</Button> </PrimaryButton>
</div> </div>
</div> </div>
<div className="border border-[rgba(0,0,0,0.08)] bg-white rounded-lg shadow-sm"> {/* Table Section */}
<DataTable <DataTable
columns={columns} columns={columns}
data={emails} data={emails}
@ -185,7 +186,9 @@ export const FailedEmailsTable: React.FC = () => {
error={error} error={error}
/> />
{/* Pagination Footer */}
{total > limit && ( {total > limit && (
<div className="border-t border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3">
<Pagination <Pagination
currentPage={currentPage} currentPage={currentPage}
totalPages={Math.ceil(total / limit)} totalPages={Math.ceil(total / limit)}
@ -197,9 +200,10 @@ export const FailedEmailsTable: React.FC = () => {
fetchEmails(1, newLimit); fetchEmails(1, newLimit);
}} }}
/> />
)}
</div> </div>
)}
{/* Modal Details */}
<Modal <Modal
title="Email Details" title="Email Details"
isOpen={isModalVisible} isOpen={isModalVisible}
@ -209,12 +213,12 @@ export const FailedEmailsTable: React.FC = () => {
{selectedEmail && ( {selectedEmail && (
<div className="p-5"> <div className="p-5">
<div className="grid grid-cols-1 gap-2 mb-4"> <div className="grid grid-cols-1 gap-2 mb-4">
<p><strong className="text-[#0e1b2a]">To:</strong> <span className="text-[#6b7280]">{selectedEmail.to_email}</span></p> <p><strong className="text-[#0f1724]">To:</strong> <span className="text-[#6b7280]">{selectedEmail.to_email}</span></p>
<p><strong className="text-[#0e1b2a]">Subject:</strong> <span className="text-[#6b7280]">{selectedEmail.subject}</span></p> <p><strong className="text-[#0f1724]">Subject:</strong> <span className="text-[#6b7280]">{selectedEmail.subject}</span></p>
<p><strong className="text-[#0e1b2a]">Error Message:</strong> <span className="text-[#ef4444]">{selectedEmail.error_message}</span></p> <p><strong className="text-[#0f1724]">Error Message:</strong> <span className="text-[#ef4444]">{selectedEmail.error_message}</span></p>
</div> </div>
<div className="mt-4"> <div className="mt-4">
<strong className="text-[#0e1b2a] mb-2 block">Body:</strong> <strong className="text-[#0f1724] mb-2 block">Body:</strong>
<div <div
className="border border-[rgba(0,0,0,0.08)] rounded p-4 mt-2 max-h-[400px] overflow-auto bg-gray-50 text-sm" className="border border-[rgba(0,0,0,0.08)] rounded p-4 mt-2 max-h-[400px] overflow-auto bg-gray-50 text-sm"
dangerouslySetInnerHTML={{ __html: selectedEmail.body }} dangerouslySetInnerHTML={{ __html: selectedEmail.body }}

View File

@ -180,9 +180,9 @@ export const NotificationBell = () => {
</button> </button>
{isOpen && ( {isOpen && (
<div className="absolute right-0 mt-3 w-[400px] max-w-[90vw] bg-white border border-gray-200 rounded-xl shadow-lg z-[100] flex flex-col overflow-hidden animate-in fade-in slide-in-from-top-2 duration-200"> <div className="absolute right-0 mt-3 w-[320px] sm:w-[380px] md:w-[400px] max-w-[95vw] max-h-[80vh] md:max-h-[60vh] lg:max-h-[500px] bg-white border border-gray-200 rounded-xl shadow-lg z-[100] flex flex-col overflow-hidden animate-in fade-in slide-in-from-top-2 duration-200">
{/* Header */} {/* Header */}
<div className="px-4 py-3 border-b border-gray-100 flex items-center justify-between bg-gray-50/50"> <div className="flex-shrink-0 px-4 py-3 border-b border-gray-100 flex items-center justify-between bg-gray-50/50">
<h3 className="text-sm font-semibold text-gray-900"> <h3 className="text-sm font-semibold text-gray-900">
Notifications Notifications
</h3> </h3>
@ -206,7 +206,7 @@ export const NotificationBell = () => {
</div> </div>
{/* List */} {/* List */}
<div className="flex-1 overflow-y-auto max-h-[480px]"> <div className="flex-1 overflow-y-auto">
{notifications.length === 0 ? ( {notifications.length === 0 ? (
<div className="py-12 px-4 flex flex-col items-center justify-center text-center"> <div className="py-12 px-4 flex flex-col items-center justify-center text-center">
<div className="w-12 h-12 bg-gray-50 rounded-full flex items-center justify-center mb-3"> <div className="w-12 h-12 bg-gray-50 rounded-full flex items-center justify-center mb-3">
@ -296,7 +296,7 @@ export const NotificationBell = () => {
</div> </div>
{/* Footer */} {/* Footer */}
<div className="px-4 py-2 bg-gray-50 border-t border-gray-100 flex items-center justify-between"> <div className="flex-shrink-0 px-4 py-2 bg-gray-50 border-t border-gray-100 flex items-center justify-between">
<button <button
onClick={() => { onClick={() => {
const isSuperAdmin = roles.includes("super_admin"); const isSuperAdmin = roles.includes("super_admin");

View File

@ -17,12 +17,12 @@ interface PageHeaderProps {
} }
const defaultTabs: TabItem[] = [ const defaultTabs: TabItem[] = [
{ label: 'Overview', path: '/dashboard' }, // { label: 'Overview', path: '/dashboard' },
{ label: 'Tenants', path: '/tenants' }, // { label: 'Tenants', path: '/tenants' },
// { label: 'Users', path: '/users' }, // // { label: 'Users', path: '/users' },
// { label: 'Roles', path: '/roles' }, // // { label: 'Roles', path: '/roles' },
{ label: 'Modules', path: '/modules' }, // { label: 'Modules', path: '/modules' },
{ label: 'Audit Logs', path: '/audit-logs' }, // { label: 'Audit Logs', path: '/audit-logs' },
]; ];
export const PageHeader = ({ export const PageHeader = ({

View File

@ -3,10 +3,14 @@ import { Layout } from "@/components/layout/Layout";
export default function FailedEmails() { export default function FailedEmails() {
return ( return (
<Layout currentPage="Failed Emails"> <Layout
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-100"> currentPage="Failed Emails"
pageHeader={{
title: "Platform Failed Emails",
description: "Global monitoring of all failed system email dispatches and automatic/manual retry logs across all tenants."
}}
>
<FailedEmailsTable /> <FailedEmailsTable />
</div>
</Layout> </Layout>
); );
} }

View File

@ -3,10 +3,14 @@ import { Layout } from "@/components/layout/Layout";
export default function FailedEmails() { export default function FailedEmails() {
return ( return (
<Layout currentPage="Failed Emails"> <Layout
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-100"> currentPage="Failed Emails"
pageHeader={{
title: "Failed Emails Log",
description: "View and resend failed system email dispatches and transaction logs for this tenant."
}}
>
<FailedEmailsTable /> <FailedEmailsTable />
</div>
</Layout> </Layout>
); );
} }