Files
igny8/frontend/src/config/pages/review.config.tsx
2025-11-29 11:23:42 +05:00

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