/** * Approved Page Configuration * Centralized config for Approved page table, filters, and actions */ import { Content } from '../../services/api'; import Badge from '../../components/ui/badge/Badge'; import { formatRelativeDate } from '../../utils/date'; import { CheckCircleIcon, ArrowRightIcon } from '../../icons'; import { TYPE_LABELS, CONTENT_TYPE_OPTIONS, ALL_CONTENT_STRUCTURES } from '../structureMapping'; import { getSectorBadgeColor, getStructureBadgeColor, getStructureLabel } from '../../utils/badgeColors'; import type { Sector } from '../../store/sectorStore'; export interface ColumnConfig { key: string; label: string; sortable?: boolean; sortField?: string; align?: 'left' | 'center' | 'right'; width?: string; numeric?: boolean; date?: boolean; render?: (value: any, row: any) => React.ReactNode; toggleable?: boolean; toggleContentKey?: string; toggleContentLabel?: string; defaultVisible?: boolean; } export interface FilterConfig { key: string; label: string; type: 'text' | 'select'; placeholder?: string; options?: Array<{ value: string; label: string }>; } export interface HeaderMetricConfig { label: string; accentColor: 'blue' | 'green' | 'amber' | 'purple'; calculate: (data: { content: Content[]; totalCount: number }) => number; } export interface ApprovedPageConfig { columns: ColumnConfig[]; filters: FilterConfig[]; headerMetrics: HeaderMetricConfig[]; } export function createApprovedPageConfig(params: { searchTerm: string; setSearchTerm: (value: string) => void; statusFilter: string; setStatusFilter: (value: string) => void; siteStatusFilter: string; setSiteStatusFilter: (value: string) => void; setCurrentPage: (page: number) => void; activeSector: { id: number; name: string } | null; sectors?: Sector[]; onRowClick?: (row: Content) => void; statusOptions?: Array<{ value: string; label: string }>; siteStatusOptions?: Array<{ value: string; label: string }>; contentTypeOptions?: Array<{ value: string; label: string }>; contentStructureOptions?: Array<{ value: string; label: string }>; }): ApprovedPageConfig { const showSectorColumn = !params.activeSector; const columns: ColumnConfig[] = [ { key: 'title', label: 'Content Idea Title', sortable: true, sortField: 'title', width: '400px', render: (value: string, row: Content) => (
{params.onRowClick ? ( ) : ( {value || `Content #${row.id}`} )} {row.external_url && ( )}
), }, { key: 'status', label: 'Status', sortable: true, sortField: 'status', render: (value: string, row: Content) => { // Map internal status to standard labels const statusConfig: Record = { 'draft': { color: 'gray', label: 'Draft' }, 'review': { color: 'amber', label: 'Review' }, 'approved': { color: 'blue', label: 'Approved' }, 'published': { color: 'success', label: 'Published' }, }; const config = statusConfig[value] || { color: 'gray' as const, label: value || '-' }; return ( {config.label} ); }, }, { key: 'site_status', label: 'Site Status', sortable: true, sortField: 'site_status', width: '130px', render: (value: string, row: Content) => { // Show actual site_status field const statusConfig: Record = { 'not_published': { color: 'gray', label: 'Not Published' }, 'scheduled': { color: 'amber', label: 'Scheduled' }, 'publishing': { color: 'amber', label: 'Publishing' }, 'published': { color: 'success', label: 'Published' }, 'failed': { color: 'red', label: 'Failed' }, }; const config = statusConfig[value] || { color: 'gray' as const, label: value || 'Not Published' }; return ( {config.label} ); }, }, { key: 'scheduled_publish_at', label: 'Publish Date', sortable: true, sortField: 'scheduled_publish_at', date: true, width: '150px', render: (value: string, row: Content) => { const siteStatus = row.site_status; // For published items: show when it was published if (siteStatus === 'published') { // Use site_status_updated_at if available, otherwise updated_at const publishedAt = row.site_status_updated_at || row.updated_at; if (publishedAt) { return ( {formatRelativeDate(publishedAt)} ); } return Published; } // For failed items: show when the failure occurred if (siteStatus === 'failed') { const failedAt = row.site_status_updated_at || row.updated_at; if (failedAt) { return ( {formatRelativeDate(failedAt)} ); } return Failed; } // For scheduled items: show when it will be published if (siteStatus === 'scheduled' || siteStatus === 'publishing') { if (value) { const publishDate = new Date(value); const now = new Date(); const isFuture = publishDate > now; return ( {formatRelativeDate(value)} ); } return Pending; } // For not_published items: show scheduled date if available if (value) { const publishDate = new Date(value); const now = new Date(); const isFuture = publishDate > now; return ( {formatRelativeDate(value)} ); } return Not scheduled; }, }, { key: 'content_type', label: 'Type', sortable: false, // Backend doesn't support sorting by content_type sortField: 'content_type', width: '110px', render: (value: string) => { const label = TYPE_LABELS[value] || value || '-'; const properCase = label.charAt(0).toUpperCase() + label.slice(1); return ( {properCase} ); }, }, { key: 'content_structure', label: 'Structure', sortable: false, // Backend doesn't support sorting by content_structure sortField: 'content_structure', width: '130px', render: (value: string) => { const properCase = getStructureLabel(value) .split(/[_\s]+/) .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); return ( {properCase} ); }, }, { key: 'cluster_name', label: 'Cluster', sortable: false, width: '130px', render: (_value: any, row: Content) => { const clusterName = row.cluster_name; if (!clusterName) { return -; } return ( {clusterName} ); }, }, { key: 'tags', label: 'Tags', sortable: false, width: '150px', render: (_value: any, row: Content) => { const tags = row.tags || []; if (!tags || tags.length === 0) { return -; } return (
{tags.slice(0, 2).map((tag, index) => ( {tag} ))} {tags.length > 2 && ( +{tags.length - 2} )}
); }, }, { key: 'categories', label: 'Categories', sortable: false, width: '150px', render: (_value: any, row: Content) => { const categories = row.categories || []; if (!categories || categories.length === 0) { return -; } return (
{categories.slice(0, 2).map((category, index) => ( {category} ))} {categories.length > 2 && ( +{categories.length - 2} )}
); }, }, { key: 'word_count', label: 'Words', sortable: false, // Backend doesn't support sorting by word_count sortField: 'word_count', numeric: true, align: 'center' as const, headingAlign: 'center' as const, render: (value: number) => ( {value ? value.toLocaleString() : '-'} ), }, { key: 'created_at', label: 'Created', sortable: true, sortField: 'created_at', date: true, width: '130px', hasActions: true, render: (value: string) => ( {formatRelativeDate(value)} ), }, ]; if (showSectorColumn) { columns.splice(2, 0, { key: 'sector_name', label: 'Sector', sortable: false, width: '120px', render: (value: string, row: Content) => { const color = getSectorBadgeColor(row.sector_id, row.sector_name, params.sectors); return ( {row.sector_name || '-'} ); }, }); } const filters: FilterConfig[] = [ { key: 'search', label: 'Search', type: 'text', placeholder: 'Search approved content...', }, { key: 'status', label: 'Status', type: 'select', options: params.statusOptions, }, { key: 'site_status', label: 'Site Status', type: 'select', options: params.siteStatusOptions, }, { key: 'content_type', label: 'Type', type: 'select', options: params.contentTypeOptions, }, { key: 'content_structure', label: 'Structure', type: 'select', options: params.contentStructureOptions, }, ]; const headerMetrics: HeaderMetricConfig[] = [ { label: 'Content', accentColor: 'blue', calculate: (data: { totalCount: number }) => data.totalCount, tooltip: 'Total content items tracked. Overall volume across all stages.', }, { label: 'Draft', accentColor: 'amber', calculate: (data: { content: Content[] }) => data.content.filter(c => c.status === 'draft').length, tooltip: 'Content written, images not generated. Generate images to move to review.', }, { label: 'In Review', accentColor: 'purple', calculate: (data: { content: Content[] }) => data.content.filter(c => c.status === 'review').length, tooltip: 'Images generated, awaiting approval. Review and approve to publish.', }, { label: 'Approved', accentColor: 'green', calculate: (data: { content: Content[] }) => data.content.filter(c => c.status === 'approved').length, tooltip: 'Approved content awaiting publishing. Publish to site when ready.', }, { label: 'Published', accentColor: 'green', calculate: (data: { content: Content[] }) => data.content.filter(c => c.status === 'published').length, tooltip: 'Live content on your website. Successfully published and accessible.', }, { label: 'Total Images', accentColor: 'blue', calculate: (data: { content: Content[] }) => data.content.filter(c => c.has_generated_images).length, tooltip: 'Total images generated across all content. Tracks visual asset coverage.', }, ]; return { columns, filters, headerMetrics, }; }