/** * 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, Content, ContentListResponse, ContentFilters, fetchAPI, fetchWordPressStatus, deleteContent, bulkDeleteContent, } from '../../services/api'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { CheckCircleIcon, BoltIcon } from '../../icons'; import { createApprovedPageConfig } from '../../config/pages/approved.config'; import { useSectorStore } from '../../store/sectorStore'; import { usePageSizeStore } from '../../store/pageSizeStore'; import PageHeader from '../../components/common/PageHeader'; import ModuleMetricsFooter from '../../components/dashboard/ModuleMetricsFooter'; export default function Approved() { 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 approved status const [searchTerm, setSearchTerm] = useState(''); 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 approved status (API still uses 'published' internally) const loadContent = useCallback(async () => { setLoading(true); setShowContent(false); try { const ordering = sortBy ? `${sortDirection === 'desc' ? '-' : ''}${sortBy}` : '-created_at'; const filters: ContentFilters = { ...(searchTerm && { search: searchTerm }), status: 'published', // Backend uses 'published' for approved content 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, 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 createApprovedPageConfig({ searchTerm, setSearchTerm, publishStatusFilter, setPublishStatusFilter, setCurrentPage, activeSector, onRowClick: (row: Content) => { navigate(`/writer/content/${row.id}`); }, }); }, [searchTerm, 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]); return ( <> , color: 'green' }} parent="Writer" /> , onClick: () => handleBulkAction('bulk_publish_wordpress', selectedIds), variant: 'success', }} onFilterChange={(key: string, value: any) => { if (key === 'search') { setSearchTerm(value); } 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 - 3-Widget Layout */} c.external_id).length, percentage: `${totalCount > 0 ? Math.round((content.filter(c => c.external_id).length / totalCount) * 100) : 0}%` }, { label: 'Pending Publish', value: content.filter(c => !c.external_id).length }, { label: 'This Page', value: content.length }, ], progress: { label: 'Published to Site', value: totalCount > 0 ? Math.round((content.filter(c => c.external_id).length / totalCount) * 100) : 0, color: 'green', }, hint: content.filter(c => !c.external_id).length > 0 ? `${content.filter(c => !c.external_id).length} items ready for site publishing` : 'All approved content published!', }, moduleStats: { title: 'Writer Module', pipeline: [ { fromLabel: 'Tasks', fromValue: 0, fromHref: '/writer/tasks', actionLabel: 'Generate Content', toLabel: 'Drafts', toValue: 0, toHref: '/writer/content', progress: 100, color: 'blue', }, { fromLabel: 'Drafts', fromValue: 0, fromHref: '/writer/content', actionLabel: 'Generate Images', toLabel: 'Images', toValue: 0, toHref: '/writer/images', progress: 100, color: 'purple', }, { fromLabel: 'Ready', fromValue: 0, fromHref: '/writer/review', actionLabel: 'Review & Publish', toLabel: 'Published', toValue: totalCount, progress: totalCount > 0 ? Math.round((content.filter(c => c.external_id).length / totalCount) * 100) : 0, color: 'green', }, ], links: [ { label: 'Tasks', href: '/writer/tasks' }, { label: 'Content', href: '/writer/content' }, { label: 'Images', href: '/writer/images' }, { label: 'Published', href: '/writer/approved' }, ], }, completion: { title: 'Workflow Completion', plannerItems: [ { label: 'Keywords', value: 0, color: 'blue' }, { label: 'Clusters', value: 0, color: 'green' }, { label: 'Ideas', value: 0, color: 'amber' }, ], writerItems: [ { label: 'Content', value: 0, color: 'purple' }, { label: 'Images', value: 0, color: 'amber' }, { label: 'Published', value: content.filter(c => c.external_id).length, color: 'green' }, ], creditsUsed: 0, operationsCount: 0, analyticsHref: '/analytics', }, }} /> ); }