414 lines
14 KiB
TypeScript
414 lines
14 KiB
TypeScript
/**
|
|
* 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<Content[]>([]);
|
|
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<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 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: <TaskIcon /> },
|
|
{ label: 'Content', path: '/writer/content', icon: <FileIcon /> },
|
|
{ label: 'Images', path: '/writer/images', icon: <ImageIcon /> },
|
|
{ label: 'Review', path: '/writer/review', icon: <CheckCircleIcon /> },
|
|
{ label: 'Published', path: '/writer/published', icon: <CheckCircleIcon /> },
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<PageHeader
|
|
title="Content Review"
|
|
badge={{ icon: <CheckCircleIcon />, color: 'blue' }}
|
|
navigation={<ModuleNavigationTabs tabs={writerTabs} />}
|
|
/>
|
|
<TablePageTemplate
|
|
columns={pageConfig.columns}
|
|
data={content}
|
|
loading={loading}
|
|
showContent={showContent}
|
|
filters={pageConfig.filters}
|
|
filterValues={{
|
|
search: searchTerm,
|
|
}}
|
|
onFilterChange={(key, value) => {
|
|
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}
|
|
/>
|
|
<ModuleMetricsFooter
|
|
metrics={[
|
|
{
|
|
title: 'Ready to Publish',
|
|
value: content.length,
|
|
subtitle: 'Total review items',
|
|
icon: <CheckCircleIcon className="w-5 h-5" />,
|
|
accentColor: 'blue',
|
|
},
|
|
]}
|
|
/>
|
|
</>
|
|
);
|
|
}
|