443 lines
14 KiB
TypeScript
443 lines
14 KiB
TypeScript
/**
|
|
* 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 { 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 { usePageSizeStore } from '../../store/pageSizeStore';
|
|
import PageHeader from '../../components/common/PageHeader';
|
|
import ThreeWidgetFooter from '../../components/dashboard/ThreeWidgetFooter';
|
|
|
|
export default function Approved() {
|
|
const toast = useToast();
|
|
const navigate = useNavigate();
|
|
const { activeSector } = useSectorStore();
|
|
const { pageSize } = usePageSizeStore();
|
|
|
|
// Data state
|
|
const [content, setContent] = useState<Content[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
// Filter state - default to approved status
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [publishStatusFilter, setPublishStatusFilter] = useState('');
|
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
|
|
|
// Pagination state
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const [totalPages, setTotalPages] = useState(1);
|
|
const [totalCount, setTotalCount] = useState(0);
|
|
|
|
// Sorting state
|
|
const [sortBy, setSortBy] = useState<string>('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 (
|
|
<>
|
|
<PageHeader
|
|
title="Content Approved"
|
|
badge={{ icon: <CheckCircleIcon />, color: 'green' }}
|
|
parent="Writer"
|
|
/>
|
|
<TablePageTemplate
|
|
columns={pageConfig.columns}
|
|
data={content}
|
|
loading={loading}
|
|
showContent={showContent}
|
|
filters={pageConfig.filters}
|
|
filterValues={{
|
|
search: searchTerm,
|
|
publishStatus: publishStatusFilter,
|
|
}}
|
|
primaryAction={{
|
|
label: 'Publish to Site',
|
|
icon: <BoltIcon className="w-4 h-4" />,
|
|
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}`}
|
|
/>
|
|
|
|
{/* Three Widget Footer - Section 3 Layout */}
|
|
<ThreeWidgetFooter
|
|
submoduleColor="green"
|
|
pageProgress={{
|
|
title: 'Page Progress',
|
|
submoduleColor: 'green',
|
|
metrics: [
|
|
{ label: 'Published', value: totalCount },
|
|
{ label: 'On Site', value: content.filter(c => 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!',
|
|
}}
|
|
moduleStats={{
|
|
title: 'Writer Module',
|
|
pipeline: [
|
|
{
|
|
fromLabel: 'Tasks',
|
|
fromValue: 0,
|
|
fromHref: '/writer/tasks',
|
|
actionLabel: 'Generate Content',
|
|
toLabel: 'Drafts',
|
|
toValue: 0,
|
|
toHref: '/writer/content',
|
|
progress: 0,
|
|
color: 'blue',
|
|
},
|
|
{
|
|
fromLabel: 'Drafts',
|
|
fromValue: 0,
|
|
fromHref: '/writer/content',
|
|
actionLabel: 'Generate Images',
|
|
toLabel: 'Images',
|
|
toValue: 0,
|
|
toHref: '/writer/images',
|
|
progress: 0,
|
|
color: 'purple',
|
|
},
|
|
{
|
|
fromLabel: 'Ready',
|
|
fromValue: 0,
|
|
fromHref: '/writer/review',
|
|
actionLabel: 'Review & Publish',
|
|
toLabel: 'Published',
|
|
toValue: totalCount,
|
|
toHref: '/writer/published',
|
|
progress: 100,
|
|
color: 'green',
|
|
},
|
|
],
|
|
links: [
|
|
{ label: 'Tasks', href: '/writer/tasks' },
|
|
{ label: 'Content', href: '/writer/content' },
|
|
{ label: 'Images', href: '/writer/images' },
|
|
{ label: 'Published', href: '/writer/published' },
|
|
],
|
|
}}
|
|
completion={{
|
|
title: 'Workflow Completion',
|
|
plannerItems: [
|
|
{ label: 'Keywords Clustered', value: 0, color: 'blue' },
|
|
{ label: 'Clusters Created', value: 0, color: 'green' },
|
|
{ label: 'Ideas Generated', value: 0, color: 'amber' },
|
|
],
|
|
writerItems: [
|
|
{ label: 'Content Generated', value: 0, color: 'blue' },
|
|
{ label: 'Images Created', value: 0, color: 'purple' },
|
|
{ label: 'Articles Published', value: totalCount, color: 'green' },
|
|
],
|
|
analyticsHref: '/account/usage',
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
}
|