Current File : /home/jeconsul/public_html/wp-content/plugins/suremails/src/screens/logs/logs.js
// Logs.js
import { useState, useEffect, useRef } from '@wordpress/element';
import {
	X,
	Eye as EyeIcon,
	Trash as DeleteIcon,
	RefreshCw as ResendIcon,
	Calendar,
	Search,
} from 'lucide-react';
import {
	toast,
	Select,
	Input,
	Button,
	Badge,
	Pagination,
	DatePicker,
	Table,
} from '@bsf/force-ui';
import { __ } from '@wordpress/i18n';
import EmptyLogs from './empty-logs';
import NoFilteredLogs from './no-filtered-logs';
import EmailLogDrawer from './email-log-drawer';
import LogsSkeleton from './logs-skeleton';
import ConfirmationDialog from '@components/confirmation-dialog/confirmation-dialog'; // Import the ConfirmationDialog component
import {
	fetchLogs,
	deleteLogs as apiDeleteLogs,
	resendEmails as apiResendEmails,
} from '@api/logs'; // Import API functions
import {
	formatDate,
	getSelectedDate,
	getPaginationRange,
	getStatusLabel,
	getStatusVariant,
	get_pending_status,
} from '@utils/utils';
import Tooltip from '@components/tooltip/tooltip';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import TruncatedTooltipText from '@components/truncated-tooltip-text';
import Title from '@components/title/title';

const STATUS_FILTERS = [
	{ value: 'sent', label: __( 'Successful', 'suremails' ) },
	{ value: 'failed', label: __( 'Failed', 'suremails' ) },
	{ value: 'pending', label: __( 'In Progress', 'suremails' ) },
	{ value: 'blocked', label: __( 'Blocked', 'suremails' ) },
];

