/**
* 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,
};
}