/** * Review Page - Built with TablePageTemplate * Shows content with status='review' ready for publishing */ import { useState, useEffect, useMemo, useCallback } from 'react'; import TablePageTemplate from '../../templates/TablePageTemplate'; import { fetchContent, Content, ContentListResponse, ContentFilters, fetchAPI, } from '../../services/api'; import { useNavigate } from 'react-router'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { FileIcon, TaskIcon, ImageIcon, CheckCircleIcon } from '../../icons'; import { createReviewPageConfig } from '../../config/pages/review.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 Review() { 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 review status const [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState('review'); // Default to review 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 review status const loadContent = useCallback(async () => { setLoading(true); setShowContent(false); try { const ordering = sortBy ? `${sortDirection === 'desc' ? '-' : ''}${sortBy}` : '-created_at'; const filters: ContentFilters = { ...(searchTerm && { search: searchTerm }), status: 'review', // Always filter for review status page: currentPage, page_size: pageSize, ordering, }; const data: ContentListResponse = await fetchContent(filters); setContent(data.results || []); 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, sortBy, sortDirection, searchTerm, pageSize, toast]); useEffect(() => { loadContent(); }, [loadContent]); // Listen for site and sector changes and refresh data useEffect(() => { const handleSiteChange = () => { loadContent(); }; window.addEventListener('site-changed', handleSiteChange); window.addEventListener('sector-changed', handleSiteChange); return () => { window.removeEventListener('site-changed', handleSiteChange); window.removeEventListener('sector-changed', handleSiteChange); }; }, [loadContent]); // Sorting handler const handleSort = useCallback((column: string) => { if (column === sortBy) { setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc'); } else { setSortBy(column); setSortDirection('asc'); } setCurrentPage(1); }, [sortBy]); // Handle row click - navigate to content view const handleRowClick = useCallback((row: Content) => { navigate(`/writer/content/${row.id}`); }, [navigate]); // Build page config const pageConfig = useMemo(() => createReviewPageConfig({ activeSector, searchTerm, setSearchTerm, statusFilter, setStatusFilter, setCurrentPage, onRowClick: handleRowClick, }), [activeSector, searchTerm, statusFilter, handleRowClick] ); // Header metrics (calculated from loaded data) const headerMetrics = useMemo(() => pageConfig.headerMetrics.map(metric => ({ ...metric, value: metric.calculate({ content, totalCount }), })), [pageConfig.headerMetrics, content, totalCount] ); // Export handler const handleBulkExport = useCallback(async (ids: string[]) => { toast.info(`Exporting ${ids.length} item(s)...`); return { success: true }; }, [toast]); // Publish to WordPress - single item const handlePublishSingle = useCallback(async (row: Content) => { console.group('🚀 [Review.handlePublishSingle] WordPress Publish Started'); console.log('📄 Content Details:', { id: row.id, title: row.title, status: row.status, timestamp: new Date().toISOString() }); try { // Log the payload being sent const payload = { content_id: row.id, destinations: ['wordpress'] }; console.log('📦 Request Payload:', payload); console.log('🌐 API Endpoint: POST /v1/publisher/publish/'); console.log('📡 Sending request to backend...'); const response = await fetchAPI('/v1/publisher/publish/', { method: 'POST', body: JSON.stringify(payload) }); console.log('📬 Full API Response:', JSON.parse(JSON.stringify(response))); console.log('📊 Response Structure:', { success: response.success, has_results: !!response.results, results_count: response.results?.length || 0, has_error: !!response.error, has_message: !!response.message }); // Handle the response with results array if (response.success && response.results) { console.log('✅ Overall publish success: true'); console.log('📋 Publish Results:', response.results); // Check individual destination results const wordpressResult = response.results.find((r: any) => r.destination === 'wordpress'); console.log('🎯 WordPress Result:', wordpressResult); if (wordpressResult && wordpressResult.success) { console.log('✅ WordPress publish successful:', { external_id: wordpressResult.external_id, url: wordpressResult.url, publishing_record_id: wordpressResult.publishing_record_id }); toast.success(`Successfully published "${row.title}" to WordPress`); loadContent(); // Reload to reflect changes } else { const error = wordpressResult?.error || 'Unknown error'; console.error('❌ WordPress publish failed:', { error: error, result: wordpressResult }); toast.error(`Failed to publish to WordPress: ${error}`); } } else if (!response.success) { // Handle overall failure console.error('❌ Publish failed (overall):', { error: response.error, message: response.message, results: response.results }); // Try to extract error from results let errorMsg = response.error || response.message; if (response.results && response.results.length > 0) { const failedResult = response.results[0]; errorMsg = failedResult.error || failedResult.message || errorMsg; } toast.error(`Failed to publish: ${errorMsg || 'Unknown error'}`); } else { console.warn('⚠️ Unexpected response format:', response); toast.error('Failed to publish: Unexpected response format'); } } catch (error: any) { console.error('❌ Exception during publish:', { content_id: row.id, error_type: error.constructor.name, error_message: error.message, error_stack: error.stack, error_object: error }); toast.error(`Failed to publish to WordPress: ${error.message || 'Network error'}`); } finally { console.groupEnd(); } }, [loadContent, toast]); // Publish to WordPress - bulk const handlePublishBulk = useCallback(async (ids: string[]) => { try { let successCount = 0; let failedCount = 0; // Publish each item individually for (const id of ids) { try { const response = await fetchAPI('/v1/publisher/publish/', { method: 'POST', body: JSON.stringify({ content_id: parseInt(id), destinations: ['wordpress'] }) }); if (response.success) { successCount++; } else { failedCount++; console.warn(`Failed to publish content ${id}:`, response.error); } } catch (error) { failedCount++; console.error(`Error publishing content ${id}:`, error); } } if (successCount > 0) { toast.success(`Successfully published ${successCount} item(s) to WordPress`); } if (failedCount > 0) { toast.warning(`${failedCount} item(s) failed to publish`); } loadContent(); // Reload to reflect changes } catch (error: any) { console.error('Bulk WordPress publish error:', error); toast.error(`Failed to bulk publish to WordPress: ${error.message || 'Network error'}`); } }, [loadContent, toast]); // Bulk action handler const handleBulkAction = useCallback(async (action: string, ids: string[]) => { if (action === 'bulk_publish_wordpress') { await handlePublishBulk(ids); } else { toast.info(`Bulk action "${action}" for ${ids.length} items`); } }, [handlePublishBulk, toast]); // Row action handler const handleRowAction = useCallback(async (action: string, row: Content) => { if (action === 'publish_wordpress') { await handlePublishSingle(row); } else if (action === 'view') { navigate(`/writer/content/${row.id}`); } }, [handlePublishSingle, navigate]); // Delete handler (single) const handleDelete = useCallback(async (id: string) => { try { await fetchAPI(`/v1/writer/content/${id}/`, { method: 'DELETE', }); toast.success('Content deleted successfully'); loadContent(); } catch (error: any) { toast.error(`Failed to delete content: ${error.message}`); throw error; } }, [loadContent, toast]); // Delete handler (bulk) const handleBulkDelete = useCallback(async (ids: string[]) => { try { // Delete each item individually let successCount = 0; for (const id of ids) { try { await fetchAPI(`/v1/writer/content/${id}/`, { method: 'DELETE', }); successCount++; } catch (error) { console.error(`Failed to delete content ${id}:`, error); } } toast.success(`Deleted ${successCount} content item(s)`); loadContent(); } catch (error: any) { toast.error(`Failed to bulk delete: ${error.message}`); throw error; } }, [loadContent, toast]); // Writer navigation tabs const writerTabs = [ { label: 'Tasks', path: '/writer/tasks', icon: }, { label: 'Content', path: '/writer/content', icon: }, { label: 'Images', path: '/writer/images', icon: }, { label: 'Review', path: '/writer/review', icon: }, { label: 'Published', path: '/writer/published', icon: }, ]; return ( <> , color: 'blue' }} navigation={} /> { const stringValue = value === null || value === undefined ? '' : String(value); if (key === 'search') { setSearchTerm(stringValue); } setCurrentPage(1); }} onBulkExport={handleBulkExport} onBulkAction={handleBulkAction} onDelete={handleDelete} onBulkDelete={handleBulkDelete} getItemDisplayName={(row: Content) => row.title || `Content #${row.id}`} onExport={async () => { toast.info('Export functionality coming soon'); }} selectionLabel="content" pagination={{ currentPage, totalPages, totalCount, onPageChange: setCurrentPage, }} selection={{ selectedIds, onSelectionChange: setSelectedIds, }} sorting={{ sortBy, sortDirection, onSort: handleSort, }} headerMetrics={headerMetrics} onFilterReset={() => { setSearchTerm(''); setCurrentPage(1); }} onRowAction={handleRowAction} /> , accentColor: 'blue', }, ]} /> ); }