497 lines
16 KiB
TypeScript
497 lines
16 KiB
TypeScript
/**
|
|
* Tasks Page Configuration
|
|
* Centralized config for Tasks page table, filters, and actions
|
|
*/
|
|
|
|
import React from 'react';
|
|
import {
|
|
titleColumn,
|
|
statusColumn,
|
|
createdColumn,
|
|
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<React.SetStateAction<any>>;
|
|
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',
|
|
toggleable: true,
|
|
toggleContentKey: 'description',
|
|
toggleContentLabel: 'Idea & Content Outline',
|
|
render: (value: string, row: Task) => {
|
|
const isSiteBuilder = value?.startsWith('[Site Builder]');
|
|
const displayTitle = isSiteBuilder && value ? value.replace('[Site Builder] ', '') : (value || 'Untitled');
|
|
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-base font-light text-gray-900 dark:text-white">
|
|
{displayTitle}
|
|
</span>
|
|
{isSiteBuilder && (
|
|
<Badge color="purple" size="xs" variant="soft">
|
|
<span className="text-[11px] font-normal">Site Builder</span>
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
// Sector column - only show when viewing all sectors
|
|
...(showSectorColumn ? [{
|
|
...sectorColumn,
|
|
render: (value: string, row: Task) => (
|
|
<Badge color="info" size="xs" variant="soft">
|
|
<span className="text-[11px] font-normal">{row.sector_name || '-'}</span>
|
|
</Badge>
|
|
),
|
|
}] : []),
|
|
{
|
|
key: 'cluster_name',
|
|
label: 'Cluster',
|
|
sortable: false, // Backend doesn't support sorting by cluster_id
|
|
sortField: 'cluster_id',
|
|
width: '200px',
|
|
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 <span className="text-gray-400 dark:text-gray-500">-</span>;
|
|
}
|
|
return (
|
|
<Badge color="purple" size="xs" variant="soft">
|
|
<span className="text-[11px] font-normal">{taxonomyName}</span>
|
|
</Badge>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
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 (
|
|
<Badge color="blue" size="xs" variant="soft">
|
|
<span className="text-[11px] font-normal">{properCase}</span>
|
|
</Badge>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
key: 'content_structure',
|
|
label: 'Structure',
|
|
sortable: true,
|
|
sortField: 'content_structure',
|
|
width: '130px',
|
|
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 (
|
|
<Badge color="purple" size="xs" variant="soft">
|
|
<span className="text-[11px] font-normal">{properCase}</span>
|
|
</Badge>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
...statusColumn,
|
|
sortable: true,
|
|
sortField: 'status',
|
|
render: (value: string) => {
|
|
const statusColors: Record<string, 'success' | 'amber'> = {
|
|
queued: 'amber',
|
|
completed: 'success',
|
|
};
|
|
const label = value ? value.replace('_', ' ') : '';
|
|
const formatted = label ? label.charAt(0).toUpperCase() + label.slice(1) : '';
|
|
return (
|
|
<Badge color={statusColors[value] || 'amber'} size="xs" variant="soft">
|
|
<span className="text-[11px] font-normal">{formatted}</span>
|
|
</Badge>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
...wordCountColumn,
|
|
sortable: true,
|
|
sortField: 'word_count',
|
|
align: 'center' as const,
|
|
render: (value: number | null | undefined) => (value != null ? value.toLocaleString() : '-'),
|
|
},
|
|
{
|
|
...createdColumn,
|
|
sortable: true,
|
|
sortField: 'created_at',
|
|
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) => (
|
|
<span className="text-sm text-gray-600 dark:text-gray-400 truncate block max-w-[200px]">
|
|
{row.idea_title || '-'}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'keywords',
|
|
label: 'Keywords',
|
|
sortable: false,
|
|
defaultVisible: false,
|
|
width: '200px',
|
|
render: (value: string | null) => (
|
|
<span className="text-sm text-gray-600 dark:text-gray-400 truncate block max-w-[200px]">
|
|
{value || '-'}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'meta_title',
|
|
label: 'Meta Title',
|
|
sortable: false,
|
|
defaultVisible: false,
|
|
width: '200px',
|
|
render: (value: string | null) => (
|
|
<span className="text-sm text-gray-600 dark:text-gray-400 truncate block max-w-[200px]">
|
|
{value || '-'}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'meta_description',
|
|
label: 'Meta Description',
|
|
sortable: false,
|
|
defaultVisible: false,
|
|
width: '250px',
|
|
render: (value: string | null) => (
|
|
<span className="text-sm text-gray-600 dark:text-gray-400 truncate block max-w-[250px]">
|
|
{value || '-'}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'post_url',
|
|
label: 'Post URL',
|
|
sortable: false,
|
|
defaultVisible: false,
|
|
width: '200px',
|
|
render: (value: string | null) => value ? (
|
|
<a
|
|
href={value}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-sm text-brand-600 hover:text-brand-700 dark:text-brand-400 dark:hover:text-brand-300 truncate block max-w-[200px]"
|
|
>
|
|
{value}
|
|
</a>
|
|
) : (
|
|
<span className="text-sm text-gray-400 dark:text-gray-500">-</span>
|
|
),
|
|
},
|
|
{
|
|
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.',
|
|
},
|
|
],
|
|
};
|
|
};
|
|
|