/** * Tasks Page Configuration * Centralized config for Tasks page table, filters, and actions */ import React from 'react'; import { titleColumn, statusColumn, createdWithActionsColumn, wordCountColumn, sectorColumn, } from '../snippets/columns.snippets'; import Badge from '../../components/ui/badge/Badge'; import { formatRelativeDate } from '../../utils/date'; import { Task, Cluster } from '../../services/api'; import { CONTENT_TYPE_OPTIONS, CONTENT_STRUCTURE_BY_TYPE, STRUCTURE_LABELS, TYPE_LABELS } from '../structureMapping'; export interface ColumnConfig { key: string; label: string; sortable?: boolean; sortField?: string; align?: 'left' | 'center' | 'right'; width?: string; render?: (value: any, row: any) => React.ReactNode; toggleable?: boolean; // If true, this column will have a toggle button for expanding content toggleContentKey?: string; // Key of the field containing content to display when toggled toggleContentLabel?: string; // Label for the expanded content (e.g., "Content Outline", "Generated Content") defaultVisible?: boolean; // Whether column is visible by default (default: true) } export interface FormFieldConfig { key: string; label: string; type: 'text' | 'number' | 'select' | 'textarea'; placeholder?: string; required?: boolean; value: any; onChange: (value: any) => void; options?: Array<{ value: string; label: string }>; } export interface FilterConfig { key: string; label: string; type: 'text' | 'select'; placeholder?: string; options?: Array<{ value: string; label: string }>; dynamicOptions?: string; } export interface HeaderMetricConfig { label: string; value: number; accentColor: 'blue' | 'green' | 'amber' | 'purple'; calculate: (data: { tasks: any[]; totalCount: number }) => number; } export interface TasksPageConfig { columns: ColumnConfig[]; filters: FilterConfig[]; formFields: (clusters: Array<{ id: number; name: string }>) => FormFieldConfig[]; headerMetrics: HeaderMetricConfig[]; } export const createTasksPageConfig = ( handlers: { clusters: Array<{ id: number; name: string }>; activeSector: { id: number; name: string } | null; formData: { title: string; description?: string | null; keywords?: string | null; cluster_id?: number | null; idea_id?: number | null; content_structure: string; content_type: string; status: string; word_count?: number; }; setFormData: React.Dispatch>; searchTerm: string; setSearchTerm: (value: string) => void; statusFilter: string; setStatusFilter: (value: string) => void; clusterFilter: string; setClusterFilter: (value: string) => void; structureFilter: string; setStructureFilter: (value: string) => void; typeFilter: string; setTypeFilter: (value: string) => void; sourceFilter: string; setSourceFilter: (value: string) => void; setCurrentPage: (page: number) => void; } ): TasksPageConfig => { const showSectorColumn = !handlers.activeSector; // Show when viewing all sectors return { columns: [ { ...titleColumn, label: 'Content Idea Title', sortable: true, sortField: 'title', width: '400px', toggleable: true, toggleContentKey: 'description', toggleContentLabel: 'Idea & Content Outline', render: (value: string, row: Task) => { const displayTitle = value || 'Untitled'; return (
{displayTitle}
); }, }, // Sector column - only show when viewing all sectors ...(showSectorColumn ? [{ ...sectorColumn, render: (value: string, row: Task) => ( {row.sector_name || '-'} ), }] : []), { key: 'cluster_name', label: 'Cluster', sortable: false, // Backend doesn't support sorting by cluster_id sortField: 'cluster_id', render: (_value: string, row: Task) => ( {row.cluster_name || '-'} ), }, { key: 'taxonomy_name', label: 'Taxonomy', sortable: false, width: '150px', defaultVisible: false, render: (_value: string, row: Task) => { const taxonomyName = row.taxonomy_name; if (!taxonomyName) { return -; } return ( {taxonomyName} ); }, }, { key: 'content_type', label: 'Type', sortable: false, // Backend doesn't support sorting by content_type sortField: 'content_type', 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: true, sortField: 'content_structure', render: (value: string) => { const label = STRUCTURE_LABELS[value] || value || '-'; const properCase = label.split(/[_\s]+/).map(word => word.charAt(0).toUpperCase() + word.slice(1) ).join(' '); return ( {properCase} ); }, }, { ...statusColumn, sortable: true, sortField: 'status', render: (value: string) => { const statusColors: Record = { queued: 'amber', completed: 'success', }; const label = value ? value.replace('_', ' ') : ''; const formatted = label ? label.charAt(0).toUpperCase() + label.slice(1) : ''; return ( {formatted} ); }, }, { ...wordCountColumn, sortable: true, sortField: 'word_count', align: 'center' as const, headingAlign: 'center' as const, render: (value: number | null | undefined) => (value != null ? value.toLocaleString() : '-'), }, { ...createdWithActionsColumn, sortable: true, sortField: 'created_at', width: '130px', render: (value: string) => formatRelativeDate(value), }, // Optional columns - hidden by default { key: 'idea_title', label: 'Idea', sortable: true, sortField: 'idea_id', defaultVisible: false, width: '200px', render: (_value: string, row: Task) => ( {row.idea_title || '-'} ), }, { key: 'keywords', label: 'Keywords', sortable: false, defaultVisible: false, width: '200px', render: (value: string | null) => ( {value || '-'} ), }, { key: 'meta_title', label: 'Meta Title', sortable: false, defaultVisible: false, width: '200px', render: (value: string | null) => ( {value || '-'} ), }, { key: 'meta_description', label: 'Meta Description', sortable: false, defaultVisible: false, width: '250px', render: (value: string | null) => ( {value || '-'} ), }, { key: 'post_url', label: 'Post URL', sortable: false, defaultVisible: false, width: '200px', render: (value: string | null) => value ? ( {value} ) : ( - ), }, { key: 'updated_at', label: 'Modified', sortable: true, sortField: 'updated_at', defaultVisible: false, render: (value: string) => formatRelativeDate(value), }, ], filters: [ { key: 'search', label: 'Search', type: 'text', placeholder: 'Search tasks...', }, { key: 'status', label: 'Status', type: 'select', options: [ { value: '', label: 'All Status' }, { value: 'queued', label: 'Queued' }, { value: 'completed', label: 'Completed' }, ], }, { key: 'content_type', label: 'Content Type', type: 'select', options: [ { value: '', label: 'All Types' }, ...CONTENT_TYPE_OPTIONS, ], }, { key: 'content_structure', label: 'Content Structure', type: 'select', options: [ { value: '', label: 'All Structures' }, { value: 'article', label: 'Article' }, { value: 'guide', label: 'Guide' }, { value: 'comparison', label: 'Comparison' }, { value: 'review', label: 'Review' }, { value: 'listicle', label: 'Listicle' }, { value: 'landing_page', label: 'Landing Page' }, { value: 'business_page', label: 'Business Page' }, { value: 'service_page', label: 'Service Page' }, { value: 'general', label: 'General' }, { value: 'cluster_hub', label: 'Cluster Hub' }, { value: 'product_page', label: 'Product Page' }, { value: 'category_archive', label: 'Category Archive' }, { value: 'tag_archive', label: 'Tag Archive' }, { value: 'attribute_archive', label: 'Attribute Archive' }, ], }, { key: 'cluster_id', label: 'Cluster', type: 'select', options: (() => { return [ { value: '', label: 'All Clusters' }, ...handlers.clusters.map((c) => ({ value: c.id.toString(), label: c.name })), ]; })(), dynamicOptions: 'clusters', }, ], formFields: (clusters: Array<{ id: number; name: string }>) => [ { key: 'title', label: 'Title', type: 'text', placeholder: 'Enter task title', required: true, value: handlers.formData.title || '', onChange: (value: any) => handlers.setFormData({ ...handlers.formData, title: value }), }, { key: 'description', label: 'Description', type: 'textarea', placeholder: 'Enter description', value: handlers.formData.description || '', onChange: (value: any) => handlers.setFormData({ ...handlers.formData, description: value }), }, { key: 'keywords', label: 'Keywords', type: 'text', placeholder: 'Enter keywords (comma-separated)', value: handlers.formData.keywords || '', onChange: (value: any) => handlers.setFormData({ ...handlers.formData, keywords: value }), }, { key: 'cluster_id', label: 'Cluster', type: 'select', value: handlers.formData.cluster_id?.toString() || '', onChange: (value: any) => handlers.setFormData({ ...handlers.formData, cluster_id: value ? parseInt(value) : null, }), options: [ { value: '', label: 'No Cluster' }, ...clusters.map((c) => ({ value: c.id.toString(), label: c.name })), ], }, { key: 'content_structure', label: 'Content Structure', type: 'select', value: handlers.formData.content_structure || 'article', onChange: (value: any) => handlers.setFormData({ ...handlers.formData, content_structure: value }), options: [ { value: 'article', label: 'Article' }, { value: 'guide', label: 'Guide' }, { value: 'comparison', label: 'Comparison' }, { value: 'review', label: 'Review' }, { value: 'listicle', label: 'Listicle' }, { value: 'landing_page', label: 'Landing Page' }, { value: 'business_page', label: 'Business Page' }, { value: 'service_page', label: 'Service Page' }, { value: 'general', label: 'General' }, { value: 'cluster_hub', label: 'Cluster Hub' }, { value: 'product_page', label: 'Product Page' }, { value: 'category_archive', label: 'Category Archive' }, { value: 'tag_archive', label: 'Tag Archive' }, { value: 'attribute_archive', label: 'Attribute Archive' }, ], }, { key: 'content_type', label: 'Content Type', type: 'select', value: handlers.formData.content_type || 'post', onChange: (value: any) => handlers.setFormData({ ...handlers.formData, content_type: value }), options: CONTENT_TYPE_OPTIONS, }, { key: 'status', label: 'Status', type: 'select', value: handlers.formData.status || 'queued', onChange: (value: any) => handlers.setFormData({ ...handlers.formData, status: value }), options: [ { value: 'queued', label: 'Queued' }, { value: 'completed', label: 'Completed' }, ], }, ], headerMetrics: [ { label: 'Tasks', value: 0, accentColor: 'blue' as const, calculate: (data) => data.totalCount || 0, tooltip: 'Total content generation tasks. Tasks process ideas into written content automatically.', }, { label: 'In Queue', value: 0, accentColor: 'amber' as const, calculate: (data) => data.tasks.filter((t: Task) => t.status === 'queued').length, tooltip: 'Tasks queued for processing. These will be picked up by the content generation system.', }, { label: 'Processing', value: 0, accentColor: 'blue' as const, calculate: (data) => data.tasks.filter((t: Task) => t.status === 'in_progress').length, tooltip: 'Tasks currently being processed. Content is being generated by AI right now.', }, { label: 'Completed', value: 0, accentColor: 'green' as const, calculate: (data) => data.tasks.filter((t: Task) => t.status === 'completed').length, tooltip: 'Successfully completed tasks. Generated content is ready for review and publishing.', }, { label: 'Failed', value: 0, accentColor: 'red' as const, calculate: (data) => data.tasks.filter((t: Task) => t.status === 'failed').length, tooltip: 'Failed tasks that need attention. Review error logs and retry or modify the task.', }, ], }; };