Files
igny8/frontend/src/config/pages/tasks.config.tsx
2025-11-26 15:12:14 +00:00

471 lines
15 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,
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="font-medium text-gray-900 dark:text-white">
{displayTitle}
</span>
{isSiteBuilder && (
<Badge color="purple" size="sm" variant="light">
Site Builder
</Badge>
)}
</div>
);
},
},
// Sector column - only show when viewing all sectors
...(showSectorColumn ? [{
...sectorColumn,
render: (value: string, row: Task) => (
<Badge color="info" size="sm" variant="light">
{row.sector_name || '-'}
</Badge>
),
}] : []),
{
key: 'cluster_name',
label: 'Cluster',
sortable: true,
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="sm" variant="light">
{taxonomyName}
</Badge>
);
},
},
{
key: 'content_type',
label: 'Content 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>
),
},
{
...statusColumn,
sortable: true,
sortField: 'status',
render: (value: string) => {
const statusColors: Record<string, 'success' | 'warning'> = {
queued: 'warning',
completed: 'success',
};
const label = value ? value.replace('_', ' ') : '';
const formatted = label ? label.charAt(0).toUpperCase() + label.slice(1) : '';
return (
<Badge
color={statusColors[value] || 'warning'}
size="sm"
>
{formatted}
</Badge>
);
},
},
{
...wordCountColumn,
sortable: true,
sortField: 'word_count',
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: 'Updated',
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: 'Total Tasks',
value: 0,
accentColor: 'blue' as const,
calculate: (data) => data.totalCount || 0,
},
{
label: 'Queued',
value: 0,
accentColor: 'amber' as const,
calculate: (data) => data.tasks.filter((t: Task) => t.status === 'queued').length,
},
{
label: 'Completed',
value: 0,
accentColor: 'green' as const,
calculate: (data) => data.tasks.filter((t: Task) => t.status === 'completed').length,
},
],
};
};