/** * Approved Page - Built with TablePageTemplate * Shows approved content ready for publishing to WordPress/external sites */ import { useState, useEffect, useMemo, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import TablePageTemplate from '../../templates/TablePageTemplate'; import { fetchContent, fetchImages, fetchWriterContentFilterOptions, Content, ContentListResponse, ContentFilters, fetchAPI, deleteContent, bulkDeleteContent, } from '../../services/api'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { FileIcon, CheckCircleIcon, BoltIcon } from '../../icons'; import { RocketLaunchIcon } from '@heroicons/react/24/outline'; import { createApprovedPageConfig } from '../../config/pages/approved.config'; import { useSectorStore } from '../../store/sectorStore'; import { useSiteStore } from '../../store/siteStore'; import { usePageSizeStore } from '../../store/pageSizeStore'; import PageHeader from '../../components/common/PageHeader'; import StandardThreeWidgetFooter from '../../components/dashboard/StandardThreeWidgetFooter'; import PublishingProgressModal, { PublishingProgressState } from '../../components/common/PublishingProgressModal'; import BulkPublishingModal, { PublishQueueItem } from '../../components/common/BulkPublishingModal'; import PublishLimitModal from '../../components/common/PublishLimitModal'; import ScheduleContentModal from '../../components/common/ScheduleContentModal'; import BulkScheduleModal from '../../components/common/BulkScheduleModal'; import BulkSchedulePreviewModal from '../../components/common/BulkSchedulePreviewModal'; import ErrorDetailsModal from '../../components/common/ErrorDetailsModal'; export default function Approved() { const toast = useToast(); const navigate = useNavigate(); const { activeSector } = useSectorStore(); const { activeSite } = useSiteStore(); const { pageSize } = usePageSizeStore(); // Data state const [content, setContent] = useState([]); const [loading, setLoading] = useState(true); // Total counts for footer widget and header metrics (not page-filtered) const [totalContent, setTotalContent] = useState(0); const [totalDraft, setTotalDraft] = useState(0); const [totalReview, setTotalReview] = useState(0); const [totalApproved, setTotalApproved] = useState(0); const [totalPublished, setTotalPublished] = useState(0); const [totalImagesCount, setTotalImagesCount] = useState(0); // Dynamic filter options (loaded from backend) const [statusOptions, setStatusOptions] = useState | undefined>(undefined); const [siteStatusOptions, setSiteStatusOptions] = useState | undefined>(undefined); const [contentTypeOptions, setContentTypeOptions] = useState | undefined>(undefined); const [contentStructureOptions, setContentStructureOptions] = useState | undefined>(undefined); // Filter state const [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState(''); // Status filter (draft/review/approved/published) const [siteStatusFilter, setSiteStatusFilter] = useState(''); // Site status filter (not_published/scheduled/published/failed) const [contentTypeFilter, setContentTypeFilter] = useState(''); // Content type filter (post/page/product/taxonomy) const [contentStructureFilter, setContentStructureFilter] = useState(''); // Content structure filter const [selectedIds, setSelectedIds] = useState([]); // Pagination state const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [totalCount, setTotalCount] = useState(0); // Sorting state const [sortBy, setSortBy] = useState('created_at'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); const [showContent, setShowContent] = useState(false); // Publishing modals state const [showPublishLimitModal, setShowPublishLimitModal] = useState(false); const [showSinglePublishModal, setShowSinglePublishModal] = useState(false); const [showBulkPublishModal, setShowBulkPublishModal] = useState(false); const [singlePublishState, setSinglePublishState] = useState(null); const [bulkPublishQueue, setBulkPublishQueue] = useState([]); // Scheduling modals state const [showScheduleModal, setShowScheduleModal] = useState(false); const [showBulkScheduleModal, setShowBulkScheduleModal] = useState(false); const [showBulkSchedulePreviewModal, setShowBulkSchedulePreviewModal] = useState(false); const [scheduleContent, setScheduleContent] = useState(null); const [bulkScheduleItems, setBulkScheduleItems] = useState([]); const [bulkSchedulePreview, setBulkSchedulePreview] = useState(null); // Error details modal state const [showErrorDetailsModal, setShowErrorDetailsModal] = useState(false); const [errorContent, setErrorContent] = useState(null); // Load dynamic filter options based on current site's data and applied filters // This implements cascading filters - each filter's options reflect what's available // given the other currently applied filters const loadFilterOptions = useCallback(async (currentFilters?: { status?: string; site_status?: string; content_type?: string; content_structure?: string; search?: string; }) => { if (!activeSite) return; try { const options = await fetchWriterContentFilterOptions(activeSite.id, currentFilters); setStatusOptions(options.statuses || []); setSiteStatusOptions(options.site_statuses || []); setContentTypeOptions(options.content_types || []); setContentStructureOptions(options.content_structures || []); } catch (error) { console.error('Error loading filter options:', error); } }, [activeSite]); // Load filter options when site changes (initial load with no filters) useEffect(() => { loadFilterOptions(); }, [activeSite]); // Reload filter options when any filter changes (cascading filters) useEffect(() => { loadFilterOptions({ status: statusFilter || undefined, site_status: siteStatusFilter || undefined, content_type: contentTypeFilter || undefined, content_structure: contentStructureFilter || undefined, search: searchTerm || undefined, }); }, [statusFilter, siteStatusFilter, contentTypeFilter, contentStructureFilter, searchTerm, loadFilterOptions]); // Load total metrics for footer widget and header metrics (not affected by pagination) const loadTotalMetrics = useCallback(async () => { try { // Fetch counts in parallel for performance const [allRes, draftRes, reviewRes, approvedRes, publishedRes, imagesRes] = await Promise.all([ fetchContent({ page_size: 1, site_id: activeSite?.id }), fetchContent({ page_size: 1, status: 'draft', site_id: activeSite?.id }), fetchContent({ page_size: 1, status: 'review', site_id: activeSite?.id }), fetchContent({ page_size: 1, status: 'approved', site_id: activeSite?.id }), fetchContent({ page_size: 1, status: 'published', site_id: activeSite?.id }), fetchImages({ page_size: 1, site_id: activeSite?.id }), ]); setTotalContent(allRes.count || 0); setTotalDraft(draftRes.count || 0); setTotalReview(reviewRes.count || 0); setTotalApproved(approvedRes.count || 0); setTotalPublished(publishedRes.count || 0); setTotalImagesCount(imagesRes.count || 0); } catch (error) { console.error('Error loading total metrics:', error); } }, [activeSite]); // Load total metrics on mount useEffect(() => { loadTotalMetrics(); }, [loadTotalMetrics]); // Load content - filtered for approved+published status const loadContent = useCallback(async () => { setLoading(true); setShowContent(false); try { const ordering = sortBy ? `${sortDirection === 'desc' ? '-' : ''}${sortBy}` : '-created_at'; const filters: ContentFilters = { ...(searchTerm && { search: searchTerm }), // Default to approved+published if no status filter selected ...(statusFilter ? { status: statusFilter } : { status__in: 'approved,published' }), ...(contentTypeFilter && { content_type: contentTypeFilter }), ...(contentStructureFilter && { content_structure: contentStructureFilter }), page: currentPage, page_size: pageSize, ordering, }; const data: ContentListResponse = await fetchContent(filters); // Client-side filter for site_status if needed (backend may not support this filter yet) let filteredResults = data.results || []; if (siteStatusFilter) { filteredResults = filteredResults.filter(c => c.site_status === siteStatusFilter); } setContent(filteredResults); setTotalCount(data.count || 0); setTotalPages(Math.ceil((data.count || 0) / pageSize)); setTimeout(() => { setShowContent(true); setLoading(false); }, 100); } catch (error: any) { console.error('Error loading content:', error); toast.error(`Failed to load content: ${error.message}`); setShowContent(true); setLoading(false); } }, [currentPage, statusFilter, siteStatusFilter, contentTypeFilter, contentStructureFilter, sortBy, sortDirection, searchTerm, pageSize, toast]); useEffect(() => { loadContent(); }, [loadContent]); // Listen for site and sector changes and refresh data useEffect(() => { const handleSiteChange = () => { loadContent(); }; const handleSectorChange = () => { loadContent(); }; window.addEventListener('siteChanged', handleSiteChange); window.addEventListener('sectorChanged', handleSectorChange); return () => { window.removeEventListener('siteChanged', handleSiteChange); window.removeEventListener('sectorChanged', handleSectorChange); }; }, [loadContent]); // Reset to page 1 when pageSize changes useEffect(() => { setCurrentPage(1); }, [pageSize]); // Debounced search - reset to page 1 when search term changes // Only depend on searchTerm to avoid pagination reset on page navigation useEffect(() => { const timer = setTimeout(() => { // Always reset to page 1 when search changes // The main useEffect will handle reloading when currentPage changes setCurrentPage(1); }, 500); return () => clearTimeout(timer); }, [searchTerm]); // Handle sorting const handleSort = (field: string, direction: 'asc' | 'desc') => { setSortBy(field || 'created_at'); setSortDirection(direction); setCurrentPage(1); }; // Handle single content publish with progress modal const handleSinglePublish = useCallback(async (row: Content) => { if (!activeSite) { toast.error('No active site selected'); return; } // Initialize publishing state const initialState: PublishingProgressState = { contentId: row.id, contentTitle: row.title, destination: activeSite.platform_type || 'wordpress', siteName: activeSite.name, status: 'preparing', progress: 0, statusMessage: 'Preparing content...', error: null, externalUrl: null, externalId: null, }; setSinglePublishState(initialState); setShowSinglePublishModal(true); try { // Phase 1: Preparing (0-25%) await new Promise(resolve => setTimeout(resolve, 500)); setSinglePublishState(prev => prev ? { ...prev, progress: 25, statusMessage: 'Uploading to site...' } : null); // Phase 2: Uploading (25-50%) - Make API call const response = await fetchAPI('/v1/publisher/publish/', { method: 'POST', body: JSON.stringify({ content_id: row.id, destinations: [activeSite.platform_type || 'wordpress'] }) }); // Note: fetchAPI unwraps the response, so response IS the data object // Response format: { success: true, results: [{ success: true, url: '...', external_id: '...' }] } if (response.success && response.results?.[0]?.success) { const result = response.results[0]; // Phase 3: Processing (50-75%) setSinglePublishState(prev => prev ? { ...prev, progress: 50, statusMessage: 'Processing response...' } : null); await new Promise(resolve => setTimeout(resolve, 300)); // Phase 4: Finalizing (75-100%) setSinglePublishState(prev => prev ? { ...prev, progress: 75, statusMessage: 'Finalizing...' } : null); await new Promise(resolve => setTimeout(resolve, 300)); // Complete setSinglePublishState(prev => prev ? { ...prev, status: 'completed', progress: 100, statusMessage: 'Published successfully!', externalUrl: result.url, externalId: result.external_id, } : null); loadContent(); } else { throw new Error(response.error || response.results?.[0]?.error || 'Failed to publish'); } } catch (error: any) { console.error('Publish error:', error); setSinglePublishState(prev => prev ? { ...prev, status: 'failed', progress: 0, statusMessage: 'Failed to publish', error: error.message || 'Network error', } : null); } }, [activeSite, toast, loadContent]); // Schedule single content const handleScheduleContent = useCallback(async (contentId: number, scheduledDate: string) => { try { await fetchAPI(`/v1/writer/content/${contentId}/schedule/`, { method: 'POST', body: JSON.stringify({ scheduled_publish_at: scheduledDate }), }); toast.success('Content scheduled successfully'); loadContent(); } catch (error: any) { toast.error(`Failed to schedule: ${error.message}`); throw error; } }, [toast, loadContent]); // Reschedule content (same API endpoint) const handleRescheduleContent = useCallback(async (contentId: number, scheduledDate: string) => { try { await fetchAPI(`/v1/writer/content/${contentId}/reschedule/`, { method: 'POST', body: JSON.stringify({ scheduled_at: scheduledDate }), }); toast.success('Content rescheduled successfully'); loadContent(); } catch (error: any) { toast.error(`Failed to reschedule: ${error.message}`); throw error; } }, [toast, loadContent]); // Unschedule content const handleUnscheduleContent = useCallback(async (contentId: number) => { try { await fetchAPI(`/v1/writer/content/${contentId}/unschedule/`, { method: 'POST', }); toast.success('Content unscheduled successfully'); loadContent(); } catch (error: any) { toast.error(`Failed to unschedule: ${error.message}`); } }, [toast, loadContent]); // Bulk schedule with manual date/time const handleBulkScheduleManual = useCallback(async (contentIds: number[], scheduledDate: string) => { try { let successCount = 0; let failedCount = 0; for (const contentId of contentIds) { try { await fetchAPI(`/v1/writer/content/${contentId}/schedule/`, { method: 'POST', body: JSON.stringify({ scheduled_publish_at: scheduledDate }), }); successCount++; } catch (error) { console.error(`Failed to schedule content ${contentId}:`, error); failedCount++; } } if (successCount > 0) { toast.success(`Scheduled ${successCount} item(s)`); } if (failedCount > 0) { toast.warning(`${failedCount} item(s) failed to schedule`); } loadContent(); } catch (error: any) { toast.error(`Failed to schedule: ${error.message}`); throw error; } }, [toast, loadContent]); // Bulk schedule with site defaults - show preview first const handleBulkScheduleWithDefaults = useCallback(async () => { if (!activeSite || selectedIds.length === 0) return; try { const contentIds = selectedIds.map(id => parseInt(id)); // Note: fetchAPI unwraps success_response, so response IS the data object // Response format: { scheduled_count: N, schedule_preview: [...], site_settings: {...} } const response = await fetchAPI('/v1/writer/content/bulk_schedule_preview/', { method: 'POST', body: JSON.stringify({ content_ids: contentIds, site_id: activeSite.id }) }); // fetchAPI throws on error, so if we get here, response is successful // Response is the unwrapped data object if (response && response.schedule_preview) { setBulkSchedulePreview(response); setShowBulkSchedulePreviewModal(true); } else { throw new Error('Invalid response from server'); } } catch (error: any) { toast.error(`Failed to generate schedule preview: ${error.message}`); } }, [activeSite, selectedIds, toast]); // Confirm bulk schedule with site defaults const handleConfirmBulkSchedule = useCallback(async () => { if (!activeSite || selectedIds.length === 0) return; try { const contentIds = selectedIds.map(id => parseInt(id)); // Note: fetchAPI unwraps success_response, so response IS the data object const response = await fetchAPI('/v1/writer/content/bulk_schedule/', { method: 'POST', body: JSON.stringify({ content_ids: contentIds, use_site_defaults: true, site_id: activeSite.id }) }); // fetchAPI throws on error, so if we get here, response is successful if (response && response.scheduled_count !== undefined) { toast.success(`Scheduled ${response.scheduled_count} item(s)`); setShowBulkSchedulePreviewModal(false); setBulkSchedulePreview(null); setSelectedIds([]); loadContent(); } else { throw new Error('Invalid response from server'); } } catch (error: any) { toast.error(`Failed to schedule: ${error.message}`); } }, [activeSite, selectedIds, toast, loadContent]); // Open site settings in new tab const handleOpenSiteSettings = useCallback(() => { if (activeSite) { window.open(`/sites/${activeSite.id}/settings?tab=publishing`, '_blank'); } }, [activeSite]); // Row action handler const handleRowAction = useCallback(async (action: string, row: Content) => { if (action === 'publish_site') { await handleSinglePublish(row); } else if (action === 'view_on_site') { if (row.external_url) { window.open(row.external_url, '_blank'); } else { toast.warning('Site URL not available'); } } else if (action === 'schedule') { setScheduleContent(row); setShowScheduleModal(true); } else if (action === 'reschedule') { setScheduleContent(row); setShowScheduleModal(true); } else if (action === 'unschedule') { if (window.confirm(`Are you sure you want to unschedule "${row.title}"?`)) { await handleUnscheduleContent(row.id); } } else if (action === 'view_error') { setErrorContent(row); setShowErrorDetailsModal(true); } else if (action === 'edit') { // Navigate to content editor if (row.site_id) { navigate(`/sites/${row.site_id}/posts/${row.id}/edit`); } else { // Fallback if site_id not available toast.warning('Unable to edit: Site information not available'); } } }, [toast, navigate, handleSinglePublish, handleUnscheduleContent]); const handleDelete = useCallback(async (id: number) => { await deleteContent(id); loadContent(); }, [loadContent]); const handleBulkDelete = useCallback(async (ids: number[]) => { const result = await bulkDeleteContent(ids); loadContent(); return result; }, [loadContent]); // Handle bulk publish with progress modal and limit validation const handleBulkPublishToSite = useCallback(async (ids: string[]) => { if (!activeSite) { toast.error('No active site selected'); return; } // Validate: Max 5 items for direct bulk publish if (ids.length > 5) { setShowPublishLimitModal(true); return; } try { // Initialize queue const queue: PublishQueueItem[] = ids.map((id, index) => { const contentItem = content.find(c => c.id === parseInt(id)); return { contentId: parseInt(id), contentTitle: contentItem?.title || `Content #${id}`, index: index + 1, status: 'pending' as const, progress: 0, statusMessage: 'Pending', error: null, externalUrl: null, externalId: null, }; }); setBulkPublishQueue(queue); setShowBulkPublishModal(true); // Process sequentially for (let i = 0; i < queue.length; i++) { // Update status to processing setBulkPublishQueue(prev => prev.map((item, idx) => idx === i ? { ...item, status: 'processing', progress: 0, statusMessage: 'Preparing...' } : item )); try { // Simulate progress animation await new Promise(resolve => setTimeout(resolve, 300)); setBulkPublishQueue(prev => prev.map((item, idx) => idx === i ? { ...item, progress: 25, statusMessage: 'Uploading to site...' } : item )); // Call API const response = await fetchAPI('/v1/publisher/publish/', { method: 'POST', body: JSON.stringify({ content_id: queue[i].contentId, destinations: [activeSite.platform_type || 'wordpress'] }) }); // Handle response - fetchAPI unwraps the data object // Response format: { success: true, results: [{ success: true, url: '...', external_id: '...' }] } if (response.success && response.results?.[0]?.success) { const result = response.results[0]; // Animate to completion setBulkPublishQueue(prev => prev.map((item, idx) => idx === i ? { ...item, progress: 75, statusMessage: 'Finalizing...' } : item )); await new Promise(resolve => setTimeout(resolve, 200)); setBulkPublishQueue(prev => prev.map((item, idx) => idx === i ? { ...item, status: 'completed', progress: 100, statusMessage: 'Published', externalUrl: result.url, externalId: result.external_id, } : item )); } else { throw new Error(response.error || response.results?.[0]?.error || 'Unknown error'); } } catch (error: any) { console.error(`Error publishing content ${queue[i].contentId}:`, error); setBulkPublishQueue(prev => prev.map((item, idx) => idx === i ? { ...item, status: 'failed', progress: 0, statusMessage: 'Failed', error: error.message || 'Network error', } : item )); } } // Refresh content after all done loadContent(); } catch (error: any) { toast.error(`Failed to bulk publish: ${error.message}`); throw error; } }, [activeSite, content, toast, loadContent]); // Bulk action handler const handleBulkAction = useCallback(async (action: string, ids: string[]) => { if (action === 'bulk_publish_site') { await handleBulkPublishToSite(ids); } else if (action === 'bulk_schedule_manual') { // Manual bulk scheduling (same time for all) handleBulkScheduleManual(ids); } else if (action === 'bulk_schedule_defaults') { // Schedule with site defaults handleBulkScheduleWithDefaults(ids); } }, [handleBulkPublishToSite, handleBulkScheduleManual, handleBulkScheduleWithDefaults]); // Bulk status update handler const handleBulkUpdateStatus = useCallback(async (ids: string[], status: string) => { try { const numIds = ids.map(id => parseInt(id)); // Note: This would need a backend endpoint like /v1/writer/content/bulk_update/ // For now, just show a toast toast.info('Bulk status update functionality coming soon'); } catch (error: any) { throw error; } }, [toast]); // Bulk export handler const handleBulkExport = useCallback(async (ids: string[]) => { try { if (!ids || ids.length === 0) { throw new Error('No records selected for export'); } toast.info('Export functionality coming soon'); } catch (error: any) { throw error; } }, [toast]); // Create page config const pageConfig = useMemo(() => { return createApprovedPageConfig({ searchTerm, setSearchTerm, statusFilter, setStatusFilter, siteStatusFilter, setSiteStatusFilter, setCurrentPage, activeSector, onRowClick: (row: Content) => { navigate(`/writer/content/${row.id}`); }, }); }, [searchTerm, statusFilter, siteStatusFilter, contentTypeFilter, contentStructureFilter, activeSector, navigate]); // Calculate header metrics - use totals from API calls (not page data) // This ensures metrics show correct totals across all pages, not just current page const headerMetrics = useMemo(() => { if (!pageConfig?.headerMetrics) return []; // Override the calculate function to use pre-loaded totals instead of filtering page data return pageConfig.headerMetrics.map((metric) => { let value: number; switch (metric.label) { case 'Content': value = totalContent || 0; break; case 'Draft': value = totalDraft; break; case 'In Review': value = totalReview; break; case 'Approved': value = totalApproved; break; case 'Published': value = totalPublished; break; case 'Total Images': value = totalImagesCount; break; default: value = metric.calculate({ content, totalCount }); } return { label: metric.label, value, accentColor: metric.accentColor, tooltip: (metric as any).tooltip, }; }); }, [pageConfig?.headerMetrics, content, totalCount, totalContent, totalDraft, totalReview, totalApproved, totalPublished, totalImagesCount]); return ( <> , color: 'green' }} parent="Publisher" /> 5 ? 'Publish (Limit Exceeded)' : 'Publish to Site', icon: , onClick: () => handleBulkAction('bulk_publish_site', selectedIds), variant: 'success', disabled: selectedIds.length === 0, tooltip: selectedIds.length > 5 ? 'You can only publish 5 items at once. Use scheduling for more.' : selectedIds.length > 0 ? `Publish ${selectedIds.length} item${selectedIds.length !== 1 ? 's' : ''} to ${activeSite?.name || 'site'}` : 'Select items to publish', }} onFilterChange={(key: string, value: any) => { if (key === 'search') { setSearchTerm(value); } else if (key === 'status') { setStatusFilter(value); setCurrentPage(1); } else if (key === 'site_status') { setSiteStatusFilter(value); setCurrentPage(1); } else if (key === 'content_type') { setContentTypeFilter(value); setCurrentPage(1); } else if (key === 'content_structure') { setContentStructureFilter(value); setCurrentPage(1); } }} pagination={{ currentPage, totalPages, totalCount, onPageChange: setCurrentPage, }} selection={{ selectedIds, onSelectionChange: setSelectedIds, }} sorting={{ sortBy, sortDirection, onSort: handleSort, }} headerMetrics={headerMetrics} onRowAction={handleRowAction} onDelete={handleDelete} onBulkDelete={handleBulkDelete} onBulkAction={handleBulkAction} onBulkUpdateStatus={handleBulkUpdateStatus} onBulkExport={handleBulkExport} getItemDisplayName={(row: Content) => row.title || `Content #${row.id}`} /> {/* Three Widget Footer - Section 3 Layout */} c.external_id).length, percentage: `${totalCount > 0 ? Math.round((content.filter(c => c.external_id).length / totalCount) * 100) : 0}%` }, { label: 'Pending', value: content.filter(c => !c.external_id).length }, ], progress: { value: totalCount > 0 ? Math.round((content.filter(c => c.external_id).length / totalCount) * 100) : 0, label: 'On Site', color: 'green', }, hint: content.filter(c => !c.external_id).length > 0 ? `${content.filter(c => !c.external_id).length} article${content.filter(c => !c.external_id).length !== 1 ? 's' : ''} pending sync to site` : 'All articles synced to site!', statusInsight: content.filter(c => !c.external_id).length > 0 ? `Select articles and publish to your WordPress site.` : totalCount > 0 ? `All content published! Check your site for live articles.` : `No approved content. Approve articles from Review page.`, }} module="writer" /> {/* Publishing Modals */} {singlePublishState && ( setShowSinglePublishModal(false)} publishingState={singlePublishState} onRetry={() => { setShowSinglePublishModal(false); const contentItem = content.find(c => c.id === singlePublishState.contentId); if (contentItem) { handleSinglePublish(contentItem); } }} /> )} { setShowBulkPublishModal(false); setBulkPublishQueue([]); setSelectedIds([]); }} queue={bulkPublishQueue} siteName={activeSite?.name || 'Site'} destination={activeSite?.platform_type || 'wordpress'} onUpdateQueue={setBulkPublishQueue} onRetryItem={(contentId) => { const contentItem = content.find(c => c.id === contentId); if (contentItem) { handleSinglePublish(contentItem); } }} /> setShowPublishLimitModal(false)} selectedCount={selectedIds.length} onScheduleInstead={() => { setShowPublishLimitModal(false); handleBulkScheduleWithDefaults(); }} /> { setShowScheduleModal(false); setScheduleContent(null); }} content={scheduleContent} onSchedule={async (contentId, scheduledDate) => { if (scheduleContent?.site_status === 'scheduled' || scheduleContent?.site_status === 'failed') { await handleRescheduleContent(contentId, scheduledDate); } else { await handleScheduleContent(contentId, scheduledDate); } setShowScheduleModal(false); setScheduleContent(null); }} mode={scheduleContent?.site_status === 'scheduled' || scheduleContent?.site_status === 'failed' ? 'reschedule' : 'schedule'} /> { setShowBulkScheduleModal(false); setBulkScheduleItems([]); }} contentItems={bulkScheduleItems} onSchedule={async (contentIds, scheduledDate) => { await handleBulkScheduleManual(contentIds, scheduledDate); setShowBulkScheduleModal(false); setBulkScheduleItems([]); setSelectedIds([]); }} /> { setShowBulkSchedulePreviewModal(false); setBulkSchedulePreview(null); }} previewData={bulkSchedulePreview} onConfirm={handleConfirmBulkSchedule} onChangeSettings={handleOpenSiteSettings} siteId={activeSite?.id || 0} /> { setShowErrorDetailsModal(false); setErrorContent(null); }} content={errorContent} site={activeSite} onPublishNow={() => { if (errorContent) { handleSinglePublish(errorContent); } }} onReschedule={() => { if (errorContent) { setScheduleContent(errorContent); setShowScheduleModal(true); } }} onFixSettings={handleOpenSiteSettings} /> ); }