/** * Content Plan Table Component * * A comprehensive table component using TanStack Table for CRUD operations */ import { useState, useMemo, useCallback, useEffect, useRef, } from '@wordpress/element'; import React from 'react'; import { __ } from '@wordpress/i18n'; import { useContentPlan } from '../context/ContentPlanContext'; import DeleteConfirmationModal from './DeleteConfirmationModal'; import GeneratePostModal from './GeneratePostModal'; import StructureDisplay from './StructureDisplay'; import KeywordDisplay from './KeywordDisplay'; import Tooltip from './Tooltip'; import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; import { useReactTable, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, flexRender, createColumnHelper, getExpandedRowModel, Row, } from '@tanstack/react-table'; import { ContentPlanItem } from '../types'; import { LANGUAGE_OPTIONS } from '../utils/languageOptions'; interface ContentPlanTableProps { currentTooltipId?: string | null; onTooltipDismiss?: () => void; currentTooltipIndex?: number; totalTooltipCount?: number; onSkipTutorial?: () => void; } const ContentPlanTable: React.FC = ({ currentTooltipId = null, onTooltipDismiss, currentTooltipIndex, totalTooltipCount, onSkipTutorial, }) => { const { getString, openSidebar, contentPlan, loading, error, fetchContentPlanItems, deleteContentPlanItemAPI, bulkDeleteContentPlanItems, openGenerationModal, getLatestContentPlanDate, taskFilterId, clearTaskFilter, tasks, settings, saveSettings, } = useContentPlan(); const [filteredData, setFilteredData] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const [currentPlanId] = useState(1); // Default to plan ID 1 for now const [deleteModalOpen, setDeleteModalOpen] = useState(false); const [itemToDelete, setItemToDelete] = useState< ContentPlanItem | undefined >(undefined); const [generateModalOpen, setGenerateModalOpen] = useState(false); const [itemToGenerate, setItemToGenerate] = useState(null); const [expanded, setExpanded] = useState({}); const [dateRangeStart, setDateRangeStart] = useState(null); const [dateRangeEnd, setDateRangeEnd] = useState(null); const [datePreset, setDatePreset] = useState('future'); const [sorting, setSorting] = useState([ { id: 'scheduled_on', desc: false }, // Sort by scheduled_on ascending (nearest first) ]); const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10, }); const [selectedItems, setSelectedItems] = useState>(new Set()); const [columnVisibility, setColumnVisibility] = useState({}); const [isMobile, setIsMobile] = useState(false); const tableContainerRef = useRef(null); // Check if tutorial should be shown const showTutorial = settings && settings.tutorial_watched !== true; // Detect mobile screen size useEffect(() => { const checkMobile = () => { setIsMobile(window.innerWidth < 768); }; checkMobile(); window.addEventListener('resize', checkMobile); return () => window.removeEventListener('resize', checkMobile); }, []); // Hide columns on mobile useEffect(() => { if (isMobile) { setColumnVisibility({ id: false, structure: false, keywords: false, lang: false, post: false, expander: false, }); } else { setColumnVisibility({}); } }, [isMobile]); // Helper function to get WordPress admin edit URL for a post const getPostEditUrl = useCallback( (postId: string | number | null): string | null => { if (!postId) return null; // Construct WordPress admin edit URL const adminUrl = `${window.location.origin}/wp-admin/post.php?post=${postId}&action=edit`; return adminUrl; }, [] ); // Event handler for generating posts const handleGenerate = useCallback((item: ContentPlanItem): void => { setItemToGenerate(item); setGenerateModalOpen(true); }, []); // Column definitions const columnHelper = createColumnHelper(); const columns = useMemo( () => [ columnHelper.display({ id: 'checkbox', header: () => ( 0 && selectedItems.size === filteredData.length } onChange={e => { if (e.target.checked) { setSelectedItems( new Set(filteredData.map(item => item.id)) ); } else { setSelectedItems(new Set()); } }} className="w-4 h-4 cursor-pointer" /> ), cell: ({ row }) => ( { const newSelected = new Set(selectedItems); if (e.target.checked) { newSelected.add(row.original.id); } else { newSelected.delete(row.original.id); } setSelectedItems(newSelected); }} className="w-4 h-4 cursor-pointer" /> ), size: 50, }), columnHelper.display({ id: 'expander', header: () => null, cell: ({ row }) => ( ), size: 50, }), columnHelper.display({ id: 'actions', header: getString('table_columns', 'actions', 'Actions'), cell: info => { const status = (info.row.original as any).status || 'planned'; const isPlanned = status === 'planned'; const isDraftReadyAi = status === 'draft_ready_ai'; // Check if there are any pending tasks (queued or processing) const hasPendingTasks = tasks.some( task => task.status === 'queued' || task.status === 'processing' ); return (
{/* Show Generate button for planned items */} {isPlanned && ( )} {/* Show Regenerate button for draft_ready_ai items */} {isDraftReadyAi && ( )}
); }, size: 120, }), columnHelper.accessor('id', { header: getString('table_columns', 'id', 'ID'), cell: info => info.getValue(), size: 60, }), columnHelper.accessor('title', { header: getString('table_columns', 'title', 'Title'), cell: info => { const value = info.getValue() || getString('table_cells', 'untitled', 'Untitled'); if (!searchTerm.trim()) { return (
{value}
); } const regex = new RegExp( `(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'ig' ); const parts = String(value).split(regex); return (
{parts.map((part, idx) => regex.test(part) ? ( {part} ) : ( {part} ) )}
); }, size: 100, }), columnHelper.accessor('structure', { header: getString('table_columns', 'structure', 'Structure'), cell: info => { const value = info.getValue(); return ( ); }, size: 300, }), columnHelper.accessor('keywords', { header: getString('table_columns', 'keywords', 'Keywords'), cell: info => { const value = info.getValue(); return ( ); }, size: 200, }), columnHelper.accessor('post', { header: getString('table_columns', 'post', 'Post'), cell: info => { const value = info.getValue(); const status = (info.row.original as any).status; // Only show post field if status is not 'planned' if (status === 'planned') { return ( {getString( 'table_cells', 'not_available', 'Not available' )} ); } if (!value) { return ( {getString('table_cells', 'no_post', 'No post')} ); } // Convert post ID to edit URL const editUrl = getPostEditUrl(value); if (editUrl) { return ( {getString( 'table_cells', 'view_post', 'View Post' )} ); } // Fallback: show post ID as text return Post #{value}; }, size: 150, }), columnHelper.display({ id: 'status', header: getString('table_columns', 'status', 'Status'), cell: info => { const value = (info.row.original as any).status || 'planned'; const statusClasses = { planned: 'bg-gray-100 text-gray-800', draft_ready_ai: 'bg-blue-100 text-blue-800', draft_ready_manual: 'bg-green-100 text-green-800', published: 'bg-purple-100 text-purple-800', }; return ( {getString( `status_${value}`, 'label', value .replace(/_/g, ' ') .replace(/\b\w/g, (l: string) => l.toUpperCase() ) )} ); }, size: 150, }), columnHelper.accessor('scheduled_on', { header: getString( 'table_columns', 'scheduled_on', 'Scheduled On' ), cell: info => { const value = info.getValue(); if (!value) { return ( {getString( 'table_cells', 'not_scheduled', 'Not scheduled' )} ); } return ( {new Date(value).toLocaleDateString()} ); }, size: 150, }), ], [ getString, selectedItems, filteredData, searchTerm, getPostEditUrl, handleGenerate, tasks, ] ); // Table instance // Expanded row content renderer const renderExpandedRow = (row: Row) => { const item = row.original; return (

{getString('table_columns', 'created', 'Created')}

{new Date(item.created_at).toLocaleDateString()}

{getString( 'table_columns', 'last_modified', 'Last Modified' )}

{new Date( item.updated_at || item.created_at ).toLocaleDateString()}

{item.lang && (

{getString( 'table_columns', 'language', 'Language' )}

{(() => { const languageOption = LANGUAGE_OPTIONS.find( opt => opt.value === item.lang ); return languageOption ? languageOption.label : item.lang; })()}

)}
); }; const table = useReactTable({ data: filteredData, columns, state: { expanded, sorting, pagination, columnVisibility, }, onExpandedChange: updater => { if (typeof updater === 'function') { setExpanded(prev => updater(prev)); } else { setExpanded(updater); } }, onSortingChange: setSorting, onPaginationChange: setPagination, onColumnVisibilityChange: setColumnVisibility, manualPagination: false, autoResetPageIndex: true, getRowCanExpand: () => true, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel(), getExpandedRowModel: getExpandedRowModel(), }); // Event handlers const handleEdit = (item: ContentPlanItem): void => { // Dispatch event to ContentPlanItemSidebar with the item data window.dispatchEvent( new CustomEvent('editContentPlanItem', { detail: { item }, }) ); openSidebar('content-plan-item'); }; const handleDelete = (item: ContentPlanItem): void => { setItemToDelete(item); setDeleteModalOpen(true); }; const handleDeleteItem = useCallback( async (itemId: number): Promise => { try { await deleteContentPlanItemAPI(itemId, currentPlanId); } catch (err) { // Error is handled by context console.error('Error deleting item:', err); } }, [deleteContentPlanItemAPI, currentPlanId] ); const handleBulkDelete = useCallback(async (): Promise => { if (selectedItems.size === 0) return; try { await bulkDeleteContentPlanItems( Array.from(selectedItems), currentPlanId ); setSelectedItems(new Set()); // Clear selection after successful deletion } catch (err) { // Error is handled by context console.error('Error bulk deleting items:', err); } }, [bulkDeleteContentPlanItems, selectedItems, currentPlanId]); // Get task filter item IDs const taskFilterItemIds = useMemo(() => { if (!taskFilterId) return null; const task = tasks.find(t => t.id === taskFilterId); if (!task) return null; // For content plan generation tasks: use item_ids array const itemIds = task.result?.output?.item_ids; if (itemIds && Array.isArray(itemIds) && itemIds.length > 0) { return itemIds; } // For post generation tasks: use single item_id from input const itemId = task.result?.input?.item_id; if (itemId !== undefined && itemId !== null) { return [itemId]; } return null; }, [taskFilterId, tasks]); // Filter data based on search term, date range, and task filter const applyFilters = useCallback( ( items: ContentPlanItem[], search: string, startDate: Date | null, endDate: Date | null, itemIds: number[] | null ): ContentPlanItem[] => { let filtered = items; // Apply task filter first (by item IDs) if (itemIds && itemIds.length > 0) { filtered = filtered.filter(item => itemIds.includes(item.id)); } // Apply search filter if (search.trim()) { const searchLower = search.toLowerCase(); filtered = filtered.filter( item => item.title?.toLowerCase().includes(searchLower) || item.structure?.toLowerCase().includes(searchLower) || item.keywords?.toLowerCase().includes(searchLower) ); } // Apply date range filter if (startDate || endDate) { filtered = filtered.filter(item => { if (!item.scheduled_on) return false; try { // Parse the scheduled date string and normalize to date only (ignore time) // Handle both "YYYY-MM-DD HH:MM:SS" and "YYYY-MM-DD" formats, and Date objects let scheduledDateStr: string; if (typeof item.scheduled_on === 'string') { scheduledDateStr = item.scheduled_on.split(' ')[0]; // Get just the date part "YYYY-MM-DD" } else { // If it's already a Date object or timestamp, convert to string first const dateObj = new Date(item.scheduled_on); scheduledDateStr = dateObj .toISOString() .split('T')[0]; } const [year, month, day] = scheduledDateStr .split('-') .map(Number); // Create date in local timezone to avoid timezone issues const itemDate = new Date(year, month - 1, day); // month is 0-indexed itemDate.setHours(0, 0, 0, 0); if (startDate) { const start = new Date(startDate); start.setHours(0, 0, 0, 0); // Compare dates only (ignore time) if (itemDate.getTime() < start.getTime()) return false; } if (endDate) { const end = new Date(endDate); end.setHours(23, 59, 59, 999); // Compare dates only (ignore time) if (itemDate.getTime() > end.getTime()) return false; } return true; } catch (error) { // If date parsing fails, exclude the item console.error( 'Error parsing scheduled date:', item.scheduled_on, error ); return false; } }); } return filtered; }, [] ); // Search functionality const handleSearch = useCallback((term: string): void => { setSearchTerm(term); }, []); // Debounce search input useEffect(() => { const id = setTimeout(() => { const filtered = applyFilters( contentPlan.contentPlanItems, searchTerm, dateRangeStart, dateRangeEnd, taskFilterItemIds ); setFilteredData(filtered); }, 300); return () => clearTimeout(id); }, [ searchTerm, contentPlan.contentPlanItems, dateRangeStart, dateRangeEnd, taskFilterItemIds, applyFilters, ]); // Helper: filter to valid dates only (prevents "Invalid time value" in DatePicker) const getValidDatesFromItems = useCallback( (items: typeof contentPlan.contentPlanItems): Date[] => { return items .filter(item => item.scheduled_on) .map(item => new Date(item.scheduled_on!)) .filter(d => !isNaN(d.getTime())); }, [] ); // Helper functions to get min/max dates from items const getMinDateFromItems = useCallback((): Date | null => { const validDates = getValidDatesFromItems(contentPlan.contentPlanItems); if (validDates.length === 0) return null; return new Date(Math.min(...validDates.map(d => d.getTime()))); }, [contentPlan.contentPlanItems, getValidDatesFromItems]); const getMaxDateFromItems = useCallback((): Date | null => { const validDates = getValidDatesFromItems(contentPlan.contentPlanItems); if (validDates.length === 0) return null; return new Date(Math.max(...validDates.map(d => d.getTime()))); }, [contentPlan.contentPlanItems, getValidDatesFromItems]); // Get minimum date from items that are in the past (before today) const getMinPastDateFromItems = useCallback((): Date | null => { const today = new Date(); today.setHours(0, 0, 0, 0); const pastDates = getValidDatesFromItems( contentPlan.contentPlanItems ).filter(d => { const dNorm = new Date(d); dNorm.setHours(0, 0, 0, 0); return dNorm < today; }); if (pastDates.length === 0) return null; return new Date(Math.min(...pastDates.map(d => d.getTime()))); }, [contentPlan.contentPlanItems, getValidDatesFromItems]); // Date range change handlers const handleDateRangeChange = useCallback( (dates: [Date | null, Date | null] | Date | null): void => { if (!Array.isArray(dates)) { setDatePreset('custom'); setDateRangeStart(dates); setDateRangeEnd(null); return; } const [start, end] = dates; setDatePreset('custom'); setDateRangeStart(start); setDateRangeEnd(end); const filtered = applyFilters( contentPlan.contentPlanItems, searchTerm, start, end, taskFilterItemIds ); setFilteredData(filtered); }, [ contentPlan.contentPlanItems, searchTerm, applyFilters, taskFilterItemIds, ] ); // Preset change handler const handlePresetChange = useCallback( (preset: string): void => { setDatePreset(preset); let newStartDate: Date | null = null; let newEndDate: Date | null = null; if (preset === 'future') { // Future posts: current day to maximum date const today = new Date(); today.setHours(0, 0, 0, 0); newStartDate = today; newEndDate = getMaxDateFromItems(); } else if (preset === 'past') { // Past posts: minimum date from past items to yesterday const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); yesterday.setHours(23, 59, 59, 999); // Get minimum date from past items, or fallback to minimum from all items // but ensure it's not in the future (clamp to yesterday if needed) let minPastDate = getMinPastDateFromItems(); if (!minPastDate) { // If no past items found, try using minimum from all items const minDate = getMinDateFromItems(); if (minDate && minDate <= yesterday) { minPastDate = minDate; } else { // Fallback: use 1 year ago if no valid date found minPastDate = new Date(); minPastDate.setFullYear(minPastDate.getFullYear() - 1); minPastDate.setHours(0, 0, 0, 0); } } // Ensure start date is not after end date if (minPastDate && minPastDate > yesterday) { newStartDate = yesterday; newEndDate = yesterday; } else { newStartDate = minPastDate; newEndDate = yesterday; } } // For "custom", don't change the dates if (preset !== 'custom') { setDateRangeStart(newStartDate); setDateRangeEnd(newEndDate); const filtered = applyFilters( contentPlan.contentPlanItems, searchTerm, newStartDate, newEndDate, taskFilterItemIds ); setFilteredData(filtered); } }, [ contentPlan.contentPlanItems, searchTerm, applyFilters, getMinDateFromItems, getMaxDateFromItems, getMinPastDateFromItems, taskFilterItemIds, ] ); // Initialize default date range (future posts preset) useEffect(() => { if ( contentPlan.contentPlanItems.length > 0 && !dateRangeStart && !dateRangeEnd ) { // Initialize with "future" preset const today = new Date(); today.setHours(0, 0, 0, 0); const maxDate = getMaxDateFromItems(); setDateRangeStart(today); setDateRangeEnd(maxDate); setDatePreset('future'); } }, [ contentPlan.contentPlanItems, dateRangeStart, dateRangeEnd, getMaxDateFromItems, ]); // Update date range when items are added and preset is "future" or "past" useEffect(() => { if ( contentPlan.contentPlanItems.length > 0 && datePreset !== 'custom' ) { if (datePreset === 'future') { // Update end date to include new items const maxDate = getMaxDateFromItems(); if (maxDate) { setDateRangeEnd(currentEnd => { if (!currentEnd || maxDate > currentEnd) { return maxDate; } return currentEnd; }); } } else if (datePreset === 'past') { // Ensure end date is always yesterday for past posts const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); yesterday.setHours(23, 59, 59, 999); setDateRangeEnd(yesterday); // Update start date to include new past items (earlier dates) let minPastDate = getMinPastDateFromItems(); if (!minPastDate) { // Fallback: use minimum from all items if it's in the past const minDate = getMinDateFromItems(); if (minDate && minDate <= yesterday) { minPastDate = minDate; } else { // Fallback: use 1 year ago minPastDate = new Date(); minPastDate.setFullYear(minPastDate.getFullYear() - 1); minPastDate.setHours(0, 0, 0, 0); } } if (minPastDate) { setDateRangeStart(currentStart => { if (!currentStart || minPastDate! < currentStart) { return minPastDate!; } return currentStart; }); } } } }, [ contentPlan.contentPlanItems.length, datePreset, getMaxDateFromItems, getMinDateFromItems, getMinPastDateFromItems, ]); // Update filtered data when data changes useEffect(() => { const filtered = applyFilters( contentPlan.contentPlanItems, searchTerm, dateRangeStart, dateRangeEnd, taskFilterItemIds ); setFilteredData(filtered); }, [ contentPlan.contentPlanItems, searchTerm, dateRangeStart, dateRangeEnd, taskFilterItemIds, applyFilters, ]); // Load data on component mount - now handled by context // useEffect(() => { // fetchContentPlanItems(currentPlanId); // }, [fetchContentPlanItems, currentPlanId]); // Listen for refresh events from the sidebar useEffect(() => { const handleRefresh = (): void => { fetchContentPlanItems(currentPlanId); }; const handleDelete = (event: CustomEvent): void => { const { itemId } = event.detail; if (itemId) { handleDeleteItem(itemId); } }; window.addEventListener('contentPlanItemUpdated', handleRefresh); window.addEventListener( 'deleteContentPlanItem', handleDelete as EventListener ); return () => { window.removeEventListener('contentPlanItemUpdated', handleRefresh); window.removeEventListener( 'deleteContentPlanItem', handleDelete as EventListener ); }; }, [fetchContentPlanItems, currentPlanId, handleDeleteItem]); // Scroll to table when task filter is applied useEffect(() => { if ( taskFilterId && tableContainerRef.current && filteredData.length > 0 ) { // Use setTimeout to ensure the DOM has updated with filtered data setTimeout(() => { tableContainerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start', }); }, 100); } }, [taskFilterId, filteredData.length]); if (loading && contentPlan.contentPlanItems.length === 0) { return (
{getString( 'content_plan_table', 'loading_items', 'Loading content plan items...' )}
); } return (
{/* Header */}

{getString( 'content_plan_table', 'content_plan_items', 'Content Plan Items' )}

{/* Action Bar - Buttons and Search */}
{/* Left side - Buttons */}
{(() => { // Check if there are any pending tasks (queued or processing) const hasPendingTasks = tasks.some( task => task.status === 'queued' || task.status === 'processing' ); const buttonText = getString( 'app', 'generate_content_plan', 'Generate content plan with AI' ); const button = ( ); return ( {button} ); })()}
{/* Right side - Search */}
) => handleSearch(e.target.value) } className="w-full h-10 sm:h-12 px-2 sm:px-3 py-2 sm:py-3 text-sm sm:text-base border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary" />
{/* Content Plan Summary */}
{contentPlan.contentPlanItems.length} {' '} {getString( 'content_plan_table', 'content_plan_items_lower', 'content plan items' )} {getLatestContentPlanDate() && ( <> {' '} {getString( 'content_plan_table', 'scheduled_until', 'scheduled until' )}{' '} {getLatestContentPlanDate()} )}
{/* Task Filter Indicator */} {taskFilterId && taskFilterItemIds && (
{getString( 'content_plan_table', 'task_filter_active', `Task filter: ${taskFilterItemIds.length} item${taskFilterItemIds.length === 1 ? '' : 's'}` )}
)} {taskFilterId && ( | )}
|
{/* Error Display */} {error && (
{error}
)} {/* Table Container */}
{/* Table - Desktop View */}
{table .getHeaderGroups() .map(headerGroup => ( {headerGroup.headers.map( header => ( ) )} ))} {table.getRowModel().rows.length === 0 ? ( ) : ( table.getRowModel().rows.map(row => ( {row .getVisibleCells() .map(cell => ( ))} {row.getIsExpanded() && ( )} )) )}
{header.isPlaceholder ? null : (
{flexRender( header .column .columnDef .header, header.getContext() )} {header.column.getIsSorted() === 'asc' && ( )} {header.column.getIsSorted() === 'desc' && ( )}
)}
col.getIsVisible() ).length } className="px-3 sm:px-4 md:px-6 py-12 sm:py-16 text-center" >

{getString( 'content_plan_table', 'no_items', 'No content plan items yet' )}

{flexRender( cell.column .columnDef .cell, cell.getContext() )}
{renderExpandedRow( row )}
{/* Mobile Card View */}
{table.getRowModel().rows.length === 0 ? (

{getString( 'content_plan_table', 'no_items', 'No content plan items yet' )}

) : ( table.getRowModel().rows.map(row => { const item = row.original; const status = (item as any).status || 'planned'; const isPlanned = status === 'planned'; const isDraftReadyAi = status === 'draft_ready_ai'; const statusClasses = { planned: 'bg-gray-100 text-gray-800', draft_ready_ai: 'bg-blue-100 text-blue-800', draft_ready_manual: 'bg-green-100 text-green-800', published: 'bg-purple-100 text-purple-800', }; return (
{/* Header Row */}
{ const newSelected = new Set( selectedItems ); if (e.target.checked) { newSelected.add( item.id ); } else { newSelected.delete( item.id ); } setSelectedItems( newSelected ); }} className="w-4 h-4 cursor-pointer mt-1 flex-shrink-0" />

{item.title || getString( 'table_cells', 'untitled', 'Untitled' )}

{getString( `status_${status}`, 'label', status .replace( /_/g, ' ' ) .replace( /\b\w/g, ( l: string ) => l.toUpperCase() ) )} {item.scheduled_on && ( {(() => { const d = new Date( item.scheduled_on! ); return !isNaN( d.getTime() ) ? d.toLocaleDateString() : '—'; })()} )}
{/* Structure - Collapsible */} {item.structure && (
)} {/* Keywords */} {item.keywords && (
)} {/* Language */} {item.lang && (

{getString( 'table_columns', 'language', 'Language' )} :

{(() => { const languageOption = LANGUAGE_OPTIONS.find( opt => opt.value === item.lang ); return languageOption ? languageOption.label : item.lang; })()}

)} {/* Post Link */} {status !== 'planned' && item.post && (
{(() => { const editUrl = getPostEditUrl( item.post ); if (editUrl) { return ( {getString( 'table_cells', 'view_post', 'View Post' )} ); } return null; })()}
)} {/* Actions */}
{/* Generate/Regenerate buttons */}
{(() => { // Check if there are any pending tasks (queued or processing) const hasPendingTasks = tasks.some( task => task.status === 'queued' || task.status === 'processing' ); return ( <> {isPlanned && ( )} {isDraftReadyAi && ( )} ); })()}
{/* Edit and Delete buttons */}
); }) )}
{/* Pagination Controls */}
{/* Left side - Results info */}

{getString( 'content_plan_table', 'showing', 'Showing' )}{' '} {filteredData.length === 0 ? 0 : table.getState().pagination .pageIndex * table.getState().pagination .pageSize + 1} {' '} {getString( 'content_plan_table', 'to', 'to' )}{' '} {Math.min( (table.getState().pagination .pageIndex + 1) * table.getState().pagination .pageSize, filteredData.length )} {' '} {getString( 'content_plan_table', 'of', 'of' )}{' '} {filteredData.length} {' '} {getString( 'content_plan_table', 'content_plan_items_lower', 'content plan items' )}

{/* Page size selector */}
{/* Right side - Page navigation */}
{getString( 'content_plan_table', 'page', 'Page' )}{' '} {table.getState().pagination.pageIndex + 1} {' '} {getString( 'content_plan_table', 'of', 'of' )}{' '} {table.getPageCount()}
{/* Bulk Delete Panel - Fixed at bottom of viewport */} {selectedItems.size > 0 && (
{selectedItems.size}{' '} {selectedItems.size === 1 ? getString( 'content_plan_table', 'item_selected', 'item selected' ) : getString( 'content_plan_table', 'items_selected', 'items selected' )}
)} {/* Delete Confirmation Modal */} { setDeleteModalOpen(false); setItemToDelete(undefined); }} onConfirm={async () => { if (itemToDelete) { try { await deleteContentPlanItemAPI( itemToDelete.id, currentPlanId ); setDeleteModalOpen(false); setItemToDelete(undefined); } catch (err) { // Error is handled by context } } }} itemTitle={itemToDelete?.title} item={itemToDelete} /> {/* Generate Post Modal */} { setGenerateModalOpen(false); setItemToGenerate(null); }} item={itemToGenerate} />
); }; export default ContentPlanTable;