const Logs = () => {
	// State Variables
	const [ page, setPage ] = useState( 1 );
	const [ selectedDates, setSelectedDates ] = useState( {
		from: null,
		to: null,
	} );
	const [ filter, setFilter ] = useState( '' );
	const [ searchTerm, setSearchTerm ] = useState( '' ); // New search state
	const [ selectedLog, setSelectedLog ] = useState( null );
	const [ isDrawerOpen, setIsDrawerOpen ] = useState( false );
	const [ selectedLogs, setSelectedLogs ] = useState( [] );
	const [ isDatePickerOpen, setIsDatePickerOpen ] = useState( false );
	const logsPerPage = 10;

	// Dialog state for ConfirmationDialog
	const [ isDialogOpen, setIsDialogOpen ] = useState( false );
	const [ dialogConfig, setDialogConfig ] = useState( {
		title: '',
		description: '',
		onConfirm: null,
	} );
	const containerRef = useRef( null );

	const useDebounce = ( value, delay = 500, callback ) => {
		const [ debouncedValue, setDebouncedValue ] = useState( value );
		useEffect( () => {
			const handler = setTimeout( () => {
				setDebouncedValue( value );
				callback();
			}, delay );
			return () => clearTimeout( handler );
		}, [ value, delay ] );
		return debouncedValue;
	};

	/**
	 * Debounced function to set the search term.
	 * This prevents excessive API calls during rapid input.
	 */
	const debouncedSearchTerm = useDebounce( searchTerm, 500, () =>
		setPage( 1 )
	);
	const queryClient = useQueryClient();

	// Replace fetchAndSetLogs with React Query
	const {
		data: logsData,
		isLoading,
		error,
	} = useQuery( {
		queryKey: [
			'logs',
			page,
			selectedDates.from,
			selectedDates.to,
			filter,
			debouncedSearchTerm,
		],
		queryFn: () =>
			fetchLogs( {
				pageNumber: page,
				startDate: selectedDates.from,
				endDate: selectedDates.to,
				filter,
				searchTerm: debouncedSearchTerm,
				logsPerPage,
			} ),
		keepPreviousData: true, // Preserve previous page data while loading next page
		refetchInterval: 100000, // Refetch every 10 minutes
		refetchOnReconnect: true,
	} );

	// Update the logs and totalPages from React Query data
	const logs = logsData?.logs || [];
	const totalPages = logsData?.total_count
		? Math.ceil( logsData.total_count / logsPerPage )
		: 1;

	// Add mutations for delete and resend operations
	const deleteMutation = useMutation( {
		mutationFn: apiDeleteLogs,
		onSuccess: ( response, variables ) => {
			if ( response.success ) {
				toast.success(
					__( 'Logs deleted successfully.', 'suremails' )
				);
				// Invalidate and refetch logs
				queryClient.invalidateQueries( {
					queryKey: [ 'logs' ],
				} );
				// Refetch dashboard data
				queryClient.refetchQueries( {
					queryKey: [ 'dashboard-data' ],
					exact: true,
				} );

				// Return to the previous page if required.
				if (
					logsData.logs.length === variables.length &&
					logsData.logs.length < logsPerPage &&
					page > 1
				) {
					setPage( ( prev ) => Math.max( prev - 1, 1 ) );
				}
			}
		},
		onError: ( deleteError ) => {
			toast.error( __( 'Failed to delete logs.', 'suremails' ), {
				description:
					deleteError.message ||
					__( 'There was an issue deleting logs.', 'suremails' ),
			} );
		},
		onSettled: () => {
			setIsDialogOpen( false );
			setSelectedLogs( [] );
		},
	} );

	const resendMutation = useMutation( {
		mutationFn: apiResendEmails,
		onSuccess: ( response ) => {
			if ( response.success ) {
				toast.success(
					__( 'Email(s) resent successfully.', 'suremails' )
				);

				// Invalidate and refetch logs
				queryClient.invalidateQueries( {
					queryKey: [ 'logs' ],
				} );
				// Refetch dashboard data
				queryClient.refetchQueries( {
					queryKey: [ 'dashboard-data' ],
					exact: true,
				} );
			}
		},
		onError: ( resendError ) => {
			toast.error( __( 'Failed to resend the email(s).', 'suremails' ), {
				description:
					resendError.message ||
					__( 'There was an issue resending emails.', 'suremails' ),
			} );
		},
		onSettled: () => {
			queryClient.invalidateQueries( {
				queryKey: [ 'logs' ],
			} );
			setIsDialogOpen( false );
			setSelectedLogs( [] );
		},
	} );

	// Update the confirmation handlers to use mutations
	const confirmDeleteLogs = async ( logIds ) => {
		await deleteMutation.mutateAsync( logIds );
	};

	const confirmResendEmails = async ( logIds ) => {
		await resendMutation.mutateAsync( logIds );
	};

	// Handle error from React Query
	if ( error ) {
		toast.error( __( 'Failed to fetch logs.', 'suremails' ), {
			description:
				error.message ||
				__( 'There was an issue fetching logs.', 'suremails' ),
		} );
	}

	useEffect( () => {
		function handleClickOutside( event ) {
			if (
				isDatePickerOpen &&
				containerRef.current &&
				! containerRef.current.contains( event.target )
			) {
				setIsDatePickerOpen( false );
			}
		}

		// Bind the event listener
		document.addEventListener( 'mousedown', handleClickOutside );
		return () => {
			// Unbind the event listener on cleanup
			document.removeEventListener( 'mousedown', handleClickOutside );
		};
	}, [ isDatePickerOpen ] );

	const handleViewDetails = ( log ) => {
		setSelectedLog( log );
		setIsDrawerOpen( true );
	};

	const handleCloseDrawer = () => {
		setIsDrawerOpen( false );
		setSelectedLog( null );
	};

	const handleSelectLog = ( logId ) => {
		setSelectedLogs( ( prevSelected ) =>
			prevSelected.includes( logId )
				? prevSelected.filter( ( id ) => id !== logId )
				: [ ...prevSelected, logId ]
		);
	};

	const handleSelectAll = () => {
		setSelectedLogs(
			selectedLogs.length === logs.length
				? []
				: logs.map( ( log ) => log.id )
		);
	};

	// Determine if EmptyLogs condition is met
	const isEmptyLogs =
		! isLoading &&
		logs.length === 0 &&
		! selectedDates.from &&
		! selectedDates.to &&
		! filter &&
		! debouncedSearchTerm; // Include searchTerm

	// Conditional Rendering: If EmptyLogs condition is true, render only EmptyLogs
	if ( isEmptyLogs ) {
		return (
			<div className="min-h-screen p-6 overflow-hidden bg-background-secondary">
				<EmptyLogs />
			</div>
		);
	}

	// Define the handleResendSuccess callback
	const handleResendSuccess = () => {
		setIsDrawerOpen( false );
		queryClient.invalidateQueries( {
			queryKey: [ 'logs' ],
		} );
	};

	// Determine the confirm button text based on the dialog title
	const getConfirmButtonText = () => {
		const titleLower = dialogConfig.title.toLowerCase();
		if ( titleLower.includes( 'resend' ) ) {
			return __( 'Resend', 'suremails' );
		}
		if ( titleLower.includes( 'deletion' ) ) {
			return __( 'Delete', 'suremails' );
		}
		return __( 'Confirm', 'suremails' );
	};

	// Handler Functions for Pagination
	const handlePageChange = ( newPage ) => {
		setPage( newPage );
	};

	/**
	 * Determine if the Resend button should be disabled based on the selected logs.
	 * The button should be disabled if any of the selected logs are in pending status.
	 */
	const isResendDisabled = selectedLogs.some( ( id ) => {
		const log = logs.find( ( logItem ) => logItem.id === id );
		return log && get_pending_status( log.status );
	} );

	// Conditional Rendering: Determine what to display based on loading and logs
	let content;

	if ( isLoading ) {
		content = <LogsSkeleton />;
	} else if (
		logs.length === 0 &&
		( selectedDates.from ||
			selectedDates.to ||
			filter ||
			debouncedSearchTerm )
	) {
		content = (
			<NoFilteredLogs
				startDate={ selectedDates.from }
				endDate={ selectedDates.to }
				filter={ filter }
				searchTerm={ debouncedSearchTerm } // Pass searchTerm if needed
				setSelectedDates={ setSelectedDates }
				setFilter={ setFilter }
				setPage={ setPage }
			/>
		);
	} else {
		content = (
			<Table className="bg-background-primary" checkboxSelection>
				<Table.Head
					className="bg-background-secondary"
					onChangeSelection={ handleSelectAll }
					indeterminate={
						selectedLogs.length > 0 &&
						selectedLogs.length < logs.length
					}
					selected={ selectedLogs?.length > 0 }
				>
					<Table.HeadCell>
						{ __( 'Subject', 'suremails' ) }
					</Table.HeadCell>
					<Table.HeadCell className="w-1/8">
						{ __( 'Status', 'suremails' ) }
					</Table.HeadCell>
					<Table.HeadCell className="w-1/6">
						{ __( 'Email To', 'suremails' ) }
					</Table.HeadCell>
					<Table.HeadCell>
						{ __( 'Connection', 'suremails' ) }
					</Table.HeadCell>
					<Table.HeadCell className="w-1/6">
						{ __( 'Date & Time', 'suremails' ) }
					</Table.HeadCell>
					<Table.HeadCell className="w-12">
						<span className="sr-only">
							{ __( 'Actions', 'suremails' ) }
						</span>
					</Table.HeadCell>
				</Table.Head>
				<Table.Body>
					{ logs.map( ( log ) => (
						<Table.Row
							key={ log.id }
							className="whitespace-nowrap"
							selected={ selectedLogs.includes( log.id ) }
							onChangeSelection={ () =>
								handleSelectLog( log.id )
							}
						>
							<Table.Cell>
								<TruncatedTooltipText
									text={ log.subject }
									className="max-w-[21.875rem]"
								/>
							</Table.Cell>
							<Table.Cell>
								<Badge
									className="max-w-fit"
									label={ getStatusLabel(
										log.status,
										log?.response
									) }
									variant={ getStatusVariant(
										log.status,
										log?.response
									) }
									size="sm"
									disableHover
								/>
							</Table.Cell>
							<Table.Cell>
								<TruncatedTooltipText text={ log.email_to } />
							</Table.Cell>
							<Table.Cell>
								<Badge
									className="inline-block"
									label={ log.connection }
									variant="blue"
									size="sm"
									disableHover
								/>
							</Table.Cell>
							<Table.Cell>
								{ formatDate( log.updated_at, {
									day: true,
									month: true,
									year: true,
									hour: true,
									minute: true,
									hour12: true,
								} ) }
							</Table.Cell>
							<Table.Cell>
								<div className="flex justify-end gap-2">
									<Tooltip
										content={ __(
											'Resend Email',
											'suremails'
										) }
										position="top"
										arrow
									>
										<Button
											className="text-icon-secondary hover:text-icon-primary"
											size="xs"
											onClick={ () =>
												handleResend( [ log.id ] )
											}
											icon={ <ResendIcon /> }
											variant="ghost"
											aria-label={ __(
												'Resend',
												'suremails'
											) }
											disabled={ get_pending_status(
												log?.status
											) }
										/>
									</Tooltip>

									<Tooltip
										content={ __(
											'Delete Log',
											'suremails'
										) }
										position="top"
										arrow
									>
										<Button
											className="text-icon-secondary hover:text-icon-primary"
											size="xs"
											onClick={ () =>
												handleDelete( [ log.id ] )
											}
											icon={ <DeleteIcon /> }
											variant="ghost"
											aria-label={ __(
												'Delete',
												'suremails'
											) }
										/>
									</Tooltip>
									<Tooltip
										content={ __(
											'View Details',
											'suremails'
										) }
										position="top"
										arrow
									>
										<Button
											className="text-icon-secondary hover:text-icon-primary"
											size="xs"
											onClick={ () =>
												handleViewDetails( log )
											}
											icon={ <EyeIcon /> }
											variant="ghost"
											aria-label={ __(
												'View Details',
												'suremails'
											) }
											disabled={
												log.status === 'pending'
											}
										/>
									</Tooltip>
								</div>
							</Table.Cell>
						</Table.Row>
					) ) }
				</Table.Body>
				<Table.Footer className="flex items-center justify-between">
					{ /* Pagination with Page Label */ }
					{ /* Page Label - aligned to the right */ }
					<div className="text-sm font-normal text-text-secondary whitespace-nowrap">
						{ __( 'Page', 'suremails' ) } { page }{ ' ' }
						{ __( 'out of', 'suremails' ) } { totalPages }
					</div>
					{ /* Pagination Controls - aligned to the left */ }
					<div className="flex items-center space-x-2">
						<Pagination size="sm">
							<Pagination.Content className="[&>li]:m-0">
								<Pagination.Previous
									tag="button"
									onClick={ () =>
										setPage( ( prev ) =>
											Math.max( prev - 1, 1 )
										)
									}
									disabled={ page === 1 }
								/>
								{ getPaginationRange( page, totalPages, 1 ).map(
									( item, index ) => {
										if ( item === 'ellipsis' ) {
											return (
												<Pagination.Ellipsis
													key={ `ellipsis-${ index }` }
												/>
											);
										}
										return (
											<Pagination.Item
												key={ item }
												isActive={ page === item }
												onClick={ () =>
													handlePageChange( item )
												}
											>
												{ item }
											</Pagination.Item>
										);
									}
								) }
								<Pagination.Next
									tag="button"
									onClick={ () =>
										setPage( ( prev ) =>
											Math.min( prev + 1, totalPages )
										)
									}
									disabled={ page === totalPages }
								/>
							</Pagination.Content>
						</Pagination>
					</div>
				</Table.Footer>
			</Table>
		);
	}

	// Handler Functions for DatePicker
	const handleDateApply = ( dates ) => {
		const { from, to } = dates;

		if ( from && to ) {
			const fromDate = new Date( from );
			const toDate = new Date( to );

			if ( fromDate > toDate ) {
				// Swap the dates to ensure 'from' is earlier than 'to'
				setSelectedDates( { from: to, to: from } );
			} else {
				setSelectedDates( dates );
			}
		} else {
			setSelectedDates( { from, to: null } );
		}

		setIsDatePickerOpen( false );
		setPage( 1 );
	};

	const handleDateCancel = () => {
		setIsDatePickerOpen( false );
	};

	// Handler Functions for ConfirmationDialog
	const handleDelete = ( logIds ) => {
		setDialogConfig( {
			title: __( 'Confirm Deletion', 'suremails' ),
			description: __(
				'Are you sure you want to delete the selected log(s)? This action cannot be undone.',
				'suremails'
			),
			onConfirm: () => confirmDeleteLogs( logIds ),
			destructiveConfirmButton: true,
		} );
		setIsDialogOpen( true );
	};

	const handleResend = ( logIds ) => {
		setDialogConfig( {
			title: __( 'Confirm Resend', 'suremails' ),
			description: __(
				'Are you sure you want to resend the selected email(s)?',
				'suremails'
			),
			onConfirm: () => confirmResendEmails( logIds ),
			destructiveConfirmButton: false,
		} );
		setIsDialogOpen( true );
	};

	return (
		<>
			<div className="min-h-screen px-8 py-8 bg-background-secondary">
				<div className="p-4 space-y-2 border-0.5 border-solid shadow-sm bg-background-primary rounded-xl border-border-subtle">
					<div>
						{ /* Filters or Batch Actions */ }
						<div className="flex items-center justify-between p-1.25">
							<Title
								title={ __( 'Email Logs', 'suremails' ) }
								tag="h1"
							/>
							<div className="flex space-x-4">
								{ selectedLogs.length > 0 ? (
									// Batch Action Buttons
									<>
										<Button
											variant="primary"
											icon={ <ResendIcon /> }
											size="sm"
											onClick={ () =>
												handleResend( selectedLogs )
											}
											className="font-medium"
											disabled={ isResendDisabled }
										>
											{ __(
												'Resend Emails',
												'suremails'
											) }
										</Button>
										<Button
											variant="outline"
											icon={ <DeleteIcon /> }
											size="sm"
											onClick={ () =>
												handleDelete( selectedLogs )
											}
											destructive
										>
											{ __( 'Delete', 'suremails' ) }
										</Button>
									</>
								) : (
									// Filter Controls
									<>
										{ /* Conditionally Render Reset Filters Button */ }
										{ ( selectedDates.from ||
											selectedDates.to ||
											filter ||
											searchTerm ) && (
											<Button
												variant="link"
												size="sm"
												icon={ <X /> }
												onClick={ () => {
													setSelectedDates( {
														from: null,
														to: null,
													} );
													setFilter( '' );
													setSearchTerm( '' );
													setPage( 1 );
												} }
												destructive
												className="leading-4 no-underline hover:no-underline min-w-fit focus:[box-shadow:none]"
											>
												{ __(
													'Clear Filters',
													'suremails'
												) }
											</Button>
										) }

										<Input
											className="w-52"
											type="text"
											size="sm"
											onChange={ setSearchTerm }
											value={ searchTerm }
											placeholder={ __(
												'Search…',
												'suremails'
											) }
											required
											prefix={
												<Search className="text-icon-secondary" />
											}
										/>

										{ /* Status Filter */ }
										<Select
											value={ filter }
											onChange={ setFilter }
											size="sm"
										>
											<Select.Button
												className="w-52 h-[2rem] [&_div]:text-xs"
												placeholder={ __(
													'Status',
													'suremails'
												) }
											>
												{ ( { value: renderValue } ) =>
													renderValue
														? getStatusLabel(
																renderValue
														  )
														: __(
																'Select Status',
																'suremails'
														  )
												}
											</Select.Button>
											<Select.Portal
												id="suremails-root-app"
												className="z-999999"
											>
												<Select.Options>
													{ STATUS_FILTERS.map(
														( option ) => (
															<Select.Option
																key={
																	option.value
																}
																value={
																	option.value
																}
																className="text-xs"
															>
																{ option.label }
															</Select.Option>
														)
													) }
												</Select.Options>
											</Select.Portal>
										</Select>
										{ /* Date Range Picker */ }
										<div
											className="relative"
											ref={ containerRef }
										>
											<Input
												type="text"
												size="sm"
												value={ getSelectedDate(
													selectedDates
												) }
												suffix={
													<Calendar className="text-icon-secondary" />
												}
												onClick={ () =>
													setIsDatePickerOpen(
														! isDatePickerOpen
													)
												}
												placeholder={ __(
													'mm/dd/yyyy - mm/dd/yyyy',
													'suremails'
												) }
												className="cursor-pointer w-52"
												readOnly
											/>
											{ isDatePickerOpen && (
												<div className="absolute right-0 z-10 mt-2 rounded-lg shadow-lg">
													<DatePicker
														applyButtonText={ __(
															'Apply',
															'suremails'
														) }
														cancelButtonText={ __(
															'Cancel',
															'suremails'
														) }
														selectionType="range"
														showOutsideDays={
															false
														}
														variant="presets"
														onApply={
															handleDateApply
														}
														onCancel={
															handleDateCancel
														}
														selected={
															selectedDates
														}
													/>
												</div>
											) }
										</div>
									</>
								) }
							</div>
						</div>
					</div>

					{ /* Conditional Rendering Based on Loading and Logs */ }
					<div className="overflow-hidden bg-background-primary">
						{ content }
					</div>
				</div>
			</div>

			{ /* Confirmation Dialog */ }
			<ConfirmationDialog
				isOpen={ isDialogOpen }
				title={ dialogConfig.title }
				description={ dialogConfig.description }
				onConfirm={ dialogConfig.onConfirm }
				onCancel={ () => setIsDialogOpen( false ) }
				confirmButtonText={ getConfirmButtonText() }
				cancelButtonText={ __( 'Cancel', 'suremails' ) }
				destructiveConfirmButton={
					!! dialogConfig?.destructiveConfirmButton
				}
			/>

			{ /* Email Log Drawer */ }
			<EmailLogDrawer
				isOpen={ selectedLog && isDrawerOpen }
				setOpen={ setIsDrawerOpen }
				log={ selectedLog }
				onClose={ handleCloseDrawer }
				onResendSuccess={ handleResendSuccess } // Pass the callback here
			/>
		</>
	);
};

export default Logs;