/** * Published Page - Built with TablePageTemplate * Shows published/review content with WordPress publishing capabilities */ import { useState, useEffect, useMemo, useCallback } from 'react'; import TablePageTemplate from '../../templates/TablePageTemplate'; import { fetchContent, Content, ContentListResponse, ContentFilters, fetchAPI, fetchWordPressStatus, deleteContent, bulkDeleteContent, } from '../../services/api'; import { useNavigate } from 'react-router-dom'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { FileIcon, TaskIcon, ImageIcon, CheckCircleIcon } from '../../icons'; import { createPublishedPageConfig } from '../../config/pages/published.config'; import { useSectorStore } from '../../store/sectorStore'; import { usePageSizeStore } from '../../store/pageSizeStore'; import PageHeader from '../../components/common/PageHeader'; import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs'; import ModuleMetricsFooter from '../../components/dashboard/ModuleMetricsFooter'; export default function Published() { const toast = useToast(); const navigate = useNavigate(); const { activeSector } = useSectorStore(); const { pageSize } = usePageSizeStore(); // Data state const [content, setContent] = useState([]); const [loading, setLoading] = useState(true); // Filter state - default to published/review status const [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState('published'); // Default to published const [publishStatusFilter, setPublishStatusFilter] = useState(''); 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); // Load content - filtered for published/review const loadContent = useCallback(async () => { setLoading(true); setShowContent(false); try { const ordering = sortBy ? `${sortDirection === 'desc' ? '-' : ''}${sortBy}` : '-created_at'; const filters: ContentFilters = { ...(searchTerm && { search: searchTerm }), ...(statusFilter && { status: statusFilter }), page: currentPage, page_size: pageSize, ordering, }; const data: ContentListResponse = await fetchContent(filters); // Client-side filter for WordPress publish status if needed let filteredResults = data.results || []; if (publishStatusFilter === 'published') { filteredResults = filteredResults.filter(c => c.external_id); } else if (publishStatusFilter === 'not_published') { filteredResults = filteredResults.filter(c => !c.external_id); } // Fetch WordPress status for published content const resultsWithWPStatus = await Promise.all( filteredResults.map(async (content) => { if (content.external_id) { try { const wpStatus = await fetchWordPressStatus(content.id); return { ...content, wordpress_status: wpStatus.wordpress_status, }; } catch (error) { console.warn(`Failed to fetch WP status for content ${content.id}:`, error); return content; } } return content; }) ); setContent(resultsWithWPStatus); 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, publishStatusFilter, 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 useEffect(() => { const timer = setTimeout(() => { if (currentPage === 1) { loadContent(); } else { setCurrentPage(1); } }, 500); return () => clearTimeout(timer); }, [searchTerm, currentPage, loadContent]); // Handle sorting const handleSort = (field: string, direction: 'asc' | 'desc') => { setSortBy(field || 'created_at'); setSortDirection(direction); setCurrentPage(1); }; // Row action handler const handleRowAction = useCallback(async (action: string, row: Content) => { if (action === 'publish_wordpress') { try { const response = await fetchAPI('/v1/publisher/publish/', { method: 'POST', body: JSON.stringify({ content_id: row.id, destinations: ['wordpress'] }) }); if (response.success) { toast.success(`Published "${row.title}" to WordPress`); loadContent(); } else { toast.error(response.error || 'Failed to publish'); } } catch (error: any) { console.error('WordPress publish error:', error); toast.error(`Failed to publish: ${error.message || 'Network error'}`); } } else if (action === 'view_on_wordpress') { if (row.external_url) { window.open(row.external_url, '_blank'); } else { toast.warning('WordPress URL not available'); } } else if (action === 'edit') { // Navigate to content editor (if exists) or show edit modal navigate(`/writer/content?id=${row.id}`); } }, [toast, loadContent, navigate]); 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]); // Bulk WordPress publish const handleBulkPublishWordPress = useCallback(async (ids: string[]) => { try { const contentIds = ids.map(id => parseInt(id)); let successCount = 0; let failedCount = 0; // Publish each item individually for (const contentId of contentIds) { try { const response = await fetchAPI('/v1/publisher/publish/', { method: 'POST', body: JSON.stringify({ content_id: contentId, destinations: ['wordpress'] }) }); if (response.success) { successCount++; } else { failedCount++; console.warn(`Failed to publish content ${contentId}:`, response.error); } } catch (error) { failedCount++; console.error(`Error publishing content ${contentId}:`, error); } } if (successCount > 0) { toast.success(`Published ${successCount} item(s) to WordPress`); } if (failedCount > 0) { toast.warning(`${failedCount} item(s) failed to publish`); } loadContent(); } catch (error: any) { toast.error(`Failed to bulk publish: ${error.message}`); throw error; } }, [toast, loadContent]); // Bulk action handler const handleBulkAction = useCallback(async (action: string, ids: string[]) => { if (action === 'bulk_publish_wordpress') { await handleBulkPublishWordPress(ids); } }, [handleBulkPublishWordPress]); // 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 createPublishedPageConfig({ searchTerm, setSearchTerm, statusFilter, setStatusFilter, publishStatusFilter, setPublishStatusFilter, setCurrentPage, activeSector, onRowClick: (row: Content) => { navigate(`/writer/content/${row.id}`); }, }); }, [searchTerm, statusFilter, publishStatusFilter, activeSector, navigate]); // Calculate header metrics const headerMetrics = useMemo(() => { if (!pageConfig?.headerMetrics) return []; return pageConfig.headerMetrics.map((metric) => ({ label: metric.label, value: metric.calculate({ content, totalCount }), accentColor: metric.accentColor, })); }, [pageConfig?.headerMetrics, content, totalCount]); // Writer navigation tabs const writerTabs = [ { label: 'Queue', path: '/writer/tasks', icon: }, { label: 'Drafts', path: '/writer/content', icon: }, { label: 'Images', path: '/writer/images', icon: }, { label: 'Review', path: '/writer/review', icon: }, { label: 'Published', path: '/writer/published', icon: }, ]; return ( <> , color: 'green' }} navigation={} /> { if (key === 'search') { setSearchTerm(value); } else if (key === 'status') { setStatusFilter(value); setCurrentPage(1); } else if (key === 'publishStatus') { setPublishStatusFilter(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}`} /> {/* Module Metrics Footer */} c.status === 'published').length.toLocaleString(), subtitle: `${content.filter(c => c.external_id).length} on WordPress`, icon: , accentColor: 'green', href: '/writer/published', }, { title: 'Draft Content', value: content.filter(c => c.status === 'draft').length.toLocaleString(), subtitle: 'Not yet published', icon: , accentColor: 'blue', }, ]} progress={{ label: 'WordPress Publishing Progress', value: totalCount > 0 ? Math.round((content.filter(c => c.external_id).length / totalCount) * 100) : 0, color: 'success', }} /> ); }