/** * Content Page - Built with TablePageTemplate * Displays content from Content table with filters and pagination */ import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import TablePageTemplate from '../../templates/TablePageTemplate'; import { fetchContent, fetchImages, Content as ContentType, ContentFilters, generateImagePrompts, deleteContent, bulkDeleteContent, } from '../../services/api'; import { optimizerApi } from '../../api/optimizer.api'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { FileIcon, TaskIcon, CheckCircleIcon, ArrowRightIcon } from '../../icons'; import { createContentPageConfig } from '../../config/pages/content.config'; import { useSectorStore } from '../../store/sectorStore'; import { useSiteStore } from '../../store/siteStore'; import { usePageSizeStore } from '../../store/pageSizeStore'; import ProgressModal from '../../components/common/ProgressModal'; import { useProgressModal } from '../../hooks/useProgressModal'; import PageHeader from '../../components/common/PageHeader'; import StandardThreeWidgetFooter from '../../components/dashboard/StandardThreeWidgetFooter'; import { PencilSquareIcon } from '@heroicons/react/24/outline'; export default function Content() { const toast = useToast(); const { activeSite } = useSiteStore(); const { activeSector } = useSectorStore(); 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 [totalDraft, setTotalDraft] = useState(0); const [totalReview, setTotalReview] = useState(0); const [totalPublished, setTotalPublished] = useState(0); const [totalImagesCount, setTotalImagesCount] = useState(0); // Filter state const [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState('draft'); const [sourceFilter, setSourceFilter] = 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); // Progress modal for AI functions const progressModal = useProgressModal(); const hasReloadedRef = useRef(false); // Load total metrics for footer widget and header metrics (site-wide totals, no sector filter) const loadTotalMetrics = useCallback(async () => { try { // Batch all API calls in parallel for better performance const [allRes, draftRes, reviewRes, publishedRes, imagesRes] = await Promise.all([ // Get all content (site-wide) fetchContent({ page_size: 1, site_id: activeSite?.id, }), // Get content with status='draft' fetchContent({ page_size: 1, site_id: activeSite?.id, status: 'draft', }), // Get content with status='review' fetchContent({ page_size: 1, site_id: activeSite?.id, status: 'review', }), // Get content with status='approved' or 'published' (ready for publishing or on site) fetchContent({ page_size: 1, site_id: activeSite?.id, status__in: 'approved,published', }), // Get actual total images count fetchImages({ page_size: 1 }), ]); setTotalCount(allRes.count || 0); setTotalDraft(draftRes.count || 0); setTotalReview(reviewRes.count || 0); setTotalPublished(publishedRes.count || 0); setTotalImagesCount(imagesRes.count || 0); } catch (error) { console.error('Error loading total metrics:', error); } }, [activeSite]); // Load total metrics when sector changes useEffect(() => { loadTotalMetrics(); }, [loadTotalMetrics]); // Load content - wrapped in useCallback 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 }), ...(sourceFilter && { source: sourceFilter }), page: currentPage, page_size: pageSize, ordering, }; const data = 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, statusFilter, sortBy, sortDirection, searchTerm, activeSector, 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 || 'generated_at'); setSortDirection(direction); setCurrentPage(1); }; const navigate = useNavigate(); // Handle row click - navigate to content view const handleRowClick = useCallback((row: ContentType) => { navigate(`/writer/content/${row.id}`); }, [navigate]); // Create page config const pageConfig = useMemo(() => { return createContentPageConfig({ activeSector, searchTerm, setSearchTerm, statusFilter, setStatusFilter, setCurrentPage, onRowClick: handleRowClick, }); }, [ activeSector, searchTerm, statusFilter, handleRowClick, ]); // 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 = totalCount || 0; break; case 'Draft': // Use totalDraft from loadTotalMetrics() value = totalDraft; break; case 'In Review': // Use totalReview from loadTotalMetrics() value = totalReview; break; case 'Published': // Use totalPublished from loadTotalMetrics() value = totalPublished; break; default: value = metric.calculate({ content, totalCount }); } return { label: metric.label, value, accentColor: metric.accentColor, tooltip: (metric as any).tooltip, }; }); }, [pageConfig?.headerMetrics, content, totalCount, totalDraft, totalReview, totalPublished]); const handleRowAction = useCallback(async (action: string, row: ContentType) => { 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 === 'generate_image_prompts') { try { const result = await generateImagePrompts([row.id]); if (result.success) { if (result.task_id) { // Open progress modal for async task progressModal.openModal( result.task_id, 'Smart Image Prompts', 'ai-generate-image-prompts-01-desktop' ); } else { // Synchronous completion toast.success(`Image prompts generation task started. Task ID: ${result.task_id || 'N/A'}`); loadContent(); // Reload to show new prompts } } else { toast.error(result.error || 'Failed to generate image prompts'); } } catch (error: any) { toast.error(`Failed to generate prompts: ${error.message}`); } } else if (action === 'optimize') { try { const result = await optimizerApi.optimize(row.id, 'writer'); toast.success(`Content optimized! Score: ${result.scores_after.overall_score.toFixed(1)}`); loadContent(); // Reload to show updated scores } catch (error: any) { toast.error(`Failed to optimize content: ${error.message}`); } } else if (action === 'send_to_optimizer') { navigate(`/optimizer/content?contentId=${row.id}`); } }, [toast, progressModal, 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]); return ( <> , color: 'orange' }} parent="Writer" /> { if (key === 'search') { setSearchTerm(value); } else if (key === 'status') { setStatusFilter(value); setCurrentPage(1); } else if (key === 'source') { setSourceFilter(value); setCurrentPage(1); } }} pagination={{ currentPage, totalPages, totalCount, onPageChange: setCurrentPage, }} sorting={{ sortBy, sortDirection, onSort: handleSort, }} selection={{ selectedIds, onSelectionChange: setSelectedIds, }} headerMetrics={headerMetrics} onRowAction={handleRowAction} onDelete={handleDelete} onBulkDelete={handleBulkDelete} getItemDisplayName={(row: ContentType) => row.title || `Content #${row.id}`} /> {/* Three Widget Footer - Section 3 Layout with Standardized Workflow Widget */} c.has_generated_images).length, percentage: `${totalDraft > 0 ? Math.round((content.filter(c => c.has_generated_images).length / totalDraft) * 100) : 0}%` }, { label: 'In Review', value: totalReview }, { label: 'Published', value: totalPublished }, ], progress: { value: totalDraft > 0 ? Math.round((content.filter(c => c.has_generated_images).length / totalDraft) * 100) : 0, label: 'Have Images', color: 'blue', }, hint: content.filter(c => c.status === 'draft' && !c.has_generated_images).length > 0 ? `${content.filter(c => c.status === 'draft' && !c.has_generated_images).length} drafts need images before review` : 'All drafts have images!', statusInsight: content.filter(c => c.status === 'draft' && !c.has_generated_images).length > 0 ? `Generate images for drafts, then submit to Review.` : totalDraft > 0 ? `Select drafts and submit to Review for approval.` : `No drafts. Generate content from Tasks page.`, }} module="writer" showCredits={true} analyticsHref="/account/usage" /> {/* Progress Modal for AI Functions */} { const wasCompleted = progressModal.progress.status === 'completed'; progressModal.closeModal(); // Reload data after modal closes (if completed) if (wasCompleted && !hasReloadedRef.current) { hasReloadedRef.current = true; loadContent(); setTimeout(() => { hasReloadedRef.current = false; }, 1000); } }} /> ); }