tbaels fitlers and plnaner writer other fixes

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-19 12:27:10 +00:00
parent cbb32b1c9d
commit 6c7395262f
16 changed files with 169 additions and 95 deletions

View File

@@ -22,7 +22,7 @@ export const HeaderMetrics: React.FC = () => {
)} )}
</span> </span>
<span className={`igny8-header-metric-value ${isCredits ? 'igny8-header-metric-value-credits' : ''}`}> <span className={`igny8-header-metric-value ${isCredits ? 'igny8-header-metric-value-credits' : ''}`}>
{typeof metric.value === 'number' ? metric.value.toLocaleString() : metric.value} {metric.displayValue ?? (typeof metric.value === 'number' ? metric.value.toLocaleString() : metric.value)}
</span> </span>
</div> </div>
); );

View File

@@ -3,6 +3,7 @@
* Centralized config for Approved page table, filters, and actions * Centralized config for Approved page table, filters, and actions
*/ */
import { Link } from 'react-router-dom';
import { Content } from '../../services/api'; import { Content } from '../../services/api';
import Badge from '../../components/ui/badge/Badge'; import Badge from '../../components/ui/badge/Badge';
import { formatRelativeDate } from '../../utils/date'; import { formatRelativeDate } from '../../utils/date';
@@ -68,7 +69,7 @@ export function createApprovedPageConfig(params: {
const columns: ColumnConfig[] = [ const columns: ColumnConfig[] = [
{ {
key: 'title', key: 'title',
label: 'Content Idea Title', label: 'Content Title',
sortable: true, sortable: true,
sortField: 'title', sortField: 'title',
width: '400px', width: '400px',
@@ -77,12 +78,12 @@ export function createApprovedPageConfig(params: {
{params.onRowClick ? ( {params.onRowClick ? (
<button <button
onClick={() => params.onRowClick!(row)} onClick={() => params.onRowClick!(row)}
className="text-sm text-brand-500 hover:text-brand-600 hover:underline text-left transition-colors" className="text-sm font-medium text-brand-600 hover:text-brand-900 dark:text-brand-400 dark:hover:text-brand-200 text-left transition-colors"
> >
{value || `Content #${row.id}`} {value || `Content #${row.id}`}
</button> </button>
) : ( ) : (
<span className="text-sm text-gray-900 dark:text-white"> <span className="text-sm font-medium text-brand-600 dark:text-brand-400">
{value || `Content #${row.id}`} {value || `Content #${row.id}`}
</span> </span>
)} )}
@@ -220,6 +221,7 @@ export function createApprovedPageConfig(params: {
sortable: false, // Backend doesn't support sorting by content_type sortable: false, // Backend doesn't support sorting by content_type
sortField: 'content_type', sortField: 'content_type',
width: '110px', width: '110px',
defaultVisible: false,
render: (value: string) => { render: (value: string) => {
const label = TYPE_LABELS[value] || value || '-'; const label = TYPE_LABELS[value] || value || '-';
const properCase = label.charAt(0).toUpperCase() + label.slice(1); const properCase = label.charAt(0).toUpperCase() + label.slice(1);
@@ -236,6 +238,7 @@ export function createApprovedPageConfig(params: {
sortable: false, // Backend doesn't support sorting by content_structure sortable: false, // Backend doesn't support sorting by content_structure
sortField: 'content_structure', sortField: 'content_structure',
width: '130px', width: '130px',
defaultVisible: false,
render: (value: string) => { render: (value: string) => {
const properCase = getStructureLabel(value) const properCase = getStructureLabel(value)
.split(/[_\s]+/) .split(/[_\s]+/)
@@ -252,16 +255,18 @@ export function createApprovedPageConfig(params: {
key: 'cluster_name', key: 'cluster_name',
label: 'Cluster', label: 'Cluster',
sortable: false, sortable: false,
width: '130px',
render: (_value: any, row: Content) => { render: (_value: any, row: Content) => {
const clusterName = row.cluster_name; const clusterName = row.cluster_name;
if (!clusterName) { if (!clusterName || !row.cluster_id) {
return <span className="text-gray-400 dark:text-gray-500 text-[11px]">-</span>; return '-';
} }
return ( return (
<Badge color="indigo" size="xs" variant="soft"> <Link
<span className="text-[11px] font-normal">{clusterName}</span> to={`/planner/clusters/${row.cluster_id}`}
</Badge> className="text-brand-600 hover:text-brand-900 dark:text-brand-400 dark:hover:text-brand-200"
>
{clusterName}
</Link>
); );
}, },
}, },
@@ -349,6 +354,7 @@ export function createApprovedPageConfig(params: {
label: 'Sector', label: 'Sector',
sortable: false, sortable: false,
width: '120px', width: '120px',
defaultVisible: false,
render: (value: string, row: Content) => { render: (value: string, row: Content) => {
const color = getSectorBadgeColor(row.sector_id, row.sector_name, params.sectors); const color = getSectorBadgeColor(row.sector_id, row.sector_name, params.sectors);
return ( return (
@@ -429,11 +435,11 @@ export function createApprovedPageConfig(params: {
tooltip: 'Live content on your website. Successfully published and accessible.', tooltip: 'Live content on your website. Successfully published and accessible.',
}, },
{ {
label: 'Total Images', label: 'Images',
accentColor: 'blue', accentColor: 'blue',
calculate: (data: { content: Content[] }) => calculate: (data: { content: Content[] }) =>
data.content.filter(c => c.has_generated_images).length, data.content.filter(c => c.has_generated_images).length,
tooltip: 'Total images generated across all content. Tracks visual asset coverage.', tooltip: 'Generated images / Total images. Tracks visual asset coverage.',
}, },
]; ];

View File

@@ -113,7 +113,7 @@ export const createClustersPageConfig = (
render: (value: string, row: Cluster) => ( render: (value: string, row: Cluster) => (
<Link <Link
to={`/planner/clusters/${row.id}`} to={`/planner/clusters/${row.id}`}
className="text-sm font-medium text-brand-600 hover:text-brand-700 dark:text-brand-400 dark:hover:text-brand-300" className="text-sm font-medium text-brand-600 hover:text-brand-900 dark:text-brand-400 dark:hover:text-brand-200"
> >
{value} {value}
</Link> </Link>

View File

@@ -4,6 +4,7 @@
*/ */
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom';
import { import {
titleColumn, titleColumn,
createdWithActionsColumn, createdWithActionsColumn,
@@ -101,7 +102,7 @@ export const createContentPageConfig = (
columns: [ columns: [
{ {
...titleColumn, ...titleColumn,
label: 'Content Idea Title', label: 'Content Title',
sortable: true, sortable: true,
sortField: 'title', sortField: 'title',
width: '400px', width: '400px',
@@ -110,12 +111,12 @@ export const createContentPageConfig = (
{handlers.onRowClick ? ( {handlers.onRowClick ? (
<button <button
onClick={() => handlers.onRowClick!(row)} onClick={() => handlers.onRowClick!(row)}
className="text-sm text-brand-500 hover:text-brand-600 hover:underline text-left transition-colors" className="text-sm font-medium text-brand-600 hover:text-brand-900 dark:text-brand-400 dark:hover:text-brand-200 text-left transition-colors"
> >
{row.title || `Content #${row.id}`} {row.title || `Content #${row.id}`}
</button> </button>
) : ( ) : (
<div className="text-sm text-gray-900 dark:text-white"> <div className="text-sm font-medium text-brand-600 dark:text-brand-400">
{row.title || `Content #${row.id}`} {row.title || `Content #${row.id}`}
</div> </div>
)} )}
@@ -124,6 +125,7 @@ export const createContentPageConfig = (
}, },
...(showSectorColumn ? [{ ...(showSectorColumn ? [{
...sectorColumn, ...sectorColumn,
defaultVisible: false,
render: (value: string, row: Content) => ( render: (value: string, row: Content) => (
<Badge color={getSectorBadgeColor((row as any).sector_id, row.sector_name || undefined, handlers.sectors)} size="xs" variant="soft"> <Badge color={getSectorBadgeColor((row as any).sector_id, row.sector_name || undefined, handlers.sectors)} size="xs" variant="soft">
<span className="text-[11px] font-normal">{row.sector_name || '-'}</span> <span className="text-[11px] font-normal">{row.sector_name || '-'}</span>
@@ -170,13 +172,16 @@ export const createContentPageConfig = (
sortable: false, sortable: false,
render: (_value: any, row: Content) => { render: (_value: any, row: Content) => {
const clusterName = row.cluster_name; const clusterName = row.cluster_name;
if (!clusterName) { if (!clusterName || !row.cluster_id) {
return <span className="text-gray-400 dark:text-gray-500">-</span>; return '-';
} }
return ( return (
<span className="text-gray-800 dark:text-white"> <Link
to={`/planner/clusters/${row.cluster_id}`}
className="text-brand-600 hover:text-brand-900 dark:text-brand-400 dark:hover:text-brand-200"
>
{clusterName} {clusterName}
</span> </Link>
); );
}, },
}, },
@@ -235,6 +240,7 @@ export const createContentPageConfig = (
sortable: true, sortable: true,
sortField: 'source', sortField: 'source',
width: '90px', width: '90px',
defaultVisible: false,
render: (value: any, row: Content) => { render: (value: any, row: Content) => {
const source = value || row.source || 'igny8'; const source = value || row.source || 'igny8';
const sourceColors: Record<string, 'teal' | 'cyan'> = { const sourceColors: Record<string, 'teal' | 'cyan'> = {
@@ -292,7 +298,7 @@ export const createContentPageConfig = (
sortable: true, sortable: true,
sortField: 'created_at', sortField: 'created_at',
label: 'Created', label: 'Created',
width: '130px', width: '180px',
render: (value: string, row: Content) => { render: (value: string, row: Content) => {
// Prompt icon logic (unchanged) // Prompt icon logic (unchanged)
const hasPrompts = row.has_image_prompts || false; const hasPrompts = row.has_image_prompts || false;
@@ -439,11 +445,11 @@ export const createContentPageConfig = (
tooltip: 'Live content on your website. Successfully published and accessible.', tooltip: 'Live content on your website. Successfully published and accessible.',
}, },
{ {
label: 'Total Images', label: 'Images',
value: 0, value: 0,
accentColor: 'blue' as const, accentColor: 'blue' as const,
calculate: (data) => 0, calculate: (data) => 0,
tooltip: 'Total images generated across all content. Tracks visual asset coverage.', tooltip: 'Generated images / Total images. Tracks visual asset coverage.',,
}, },
], ],
}; };

View File

@@ -160,7 +160,7 @@ export const createIdeasPageConfig = (
label: 'Primary Keywords', label: 'Primary Keywords',
sortable: false, sortable: false,
render: (value: string) => ( render: (value: string) => (
<span className="text-sm font-medium text-gray-700 dark:text-gray-300"> <span className="text-sm text-gray-600 dark:text-gray-400">
{value || '-'} {value || '-'}
</span> </span>
), ),
@@ -185,7 +185,7 @@ export const createIdeasPageConfig = (
return ( return (
<Link <Link
to={`/planner/clusters/${row.keyword_cluster_id}`} to={`/planner/clusters/${row.keyword_cluster_id}`}
className="text-brand-600 hover:text-brand-700 dark:text-brand-400 dark:hover:text-brand-300 hover:underline" className="text-brand-600 hover:text-brand-900 dark:text-brand-400 dark:hover:text-brand-200"
> >
{row.keyword_cluster_name} {row.keyword_cluster_name}
</Link> </Link>
@@ -219,6 +219,7 @@ export const createIdeasPageConfig = (
sortField: 'estimated_word_count', sortField: 'estimated_word_count',
align: 'center' as const, align: 'center' as const,
headingAlign: 'center' as const, headingAlign: 'center' as const,
defaultVisible: false,
render: (value: number) => value.toLocaleString(), render: (value: number) => value.toLocaleString(),
}, },
{ {

View File

@@ -58,7 +58,7 @@ export const createImagesPageConfig = (
onImageClick?: (contentId: number, imageType: 'featured' | 'in_article', position?: number) => void; // Handler for image click onImageClick?: (contentId: number, imageType: 'featured' | 'in_article', position?: number) => void; // Handler for image click
} }
): ImagesPageConfig => { ): ImagesPageConfig => {
const maxImages = handlers.maxInArticleImages || 5; // Default to 5 in-article images const maxImages = handlers.maxInArticleImages || 4; // Default to 4 in-article images
// Build columns dynamically based on max in-article images // Build columns dynamically based on max in-article images
const columns: ColumnConfig[] = [ const columns: ColumnConfig[] = [
@@ -86,7 +86,7 @@ export const createImagesPageConfig = (
<div> <div>
<a <a
href={`/writer/content/${row.content_id}`} href={`/writer/content/${row.content_id}`}
className="text-base font-light text-brand-500 hover:text-brand-600 dark:text-brand-400" className="text-sm font-medium text-brand-600 hover:text-brand-900 dark:text-brand-400 dark:hover:text-brand-200"
> >
{row.content_title} {row.content_title}
</a> </a>
@@ -102,9 +102,9 @@ export const createImagesPageConfig = (
}, },
{ {
key: 'featured_image', key: 'featured_image',
label: 'Featured Image', label: 'Featured',
sortable: false, sortable: false,
width: '150px', width: '100px',
render: (_value: any, row: ContentImagesGroup) => ( render: (_value: any, row: ContentImagesGroup) => (
<ContentImageCell <ContentImageCell
image={row.featured_image} image={row.featured_image}
@@ -122,9 +122,9 @@ export const createImagesPageConfig = (
const displayIndex = i + 1; // 1-indexed for display const displayIndex = i + 1; // 1-indexed for display
columns.push({ columns.push({
key: `in_article_${displayIndex}`, key: `in_article_${displayIndex}`,
label: `In-Article ${displayIndex}`, label: `Art ${displayIndex}`,
sortable: false, sortable: false,
width: '150px', width: '100px',
render: (_value: any, row: ContentImagesGroup) => { render: (_value: any, row: ContentImagesGroup) => {
const image = row.in_article_images.find(img => img.position === i); // 0-indexed position const image = row.in_article_images.find(img => img.position === i); // 0-indexed position
return ( return (
@@ -249,11 +249,11 @@ export const createImagesPageConfig = (
tooltip: 'Live content on your website. Successfully published and accessible.', tooltip: 'Live content on your website. Successfully published and accessible.',
}, },
{ {
label: 'Total Images', label: 'Images',
value: 0, value: 0,
accentColor: 'blue' as const, accentColor: 'blue' as const,
calculate: (data) => data.images.filter((i: ContentImagesGroup) => i.overall_status === 'complete').length, calculate: (data) => data.images.filter((i: ContentImagesGroup) => i.overall_status === 'complete').length,
tooltip: 'Total images generated across all content. Tracks visual asset coverage.', tooltip: 'Generated images / Total images. Tracks visual asset coverage.',
}, },
], ],
maxInArticleImages: maxImages, maxInArticleImages: maxImages,

View File

@@ -7,6 +7,7 @@
*/ */
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom';
import { import {
keywordColumn, keywordColumn,
volumeColumn, volumeColumn,
@@ -182,11 +183,14 @@ export const createKeywordsPageConfig = (
sortable: false, // Backend doesn't support sorting by cluster_id sortable: false, // Backend doesn't support sorting by cluster_id
sortField: 'cluster_id', sortField: 'cluster_id',
width: '300px', width: '300px',
render: (_value: string, row: Keyword) => row.cluster_name ? ( render: (_value: string, row: Keyword) => row.cluster_name && row.cluster_id ? (
<span className="text-gray-800 dark:text-white"> <Link
to={`/planner/clusters/${row.cluster_id}`}
className="text-brand-600 hover:text-brand-900 dark:text-brand-400 dark:hover:text-brand-200"
>
{row.cluster_name} {row.cluster_name}
</span> </Link>
) : <span className="text-gray-400">-</span>, ) : '-',
}, },
{ {
...difficultyColumn, ...difficultyColumn,

View File

@@ -3,6 +3,7 @@
* Centralized config for Review page table, filters, and actions * Centralized config for Review page table, filters, and actions
*/ */
import { Link } from 'react-router-dom';
import { Content } from '../../services/api'; import { Content } from '../../services/api';
import Badge from '../../components/ui/badge/Badge'; import Badge from '../../components/ui/badge/Badge';
import { formatRelativeDate } from '../../utils/date'; import { formatRelativeDate } from '../../utils/date';
@@ -70,7 +71,7 @@ export function createReviewPageConfig(params: {
const columns: ColumnConfig[] = [ const columns: ColumnConfig[] = [
{ {
key: 'title', key: 'title',
label: 'Content Idea Title', label: 'Content Title',
sortable: true, sortable: true,
sortField: 'title', sortField: 'title',
width: '400px', width: '400px',
@@ -79,12 +80,12 @@ export function createReviewPageConfig(params: {
{params.onRowClick ? ( {params.onRowClick ? (
<button <button
onClick={() => params.onRowClick!(row)} onClick={() => params.onRowClick!(row)}
className="text-sm text-brand-500 hover:text-brand-600 hover:underline text-left transition-colors" className="text-sm font-medium text-brand-600 hover:text-brand-900 dark:text-brand-400 dark:hover:text-brand-200 text-left transition-colors"
> >
{value || `Content #${row.id}`} {value || `Content #${row.id}`}
</button> </button>
) : ( ) : (
<span className="text-sm text-gray-900 dark:text-white"> <span className="text-sm font-medium text-brand-600 dark:text-brand-400">
{value || `Content #${row.id}`} {value || `Content #${row.id}`}
</span> </span>
)} )}
@@ -175,13 +176,16 @@ export function createReviewPageConfig(params: {
sortable: false, sortable: false,
render: (_value: any, row: Content) => { render: (_value: any, row: Content) => {
const clusterName = row.cluster_name; const clusterName = row.cluster_name;
if (!clusterName) { if (!clusterName || !row.cluster_id) {
return <span className="text-gray-400 dark:text-gray-500">-</span>; return '-';
} }
return ( return (
<span className="text-gray-800 dark:text-white"> <Link
to={`/planner/clusters/${row.cluster_id}`}
className="text-brand-600 hover:text-brand-900 dark:text-brand-400 dark:hover:text-brand-200"
>
{clusterName} {clusterName}
</span> </Link>
); );
}, },
}, },
@@ -292,10 +296,10 @@ export function createReviewPageConfig(params: {
tooltip: 'Live content on your website. Successfully published and accessible.', tooltip: 'Live content on your website. Successfully published and accessible.',
}, },
{ {
label: 'Total Images', label: 'Images',
accentColor: 'blue', accentColor: 'blue',
calculate: ({ content }) => content.filter(c => c.has_generated_images).length, calculate: ({ content }) => content.filter(c => c.has_generated_images).length,
tooltip: 'Total images generated across all content. Tracks visual asset coverage.', tooltip: 'Generated images / Total images. Tracks visual asset coverage.',
}, },
], ],
}; };

View File

@@ -4,6 +4,7 @@
*/ */
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom';
import { import {
titleColumn, titleColumn,
statusColumn, statusColumn,
@@ -106,7 +107,7 @@ export const createTasksPageConfig = (
columns: [ columns: [
{ {
...titleColumn, ...titleColumn,
label: 'Content Idea Title', label: 'Content Title',
sortable: true, sortable: true,
sortField: 'title', sortField: 'title',
width: '400px', width: '400px',
@@ -139,11 +140,14 @@ export const createTasksPageConfig = (
label: 'Cluster', label: 'Cluster',
sortable: false, // Backend doesn't support sorting by cluster_id sortable: false, // Backend doesn't support sorting by cluster_id
sortField: 'cluster_id', sortField: 'cluster_id',
render: (_value: string, row: Task) => ( render: (_value: string, row: Task) => row.cluster_name && row.cluster_id ? (
<span className="text-gray-800 dark:text-white"> <Link
{row.cluster_name || '-'} to={`/planner/clusters/${row.cluster_id}`}
</span> className="text-brand-600 hover:text-brand-900 dark:text-brand-400 dark:hover:text-brand-200"
), >
{row.cluster_name}
</Link>
) : '-',
}, },
{ {
key: 'taxonomy_name', key: 'taxonomy_name',
@@ -466,11 +470,11 @@ export const createTasksPageConfig = (
tooltip: 'Live content on your website. Successfully published and accessible.', tooltip: 'Live content on your website. Successfully published and accessible.',
}, },
{ {
label: 'Total Images', label: 'Images',
value: 0, value: 0,
accentColor: 'blue' as const, accentColor: 'blue' as const,
calculate: (data) => 0, calculate: (data) => 0,
tooltip: 'Total images generated across all content. Tracks visual asset coverage.', tooltip: 'Generated images / Total images. Tracks visual asset coverage.',,
}, },
], ],
}; };

View File

@@ -3,6 +3,7 @@ import { createContext, useContext, useState, ReactNode, useRef, useCallback } f
interface HeaderMetric { interface HeaderMetric {
label: string; label: string;
value: string | number; value: string | number;
displayValue?: string; // Optional display value (e.g., "300/600" for generated/total)
accentColor: 'blue' | 'green' | 'amber' | 'purple'; accentColor: 'blue' | 'green' | 'amber' | 'purple';
tooltip?: string; // Actionable insight for this metric tooltip?: string; // Actionable insight for this metric
} }

View File

@@ -47,8 +47,9 @@ export default function Ideas() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
// Total counts for footer widget (not page-filtered) // Total counts for footer widget (not page-filtered)
const [totalInTasks, setTotalInTasks] = useState(0); const [totalNew, setTotalNew] = useState(0);
const [totalPending, setTotalPending] = useState(0); const [totalQueued, setTotalQueued] = useState(0);
const [totalCompleted, setTotalCompleted] = useState(0);
const [totalImagesCount, setTotalImagesCount] = useState(0); const [totalImagesCount, setTotalImagesCount] = useState(0);
// Actual total count (unfiltered) for header metrics - not affected by filters // Actual total count (unfiltered) for header metrics - not affected by filters
const [actualTotalIdeas, setActualTotalIdeas] = useState(0); const [actualTotalIdeas, setActualTotalIdeas] = useState(0);
@@ -182,8 +183,9 @@ export default function Ideas() {
]); ]);
setActualTotalIdeas(allRes.count || 0); // Store actual total (unfiltered) for header metrics setActualTotalIdeas(allRes.count || 0); // Store actual total (unfiltered) for header metrics
setTotalInTasks((queuedRes.count || 0) + (completedRes.count || 0)); setTotalNew(newRes.count || 0);
setTotalPending(newRes.count || 0); setTotalQueued(queuedRes.count || 0);
setTotalCompleted(completedRes.count || 0);
setTotalImagesCount(imagesRes.count || 0); setTotalImagesCount(imagesRes.count || 0);
} catch (error) { } catch (error) {
console.error('Error loading total metrics:', error); console.error('Error loading total metrics:', error);
@@ -364,7 +366,7 @@ export default function Ideas() {
}); });
}, [clusters, activeSector, formData, searchTerm, statusFilter, clusterFilter, structureFilter, typeFilter, statusOptions, contentTypeOptions, contentStructureOptions, clusterOptions]); }, [clusters, activeSector, formData, searchTerm, statusFilter, clusterFilter, structureFilter, typeFilter, statusOptions, contentTypeOptions, contentStructureOptions, clusterOptions]);
// Calculate header metrics - use actualTotalIdeas/totalInTasks/totalPending from API calls (not page data) // Calculate header metrics - use actual counts from API calls (not page data)
// This ensures metrics show correct totals across all pages, not just current page // This ensures metrics show correct totals across all pages, not just current page
// Note: actualTotalIdeas is NOT affected by filters - it always shows the true total // Note: actualTotalIdeas is NOT affected by filters - it always shows the true total
const headerMetrics = useMemo(() => { const headerMetrics = useMemo(() => {
@@ -380,16 +382,16 @@ export default function Ideas() {
value = actualTotalIdeas || 0; value = actualTotalIdeas || 0;
break; break;
case 'New': case 'New':
// Use totalPending from loadTotalMetrics() (ideas with status='new') // Use totalNew from loadTotalMetrics() (ideas with status='new')
value = totalPending; value = totalNew;
break; break;
case 'Queued': case 'Queued':
// Use totalInTasks from loadTotalMetrics() (ideas with status='queued') // Use totalQueued from loadTotalMetrics() (ideas with status='queued')
value = totalInTasks; value = totalQueued;
break; break;
case 'Completed': case 'Completed':
// Calculate completed from totalCount - (totalPending + totalInTasks) // Use totalCompleted from loadTotalMetrics() (ideas with status='completed')
value = Math.max(0, totalCount - totalPending - totalInTasks); value = totalCompleted;
break; break;
default: default:
value = metric.calculate({ ideas, totalCount }); value = metric.calculate({ ideas, totalCount });
@@ -402,7 +404,7 @@ export default function Ideas() {
tooltip: (metric as any).tooltip, tooltip: (metric as any).tooltip,
}; };
}); });
}, [pageConfig?.headerMetrics, ideas, totalCount, totalPending, totalInTasks, actualTotalIdeas]); }, [pageConfig?.headerMetrics, ideas, totalCount, totalNew, totalQueued, totalCompleted, actualTotalIdeas]);
const resetForm = useCallback(() => { const resetForm = useCallback(() => {
setFormData({ setFormData({
@@ -565,21 +567,21 @@ export default function Ideas() {
submoduleColor: 'amber', submoduleColor: 'amber',
metrics: [ metrics: [
{ label: 'Ideas', value: totalCount }, { label: 'Ideas', value: totalCount },
{ label: 'In Tasks', value: totalInTasks, percentage: `${totalCount > 0 ? Math.round((totalInTasks / totalCount) * 100) : 0}%` }, { label: 'In Tasks', value: totalQueued + totalCompleted, percentage: `${totalCount > 0 ? Math.round(((totalQueued + totalCompleted) / totalCount) * 100) : 0}%` },
{ label: 'Pending', value: totalPending }, { label: 'Pending', value: totalNew },
{ label: 'From Clusters', value: clusters.length }, { label: 'From Clusters', value: clusters.length },
], ],
progress: { progress: {
value: totalCount > 0 ? Math.round((totalInTasks / totalCount) * 100) : 0, value: totalCount > 0 ? Math.round(((totalQueued + totalCompleted) / totalCount) * 100) : 0,
label: 'Converted', label: 'Converted',
color: 'amber', color: 'amber',
}, },
hint: totalPending > 0 hint: totalNew > 0
? `${totalPending} ideas ready to become tasks` ? `${totalNew} ideas ready to become tasks`
: 'All ideas converted!', : 'All ideas converted!',
statusInsight: totalPending > 0 statusInsight: totalNew > 0
? `Select ideas and queue them to Writer to start content generation.` ? `Select ideas and queue them to Writer to start content generation.`
: totalInTasks > 0 : (totalQueued + totalCompleted) > 0
? `Ideas queued. Go to Writer Tasks to generate content.` ? `Ideas queued. Go to Writer Tasks to generate content.`
: `No ideas yet. Generate ideas from Clusters page.`, : `No ideas yet. Generate ideas from Clusters page.`,
}} }}

View File

@@ -52,6 +52,7 @@ export default function Approved() {
const [totalApproved, setTotalApproved] = useState(0); const [totalApproved, setTotalApproved] = useState(0);
const [totalPublished, setTotalPublished] = useState(0); const [totalPublished, setTotalPublished] = useState(0);
const [totalImagesCount, setTotalImagesCount] = useState(0); const [totalImagesCount, setTotalImagesCount] = useState(0);
const [generatedImagesCount, setGeneratedImagesCount] = useState(0);
// Dynamic filter options (loaded from backend) // Dynamic filter options (loaded from backend)
const [statusOptions, setStatusOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined); const [statusOptions, setStatusOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined);
@@ -144,13 +145,14 @@ export default function Approved() {
const loadTotalMetrics = useCallback(async () => { const loadTotalMetrics = useCallback(async () => {
try { try {
// Fetch counts in parallel for performance // Fetch counts in parallel for performance
const [allRes, draftRes, reviewRes, approvedRes, publishedRes, imagesRes] = await Promise.all([ const [allRes, draftRes, reviewRes, approvedRes, publishedRes, imagesRes, generatedImagesRes] = await Promise.all([
fetchContent({ page_size: 1, site_id: activeSite?.id }), fetchContent({ page_size: 1, site_id: activeSite?.id }),
fetchContent({ page_size: 1, status: 'draft', site_id: activeSite?.id }), fetchContent({ page_size: 1, status: 'draft', site_id: activeSite?.id }),
fetchContent({ page_size: 1, status: 'review', site_id: activeSite?.id }), fetchContent({ page_size: 1, status: 'review', site_id: activeSite?.id }),
fetchContent({ page_size: 1, status: 'approved', site_id: activeSite?.id }), fetchContent({ page_size: 1, status: 'approved', site_id: activeSite?.id }),
fetchContent({ page_size: 1, status: 'published', site_id: activeSite?.id }), fetchContent({ page_size: 1, status: 'published', site_id: activeSite?.id }),
fetchImages({ page_size: 1, site_id: activeSite?.id }), fetchImages({ page_size: 1, site_id: activeSite?.id }),
fetchImages({ page_size: 1, site_id: activeSite?.id, status: 'generated' }),
]); ]);
setTotalContent(allRes.count || 0); setTotalContent(allRes.count || 0);
@@ -159,6 +161,7 @@ export default function Approved() {
setTotalApproved(approvedRes.count || 0); setTotalApproved(approvedRes.count || 0);
setTotalPublished(publishedRes.count || 0); setTotalPublished(publishedRes.count || 0);
setTotalImagesCount(imagesRes.count || 0); setTotalImagesCount(imagesRes.count || 0);
setGeneratedImagesCount(generatedImagesRes.count || 0);
} catch (error) { } catch (error) {
console.error('Error loading total metrics:', error); console.error('Error loading total metrics:', error);
} }
@@ -706,9 +709,15 @@ export default function Approved() {
case 'Published': case 'Published':
value = totalPublished; value = totalPublished;
break; break;
case 'Total Images': case 'Images':
value = totalImagesCount; value = totalImagesCount;
break; return {
label: metric.label,
displayValue: `${generatedImagesCount}/${totalImagesCount}`,
value,
accentColor: metric.accentColor,
tooltip: (metric as any).tooltip,
};
default: default:
value = metric.calculate({ content, totalCount }); value = metric.calculate({ content, totalCount });
} }
@@ -720,7 +729,7 @@ export default function Approved() {
tooltip: (metric as any).tooltip, tooltip: (metric as any).tooltip,
}; };
}); });
}, [pageConfig?.headerMetrics, content, totalCount, totalContent, totalDraft, totalReview, totalApproved, totalPublished, totalImagesCount]); }, [pageConfig?.headerMetrics, content, totalCount, totalContent, totalDraft, totalReview, totalApproved, totalPublished, totalImagesCount, generatedImagesCount]);
return ( return (
<> <>

View File

@@ -46,6 +46,7 @@ export default function Content() {
const [totalApproved, setTotalApproved] = useState(0); const [totalApproved, setTotalApproved] = useState(0);
const [totalPublished, setTotalPublished] = useState(0); const [totalPublished, setTotalPublished] = useState(0);
const [totalImagesCount, setTotalImagesCount] = useState(0); const [totalImagesCount, setTotalImagesCount] = useState(0);
const [generatedImagesCount, setGeneratedImagesCount] = useState(0);
// Dynamic filter options (loaded from backend) // Dynamic filter options (loaded from backend)
const [sourceOptions, setSourceOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined); const [sourceOptions, setSourceOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined);
@@ -122,7 +123,7 @@ export default function Content() {
const loadTotalMetrics = useCallback(async () => { const loadTotalMetrics = useCallback(async () => {
try { try {
// Batch all API calls in parallel for better performance // Batch all API calls in parallel for better performance
const [allRes, draftRes, reviewRes, approvedRes, publishedRes, imagesRes] = await Promise.all([ const [allRes, draftRes, reviewRes, approvedRes, publishedRes, imagesRes, generatedImagesRes] = await Promise.all([
// Get all content (site-wide) // Get all content (site-wide)
fetchContent({ fetchContent({
page_size: 1, page_size: 1,
@@ -152,8 +153,10 @@ export default function Content() {
site_id: activeSite?.id, site_id: activeSite?.id,
status: 'published', status: 'published',
}), }),
// Get actual total images count // Get total images count
fetchImages({ page_size: 1, site_id: activeSite?.id }), fetchImages({ page_size: 1, site_id: activeSite?.id }),
// Get generated images count
fetchImages({ page_size: 1, site_id: activeSite?.id, status: 'generated' }),
]); ]);
setTotalContent(allRes.count || 0); setTotalContent(allRes.count || 0);
@@ -162,6 +165,7 @@ export default function Content() {
setTotalApproved(approvedRes.count || 0); setTotalApproved(approvedRes.count || 0);
setTotalPublished(publishedRes.count || 0); setTotalPublished(publishedRes.count || 0);
setTotalImagesCount(imagesRes.count || 0); setTotalImagesCount(imagesRes.count || 0);
setGeneratedImagesCount(generatedImagesRes.count || 0);
} catch (error) { } catch (error) {
console.error('Error loading total metrics:', error); console.error('Error loading total metrics:', error);
} }
@@ -324,9 +328,15 @@ export default function Content() {
case 'Published': case 'Published':
value = totalPublished; value = totalPublished;
break; break;
case 'Total Images': case 'Images':
value = totalImagesCount; value = totalImagesCount;
break; return {
label: metric.label,
displayValue: `${generatedImagesCount}/${totalImagesCount}`,
value,
accentColor: metric.accentColor,
tooltip: (metric as any).tooltip,
};
default: default:
value = metric.calculate({ content, totalCount }); value = metric.calculate({ content, totalCount });
} }
@@ -338,7 +348,7 @@ export default function Content() {
tooltip: (metric as any).tooltip, tooltip: (metric as any).tooltip,
}; };
}); });
}, [pageConfig?.headerMetrics, content, totalContent, totalDraft, totalReview, totalApproved, totalPublished, totalImagesCount]); }, [pageConfig?.headerMetrics, content, totalContent, totalDraft, totalReview, totalApproved, totalPublished, totalImagesCount, generatedImagesCount]);
const handleRowAction = useCallback(async (action: string, row: ContentType) => { const handleRowAction = useCallback(async (action: string, row: ContentType) => {
if (action === 'view_on_wordpress') { if (action === 'view_on_wordpress') {

View File

@@ -47,6 +47,7 @@ export default function Images() {
const [totalApproved, setTotalApproved] = useState(0); const [totalApproved, setTotalApproved] = useState(0);
const [totalPublished, setTotalPublished] = useState(0); const [totalPublished, setTotalPublished] = useState(0);
const [totalImagesCount, setTotalImagesCount] = useState(0); const [totalImagesCount, setTotalImagesCount] = useState(0);
const [generatedImagesCount, setGeneratedImagesCount] = useState(0);
// Footer widget specific counts (image-based) // Footer widget specific counts (image-based)
const [totalComplete, setTotalComplete] = useState(0); const [totalComplete, setTotalComplete] = useState(0);
@@ -119,13 +120,14 @@ export default function Images() {
const loadTotalMetrics = useCallback(async () => { const loadTotalMetrics = useCallback(async () => {
try { try {
// Fetch counts in parallel for performance // Fetch counts in parallel for performance
const [allRes, draftRes, reviewRes, approvedRes, publishedRes, imagesRes] = await Promise.all([ const [allRes, draftRes, reviewRes, approvedRes, publishedRes, imagesRes, generatedImagesRes] = await Promise.all([
fetchContent({ page_size: 1, site_id: activeSite?.id }), fetchContent({ page_size: 1, site_id: activeSite?.id }),
fetchContent({ page_size: 1, status: 'draft', site_id: activeSite?.id }), fetchContent({ page_size: 1, status: 'draft', site_id: activeSite?.id }),
fetchContent({ page_size: 1, status: 'review', site_id: activeSite?.id }), fetchContent({ page_size: 1, status: 'review', site_id: activeSite?.id }),
fetchContent({ page_size: 1, status: 'approved', site_id: activeSite?.id }), fetchContent({ page_size: 1, status: 'approved', site_id: activeSite?.id }),
fetchContent({ page_size: 1, status: 'published', site_id: activeSite?.id }), fetchContent({ page_size: 1, status: 'published', site_id: activeSite?.id }),
fetchImages({ page_size: 1, site_id: activeSite?.id }), fetchImages({ page_size: 1, site_id: activeSite?.id }),
fetchImages({ page_size: 1, site_id: activeSite?.id, status: 'generated' }),
]); ]);
setTotalContent(allRes.count || 0); setTotalContent(allRes.count || 0);
@@ -134,6 +136,7 @@ export default function Images() {
setTotalApproved(approvedRes.count || 0); setTotalApproved(approvedRes.count || 0);
setTotalPublished(publishedRes.count || 0); setTotalPublished(publishedRes.count || 0);
setTotalImagesCount(imagesRes.count || 0); setTotalImagesCount(imagesRes.count || 0);
setGeneratedImagesCount(generatedImagesRes.count || 0);
} catch (error) { } catch (error) {
console.error('Error loading total metrics:', error); console.error('Error loading total metrics:', error);
} }
@@ -553,7 +556,13 @@ export default function Images() {
break; break;
case 'Total Images': case 'Total Images':
value = totalImagesCount; value = totalImagesCount;
break; return {
label: metric.label,
displayValue: `${generatedImagesCount}/${totalImagesCount}`,
value,
accentColor: metric.accentColor,
tooltip: (metric as any).tooltip,
};
default: default:
value = metric.calculate({ images, totalCount }); value = metric.calculate({ images, totalCount });
} }
@@ -567,7 +576,7 @@ export default function Images() {
}); });
return baseMetrics; return baseMetrics;
}, [pageConfig?.headerMetrics, images, totalCount, totalContent, totalDraft, totalReview, totalApproved, totalPublished, totalImagesCount]); }, [pageConfig?.headerMetrics, images, totalCount, totalContent, totalDraft, totalReview, totalApproved, totalPublished, totalImagesCount, generatedImagesCount]);
return ( return (
<> <>

View File

@@ -7,7 +7,7 @@
import { useState, useEffect, useMemo, useCallback } from 'react'; import { useState, useEffect, useMemo, useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import TablePageTemplate from '../../templates/TablePageTemplate'; import TablePageTemplate from '../../templates/TablePageTemplate';
const { activeSector, sectors } = useSectorStore(); import {
fetchContent, fetchContent,
fetchImages, fetchImages,
fetchWriterContentFilterOptions, fetchWriterContentFilterOptions,
@@ -28,7 +28,7 @@ import PageHeader from '../../components/common/PageHeader';
export default function Review() { export default function Review() {
const toast = useToast(); const toast = useToast();
const navigate = useNavigate(); const navigate = useNavigate();
const { activeSector } = useSectorStore(); const { activeSector, sectors } = useSectorStore();
const { activeSite } = useSiteStore(); const { activeSite } = useSiteStore();
// Data state // Data state
@@ -42,6 +42,7 @@ export default function Review() {
const [totalApproved, setTotalApproved] = useState(0); const [totalApproved, setTotalApproved] = useState(0);
const [totalPublished, setTotalPublished] = useState(0); const [totalPublished, setTotalPublished] = useState(0);
const [totalImagesCount, setTotalImagesCount] = useState(0); const [totalImagesCount, setTotalImagesCount] = useState(0);
const [generatedImagesCount, setGeneratedImagesCount] = useState(0);
// Dynamic filter options (loaded from backend) // Dynamic filter options (loaded from backend)
const [siteStatusOptions, setSiteStatusOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined); const [siteStatusOptions, setSiteStatusOptions] = useState<Array<{value: string; label: string}> | undefined>(undefined);
@@ -144,13 +145,14 @@ export default function Review() {
const loadTotalMetrics = useCallback(async () => { const loadTotalMetrics = useCallback(async () => {
try { try {
// Fetch counts in parallel for performance // Fetch counts in parallel for performance
const [allRes, draftRes, reviewRes, approvedRes, publishedRes, imagesRes] = await Promise.all([ const [allRes, draftRes, reviewRes, approvedRes, publishedRes, imagesRes, generatedImagesRes] = await Promise.all([
fetchContent({ page_size: 1, site_id: activeSite?.id }), fetchContent({ page_size: 1, site_id: activeSite?.id }),
fetchContent({ page_size: 1, status: 'draft', site_id: activeSite?.id }), fetchContent({ page_size: 1, status: 'draft', site_id: activeSite?.id }),
fetchContent({ page_size: 1, status: 'review', site_id: activeSite?.id }), fetchContent({ page_size: 1, status: 'review', site_id: activeSite?.id }),
fetchContent({ page_size: 1, status: 'approved', site_id: activeSite?.id }), fetchContent({ page_size: 1, status: 'approved', site_id: activeSite?.id }),
fetchContent({ page_size: 1, status: 'published', site_id: activeSite?.id }), fetchContent({ page_size: 1, status: 'published', site_id: activeSite?.id }),
fetchImages({ page_size: 1, site_id: activeSite?.id }), fetchImages({ page_size: 1, site_id: activeSite?.id }),
fetchImages({ page_size: 1, site_id: activeSite?.id, status: 'generated' }),
]); ]);
setTotalContent(allRes.count || 0); setTotalContent(allRes.count || 0);
@@ -159,6 +161,7 @@ export default function Review() {
setTotalApproved(approvedRes.count || 0); setTotalApproved(approvedRes.count || 0);
setTotalPublished(publishedRes.count || 0); setTotalPublished(publishedRes.count || 0);
setTotalImagesCount(imagesRes.count || 0); setTotalImagesCount(imagesRes.count || 0);
setGeneratedImagesCount(generatedImagesRes.count || 0);
} catch (error) { } catch (error) {
console.error('Error loading metrics:', error); console.error('Error loading metrics:', error);
} }
@@ -252,9 +255,14 @@ export default function Review() {
case 'Published': case 'Published':
value = totalPublished; value = totalPublished;
break; break;
case 'Total Images': case 'Images':
value = totalImagesCount; value = totalImagesCount;
break; return {
...metric,
displayValue: `${generatedImagesCount}/${totalImagesCount}`,
value,
tooltip: (metric as any).tooltip,
};
default: default:
value = metric.calculate({ content, totalCount }); value = metric.calculate({ content, totalCount });
} }
@@ -264,7 +272,7 @@ export default function Review() {
tooltip: (metric as any).tooltip, tooltip: (metric as any).tooltip,
}; };
}), }),
[pageConfig.headerMetrics, content, totalCount, totalContent, totalDraft, totalReview, totalApproved, totalPublished, totalImagesCount] [pageConfig.headerMetrics, content, totalCount, totalContent, totalDraft, totalReview, totalApproved, totalPublished, totalImagesCount, generatedImagesCount]
); );
// Export handler // Export handler

View File

@@ -55,6 +55,7 @@ export default function Tasks() {
const [totalApproved, setTotalApproved] = useState(0); const [totalApproved, setTotalApproved] = useState(0);
const [totalPublished, setTotalPublished] = useState(0); const [totalPublished, setTotalPublished] = useState(0);
const [totalImagesCount, setTotalImagesCount] = useState(0); const [totalImagesCount, setTotalImagesCount] = useState(0);
const [generatedImagesCount, setGeneratedImagesCount] = useState(0);
// Footer widget specific counts (task-based) // Footer widget specific counts (task-based)
const [totalQueued, setTotalQueued] = useState(0); const [totalQueued, setTotalQueued] = useState(0);
@@ -163,7 +164,7 @@ export default function Tasks() {
const loadTotalMetrics = useCallback(async () => { const loadTotalMetrics = useCallback(async () => {
try { try {
// Batch all API calls in parallel for better performance // Batch all API calls in parallel for better performance
const [allRes, draftRes, reviewRes, approvedRes, publishedRes, imagesRes] = await Promise.all([ const [allRes, draftRes, reviewRes, approvedRes, publishedRes, imagesRes, generatedImagesRes] = await Promise.all([
// Get all content (site-wide) // Get all content (site-wide)
fetchContent({ fetchContent({
page_size: 1, page_size: 1,
@@ -195,6 +196,8 @@ export default function Tasks() {
}), }),
// Get actual total images count // Get actual total images count
fetchImages({ page_size: 1, site_id: activeSite?.id }), fetchImages({ page_size: 1, site_id: activeSite?.id }),
// Get generated images count
fetchImages({ page_size: 1, site_id: activeSite?.id, status: 'generated' }),
]); ]);
setTotalContent(allRes.count || 0); setTotalContent(allRes.count || 0);
@@ -203,6 +206,7 @@ export default function Tasks() {
setTotalApproved(approvedRes.count || 0); setTotalApproved(approvedRes.count || 0);
setTotalPublished(publishedRes.count || 0); setTotalPublished(publishedRes.count || 0);
setTotalImagesCount(imagesRes.count || 0); setTotalImagesCount(imagesRes.count || 0);
setGeneratedImagesCount(generatedImagesRes.count || 0);
} catch (error) { } catch (error) {
console.error('Error loading total metrics:', error); console.error('Error loading total metrics:', error);
} }
@@ -454,9 +458,15 @@ export default function Tasks() {
case 'Published': case 'Published':
value = totalPublished; value = totalPublished;
break; break;
case 'Total Images': case 'Images':
value = totalImagesCount; value = totalImagesCount;
break; return {
label: metric.label,
displayValue: `${generatedImagesCount}/${totalImagesCount}`,
value,
accentColor: metric.accentColor,
tooltip: (metric as any).tooltip,
};
default: default:
value = metric.calculate({ tasks, totalCount }); value = metric.calculate({ tasks, totalCount });
} }
@@ -468,7 +478,7 @@ export default function Tasks() {
tooltip: (metric as any).tooltip, tooltip: (metric as any).tooltip,
}; };
}); });
}, [pageConfig?.headerMetrics, tasks, totalContent, totalDraft, totalReview, totalApproved, totalPublished, totalImagesCount]); }, [pageConfig?.headerMetrics, tasks, totalContent, totalDraft, totalReview, totalApproved, totalPublished, totalImagesCount, generatedImagesCount]);
const resetForm = useCallback(() => { const resetForm = useCallback(() => {
setFormData({ setFormData({