243 lines
6.8 KiB
TypeScript
243 lines
6.8 KiB
TypeScript
/**
|
|
* Review Page Configuration
|
|
* Centralized config for Review page table, filters, and actions
|
|
*/
|
|
|
|
import { Content } from '../../services/api';
|
|
import { Link } from 'react-router-dom';
|
|
import Badge from '../../components/ui/badge/Badge';
|
|
import { formatRelativeDate } from '../../utils/date';
|
|
import { CheckCircleIcon } from '../../icons';
|
|
import { STRUCTURE_LABELS, TYPE_LABELS } from '../structureMapping';
|
|
|
|
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 ReviewPageConfig {
|
|
columns: ColumnConfig[];
|
|
filters: FilterConfig[];
|
|
headerMetrics: HeaderMetricConfig[];
|
|
}
|
|
|
|
export function createReviewPageConfig(params: {
|
|
searchTerm: string;
|
|
setSearchTerm: (value: string) => void;
|
|
statusFilter: string;
|
|
setStatusFilter: (value: string) => void;
|
|
setCurrentPage: (page: number) => void;
|
|
activeSector: { id: number; name: string } | null;
|
|
}): ReviewPageConfig {
|
|
const showSectorColumn = !params.activeSector;
|
|
|
|
const columns: ColumnConfig[] = [
|
|
// Title first, then categories and tags (moved after title per change request)
|
|
{
|
|
key: 'title',
|
|
label: 'Title',
|
|
sortable: true,
|
|
sortField: 'title',
|
|
toggleable: true,
|
|
toggleContentKey: 'content_html',
|
|
toggleContentLabel: 'Generated Content',
|
|
render: (value: string, row: Content) => (
|
|
<div className="flex items-center gap-2">
|
|
<Link to={`/writer/content/${row.id}`} className="font-medium text-gray-900 dark:text-white hover:underline">
|
|
{value || `Content #${row.id}`}
|
|
</Link>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: 'categories',
|
|
label: 'Categories',
|
|
sortable: false,
|
|
width: '180px',
|
|
render: (_value: any, row: Content) => {
|
|
const categories = row.taxonomy_terms_data?.filter((t: any) => t.taxonomy_type === 'category') || [];
|
|
if (!categories.length) return <span className="text-gray-400 dark:text-gray-500">-</span>;
|
|
return (
|
|
<div className="flex flex-wrap gap-1">
|
|
{categories.map((cat: any) => (
|
|
<span key={cat.id} className="px-2 py-0.5 bg-purple-50 dark:bg-purple-900/20 text-purple-700 dark:text-purple-300 rounded-full text-xs font-medium">{cat.name}</span>
|
|
))}
|
|
</div>
|
|
);
|
|
},
|
|
toggleable: false,
|
|
defaultVisible: true,
|
|
},
|
|
{
|
|
key: 'tags',
|
|
label: 'Tags',
|
|
sortable: false,
|
|
width: '180px',
|
|
render: (_value: any, row: Content) => {
|
|
const tags = row.taxonomy_terms_data?.filter((t: any) => t.taxonomy_type === 'tag') || [];
|
|
if (!tags.length) return <span className="text-gray-400 dark:text-gray-500">-</span>;
|
|
return (
|
|
<div className="flex flex-wrap gap-1">
|
|
{tags.map((tag: any) => (
|
|
<span key={tag.id} className="px-2 py-0.5 bg-brand-50 dark:bg-brand-900/20 text-brand-700 dark:text-brand-300 rounded-full text-xs font-medium">{tag.name}</span>
|
|
))}
|
|
</div>
|
|
);
|
|
},
|
|
toggleable: false,
|
|
defaultVisible: true,
|
|
},
|
|
{
|
|
key: 'title',
|
|
label: 'Title',
|
|
sortable: true,
|
|
sortField: 'title',
|
|
toggleable: true,
|
|
toggleContentKey: 'content_html',
|
|
toggleContentLabel: 'Generated Content',
|
|
render: (value: string, row: Content) => (
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-medium text-gray-900 dark:text-white">
|
|
{value || `Content #${row.id}`}
|
|
</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: 'content_type',
|
|
label: 'Type',
|
|
sortable: true,
|
|
sortField: 'content_type',
|
|
width: '120px',
|
|
render: (value: string) => (
|
|
<Badge color="primary" size="sm" variant="light">
|
|
{TYPE_LABELS[value] || value || '-'}
|
|
</Badge>
|
|
),
|
|
},
|
|
{
|
|
key: 'content_structure',
|
|
label: 'Structure',
|
|
sortable: true,
|
|
sortField: 'content_structure',
|
|
width: '150px',
|
|
render: (value: string) => (
|
|
<Badge color="info" size="sm" variant="light">
|
|
{STRUCTURE_LABELS[value] || value || '-'}
|
|
</Badge>
|
|
),
|
|
},
|
|
{
|
|
key: 'cluster_name',
|
|
label: 'Cluster',
|
|
sortable: false,
|
|
width: '150px',
|
|
render: (_value: any, row: Content) => {
|
|
const clusterName = row.cluster_name;
|
|
if (!clusterName) {
|
|
return <span className="text-gray-400 dark:text-gray-500">-</span>;
|
|
}
|
|
return (
|
|
<Badge color="primary" size="sm" variant="light">
|
|
{clusterName}
|
|
</Badge>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
key: 'word_count',
|
|
label: 'Words',
|
|
sortable: true,
|
|
sortField: 'word_count',
|
|
numeric: true,
|
|
width: '100px',
|
|
render: (value: number) => (
|
|
<span className="font-mono text-sm text-gray-700 dark:text-gray-300">
|
|
{value?.toLocaleString() || 0}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'created_at',
|
|
label: 'Created',
|
|
sortable: true,
|
|
sortField: 'created_at',
|
|
date: true,
|
|
align: 'right',
|
|
render: (value: string) => (
|
|
<span className="text-gray-700 dark:text-gray-300 whitespace-nowrap">
|
|
{formatRelativeDate(value)}
|
|
</span>
|
|
),
|
|
},
|
|
];
|
|
|
|
if (showSectorColumn) {
|
|
columns.splice(4, 0, {
|
|
key: 'sector_name',
|
|
label: 'Sector',
|
|
sortable: false,
|
|
width: '120px',
|
|
render: (value: string, row: Content) => (
|
|
<Badge color="info" size="sm" variant="light">
|
|
{row.sector_name || '-'}
|
|
</Badge>
|
|
),
|
|
});
|
|
}
|
|
|
|
return {
|
|
columns,
|
|
filters: [
|
|
{
|
|
key: 'search',
|
|
label: 'Search',
|
|
type: 'text',
|
|
placeholder: 'Search content...',
|
|
},
|
|
],
|
|
headerMetrics: [
|
|
{
|
|
label: 'Total Ready',
|
|
accentColor: 'blue',
|
|
calculate: ({ totalCount }) => totalCount,
|
|
},
|
|
{
|
|
label: 'Has Images',
|
|
accentColor: 'green',
|
|
calculate: ({ content }) => content.filter(c => c.has_generated_images).length,
|
|
},
|
|
{
|
|
label: 'Optimized',
|
|
accentColor: 'purple',
|
|
calculate: ({ content }) => content.filter(c => (c as any).optimization_score >= 80).length,
|
|
},
|
|
],
|
|
};
|
|
}
|