refactor: update FailedEmails layout with headers and enhance table actions using dropdowns
This commit is contained in:
parent
6952a7c6f3
commit
793fa23c1b
@ -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 }}
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
@ -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 = ({
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user