Files
igny8/frontend/src/pages/Writer/Review.tsx
2025-12-01 09:32:06 +05:00

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',
},
]}
/>
</>
);
}