diff --git a/frontend/src/components/header/HeaderMetrics.tsx b/frontend/src/components/header/HeaderMetrics.tsx index 33cfd6b2..a988acd0 100644 --- a/frontend/src/components/header/HeaderMetrics.tsx +++ b/frontend/src/components/header/HeaderMetrics.tsx @@ -22,7 +22,7 @@ export const HeaderMetrics: React.FC = () => { )} - {typeof metric.value === 'number' ? metric.value.toLocaleString() : metric.value} + {metric.displayValue ?? (typeof metric.value === 'number' ? metric.value.toLocaleString() : metric.value)} ); diff --git a/frontend/src/config/pages/approved.config.tsx b/frontend/src/config/pages/approved.config.tsx index c22e7b57..87be4461 100644 --- a/frontend/src/config/pages/approved.config.tsx +++ b/frontend/src/config/pages/approved.config.tsx @@ -3,6 +3,7 @@ * Centralized config for Approved page table, filters, and actions */ +import { Link } from 'react-router-dom'; import { Content } from '../../services/api'; import Badge from '../../components/ui/badge/Badge'; import { formatRelativeDate } from '../../utils/date'; @@ -68,7 +69,7 @@ export function createApprovedPageConfig(params: { const columns: ColumnConfig[] = [ { key: 'title', - label: 'Content Idea Title', + label: 'Content Title', sortable: true, sortField: 'title', width: '400px', @@ -77,12 +78,12 @@ export function createApprovedPageConfig(params: { {params.onRowClick ? ( ) : ( - + {value || `Content #${row.id}`} )} @@ -220,6 +221,7 @@ export function createApprovedPageConfig(params: { sortable: false, // Backend doesn't support sorting by content_type sortField: 'content_type', width: '110px', + defaultVisible: false, render: (value: string) => { const label = TYPE_LABELS[value] || value || '-'; 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 sortField: 'content_structure', width: '130px', + defaultVisible: false, render: (value: string) => { const properCase = getStructureLabel(value) .split(/[_\s]+/) @@ -252,16 +255,18 @@ export function createApprovedPageConfig(params: { key: 'cluster_name', label: 'Cluster', sortable: false, - width: '130px', render: (_value: any, row: Content) => { const clusterName = row.cluster_name; - if (!clusterName) { - return -; + if (!clusterName || !row.cluster_id) { + return '-'; } return ( - - {clusterName} - + + {clusterName} + ); }, }, @@ -349,6 +354,7 @@ export function createApprovedPageConfig(params: { label: 'Sector', sortable: false, width: '120px', + defaultVisible: false, render: (value: string, row: Content) => { const color = getSectorBadgeColor(row.sector_id, row.sector_name, params.sectors); return ( @@ -429,11 +435,11 @@ export function createApprovedPageConfig(params: { tooltip: 'Live content on your website. Successfully published and accessible.', }, { - label: 'Total Images', + label: 'Images', accentColor: 'blue', calculate: (data: { content: Content[] }) => 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.', }, ]; diff --git a/frontend/src/config/pages/clusters.config.tsx b/frontend/src/config/pages/clusters.config.tsx index 376703b0..710873fb 100644 --- a/frontend/src/config/pages/clusters.config.tsx +++ b/frontend/src/config/pages/clusters.config.tsx @@ -113,7 +113,7 @@ export const createClustersPageConfig = ( render: (value: string, row: Cluster) => ( {value} diff --git a/frontend/src/config/pages/content.config.tsx b/frontend/src/config/pages/content.config.tsx index 16674a98..fdca559d 100644 --- a/frontend/src/config/pages/content.config.tsx +++ b/frontend/src/config/pages/content.config.tsx @@ -4,6 +4,7 @@ */ import React from 'react'; +import { Link } from 'react-router-dom'; import { titleColumn, createdWithActionsColumn, @@ -101,7 +102,7 @@ export const createContentPageConfig = ( columns: [ { ...titleColumn, - label: 'Content Idea Title', + label: 'Content Title', sortable: true, sortField: 'title', width: '400px', @@ -110,12 +111,12 @@ export const createContentPageConfig = ( {handlers.onRowClick ? ( ) : ( -
+
{row.title || `Content #${row.id}`}
)} @@ -124,6 +125,7 @@ export const createContentPageConfig = ( }, ...(showSectorColumn ? [{ ...sectorColumn, + defaultVisible: false, render: (value: string, row: Content) => ( {row.sector_name || '-'} @@ -170,13 +172,16 @@ export const createContentPageConfig = ( sortable: false, render: (_value: any, row: Content) => { const clusterName = row.cluster_name; - if (!clusterName) { - return -; + if (!clusterName || !row.cluster_id) { + return '-'; } return ( - + {clusterName} - + ); }, }, @@ -235,6 +240,7 @@ export const createContentPageConfig = ( sortable: true, sortField: 'source', width: '90px', + defaultVisible: false, render: (value: any, row: Content) => { const source = value || row.source || 'igny8'; const sourceColors: Record = { @@ -292,7 +298,7 @@ export const createContentPageConfig = ( sortable: true, sortField: 'created_at', label: 'Created', - width: '130px', + width: '180px', render: (value: string, row: Content) => { // Prompt icon logic (unchanged) const hasPrompts = row.has_image_prompts || false; @@ -439,11 +445,11 @@ export const createContentPageConfig = ( tooltip: 'Live content on your website. Successfully published and accessible.', }, { - label: 'Total Images', + label: 'Images', value: 0, accentColor: 'blue' as const, calculate: (data) => 0, - tooltip: 'Total images generated across all content. Tracks visual asset coverage.', + tooltip: 'Generated images / Total images. Tracks visual asset coverage.',, }, ], }; diff --git a/frontend/src/config/pages/ideas.config.tsx b/frontend/src/config/pages/ideas.config.tsx index 3e1f35d4..81014e3b 100644 --- a/frontend/src/config/pages/ideas.config.tsx +++ b/frontend/src/config/pages/ideas.config.tsx @@ -160,7 +160,7 @@ export const createIdeasPageConfig = ( label: 'Primary Keywords', sortable: false, render: (value: string) => ( - + {value || '-'} ), @@ -185,7 +185,7 @@ export const createIdeasPageConfig = ( return ( {row.keyword_cluster_name} @@ -219,6 +219,7 @@ export const createIdeasPageConfig = ( sortField: 'estimated_word_count', align: 'center' as const, headingAlign: 'center' as const, + defaultVisible: false, render: (value: number) => value.toLocaleString(), }, { diff --git a/frontend/src/config/pages/images.config.tsx b/frontend/src/config/pages/images.config.tsx index 7f75d9a1..f5fd8528 100644 --- a/frontend/src/config/pages/images.config.tsx +++ b/frontend/src/config/pages/images.config.tsx @@ -58,7 +58,7 @@ export const createImagesPageConfig = ( onImageClick?: (contentId: number, imageType: 'featured' | 'in_article', position?: number) => void; // Handler for image click } ): 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 const columns: ColumnConfig[] = [ @@ -86,7 +86,7 @@ export const createImagesPageConfig = (
{row.content_title} @@ -102,9 +102,9 @@ export const createImagesPageConfig = ( }, { key: 'featured_image', - label: 'Featured Image', + label: 'Featured', sortable: false, - width: '150px', + width: '100px', render: (_value: any, row: ContentImagesGroup) => ( { const image = row.in_article_images.find(img => img.position === i); // 0-indexed position return ( @@ -249,11 +249,11 @@ export const createImagesPageConfig = ( tooltip: 'Live content on your website. Successfully published and accessible.', }, { - label: 'Total Images', + label: 'Images', value: 0, accentColor: 'blue' as const, 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, diff --git a/frontend/src/config/pages/keywords.config.tsx b/frontend/src/config/pages/keywords.config.tsx index de4bc0d8..70eceb0c 100644 --- a/frontend/src/config/pages/keywords.config.tsx +++ b/frontend/src/config/pages/keywords.config.tsx @@ -7,6 +7,7 @@ */ import React from 'react'; +import { Link } from 'react-router-dom'; import { keywordColumn, volumeColumn, @@ -182,11 +183,14 @@ export const createKeywordsPageConfig = ( sortable: false, // Backend doesn't support sorting by cluster_id sortField: 'cluster_id', width: '300px', - render: (_value: string, row: Keyword) => row.cluster_name ? ( - + render: (_value: string, row: Keyword) => row.cluster_name && row.cluster_id ? ( + {row.cluster_name} - - ) : -, + + ) : '-', }, { ...difficultyColumn, diff --git a/frontend/src/config/pages/review.config.tsx b/frontend/src/config/pages/review.config.tsx index 27816f78..2d205630 100644 --- a/frontend/src/config/pages/review.config.tsx +++ b/frontend/src/config/pages/review.config.tsx @@ -3,6 +3,7 @@ * Centralized config for Review page table, filters, and actions */ +import { Link } from 'react-router-dom'; import { Content } from '../../services/api'; import Badge from '../../components/ui/badge/Badge'; import { formatRelativeDate } from '../../utils/date'; @@ -70,7 +71,7 @@ export function createReviewPageConfig(params: { const columns: ColumnConfig[] = [ { key: 'title', - label: 'Content Idea Title', + label: 'Content Title', sortable: true, sortField: 'title', width: '400px', @@ -79,12 +80,12 @@ export function createReviewPageConfig(params: { {params.onRowClick ? ( ) : ( - + {value || `Content #${row.id}`} )} @@ -175,13 +176,16 @@ export function createReviewPageConfig(params: { sortable: false, render: (_value: any, row: Content) => { const clusterName = row.cluster_name; - if (!clusterName) { - return -; + if (!clusterName || !row.cluster_id) { + return '-'; } return ( - + {clusterName} - + ); }, }, @@ -292,10 +296,10 @@ export function createReviewPageConfig(params: { tooltip: 'Live content on your website. Successfully published and accessible.', }, { - label: 'Total Images', + label: 'Images', accentColor: 'blue', 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.', }, ], }; diff --git a/frontend/src/config/pages/tasks.config.tsx b/frontend/src/config/pages/tasks.config.tsx index 728b76da..19df92e2 100644 --- a/frontend/src/config/pages/tasks.config.tsx +++ b/frontend/src/config/pages/tasks.config.tsx @@ -4,6 +4,7 @@ */ import React from 'react'; +import { Link } from 'react-router-dom'; import { titleColumn, statusColumn, @@ -106,7 +107,7 @@ export const createTasksPageConfig = ( columns: [ { ...titleColumn, - label: 'Content Idea Title', + label: 'Content Title', sortable: true, sortField: 'title', width: '400px', @@ -139,11 +140,14 @@ export const createTasksPageConfig = ( label: 'Cluster', sortable: false, // Backend doesn't support sorting by cluster_id sortField: 'cluster_id', - render: (_value: string, row: Task) => ( - - {row.cluster_name || '-'} - - ), + render: (_value: string, row: Task) => row.cluster_name && row.cluster_id ? ( + + {row.cluster_name} + + ) : '-', }, { key: 'taxonomy_name', @@ -466,11 +470,11 @@ export const createTasksPageConfig = ( tooltip: 'Live content on your website. Successfully published and accessible.', }, { - label: 'Total Images', + label: 'Images', value: 0, accentColor: 'blue' as const, calculate: (data) => 0, - tooltip: 'Total images generated across all content. Tracks visual asset coverage.', + tooltip: 'Generated images / Total images. Tracks visual asset coverage.',, }, ], }; diff --git a/frontend/src/context/HeaderMetricsContext.tsx b/frontend/src/context/HeaderMetricsContext.tsx index d3de4a83..d826c1e9 100644 --- a/frontend/src/context/HeaderMetricsContext.tsx +++ b/frontend/src/context/HeaderMetricsContext.tsx @@ -3,6 +3,7 @@ import { createContext, useContext, useState, ReactNode, useRef, useCallback } f interface HeaderMetric { label: string; value: string | number; + displayValue?: string; // Optional display value (e.g., "300/600" for generated/total) accentColor: 'blue' | 'green' | 'amber' | 'purple'; tooltip?: string; // Actionable insight for this metric } diff --git a/frontend/src/pages/Planner/Ideas.tsx b/frontend/src/pages/Planner/Ideas.tsx index ff137148..4160a7b9 100644 --- a/frontend/src/pages/Planner/Ideas.tsx +++ b/frontend/src/pages/Planner/Ideas.tsx @@ -47,8 +47,9 @@ export default function Ideas() { const [loading, setLoading] = useState(true); // Total counts for footer widget (not page-filtered) - const [totalInTasks, setTotalInTasks] = useState(0); - const [totalPending, setTotalPending] = useState(0); + const [totalNew, setTotalNew] = useState(0); + const [totalQueued, setTotalQueued] = useState(0); + const [totalCompleted, setTotalCompleted] = useState(0); const [totalImagesCount, setTotalImagesCount] = useState(0); // Actual total count (unfiltered) for header metrics - not affected by filters const [actualTotalIdeas, setActualTotalIdeas] = useState(0); @@ -182,8 +183,9 @@ export default function Ideas() { ]); setActualTotalIdeas(allRes.count || 0); // Store actual total (unfiltered) for header metrics - setTotalInTasks((queuedRes.count || 0) + (completedRes.count || 0)); - setTotalPending(newRes.count || 0); + setTotalNew(newRes.count || 0); + setTotalQueued(queuedRes.count || 0); + setTotalCompleted(completedRes.count || 0); setTotalImagesCount(imagesRes.count || 0); } catch (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]); - // 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 // Note: actualTotalIdeas is NOT affected by filters - it always shows the true total const headerMetrics = useMemo(() => { @@ -380,16 +382,16 @@ export default function Ideas() { value = actualTotalIdeas || 0; break; case 'New': - // Use totalPending from loadTotalMetrics() (ideas with status='new') - value = totalPending; + // Use totalNew from loadTotalMetrics() (ideas with status='new') + value = totalNew; break; case 'Queued': - // Use totalInTasks from loadTotalMetrics() (ideas with status='queued') - value = totalInTasks; + // Use totalQueued from loadTotalMetrics() (ideas with status='queued') + value = totalQueued; break; case 'Completed': - // Calculate completed from totalCount - (totalPending + totalInTasks) - value = Math.max(0, totalCount - totalPending - totalInTasks); + // Use totalCompleted from loadTotalMetrics() (ideas with status='completed') + value = totalCompleted; break; default: value = metric.calculate({ ideas, totalCount }); @@ -402,7 +404,7 @@ export default function Ideas() { tooltip: (metric as any).tooltip, }; }); - }, [pageConfig?.headerMetrics, ideas, totalCount, totalPending, totalInTasks, actualTotalIdeas]); + }, [pageConfig?.headerMetrics, ideas, totalCount, totalNew, totalQueued, totalCompleted, actualTotalIdeas]); const resetForm = useCallback(() => { setFormData({ @@ -565,21 +567,21 @@ export default function Ideas() { submoduleColor: 'amber', metrics: [ { label: 'Ideas', value: totalCount }, - { label: 'In Tasks', value: totalInTasks, percentage: `${totalCount > 0 ? Math.round((totalInTasks / totalCount) * 100) : 0}%` }, - { label: 'Pending', value: totalPending }, + { label: 'In Tasks', value: totalQueued + totalCompleted, percentage: `${totalCount > 0 ? Math.round(((totalQueued + totalCompleted) / totalCount) * 100) : 0}%` }, + { label: 'Pending', value: totalNew }, { label: 'From Clusters', value: clusters.length }, ], progress: { - value: totalCount > 0 ? Math.round((totalInTasks / totalCount) * 100) : 0, + value: totalCount > 0 ? Math.round(((totalQueued + totalCompleted) / totalCount) * 100) : 0, label: 'Converted', color: 'amber', }, - hint: totalPending > 0 - ? `${totalPending} ideas ready to become tasks` + hint: totalNew > 0 + ? `${totalNew} ideas ready to become tasks` : 'All ideas converted!', - statusInsight: totalPending > 0 + statusInsight: totalNew > 0 ? `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.` : `No ideas yet. Generate ideas from Clusters page.`, }} diff --git a/frontend/src/pages/Writer/Approved.tsx b/frontend/src/pages/Writer/Approved.tsx index 90a7d160..d53f2c98 100644 --- a/frontend/src/pages/Writer/Approved.tsx +++ b/frontend/src/pages/Writer/Approved.tsx @@ -52,6 +52,7 @@ export default function Approved() { const [totalApproved, setTotalApproved] = useState(0); const [totalPublished, setTotalPublished] = useState(0); const [totalImagesCount, setTotalImagesCount] = useState(0); + const [generatedImagesCount, setGeneratedImagesCount] = useState(0); // Dynamic filter options (loaded from backend) const [statusOptions, setStatusOptions] = useState | undefined>(undefined); @@ -144,13 +145,14 @@ export default function Approved() { const loadTotalMetrics = useCallback(async () => { try { // 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, status: 'draft', 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: 'published', 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); @@ -159,6 +161,7 @@ export default function Approved() { setTotalApproved(approvedRes.count || 0); setTotalPublished(publishedRes.count || 0); setTotalImagesCount(imagesRes.count || 0); + setGeneratedImagesCount(generatedImagesRes.count || 0); } catch (error) { console.error('Error loading total metrics:', error); } @@ -706,9 +709,15 @@ export default function Approved() { case 'Published': value = totalPublished; break; - case 'Total Images': + case 'Images': value = totalImagesCount; - break; + return { + label: metric.label, + displayValue: `${generatedImagesCount}/${totalImagesCount}`, + value, + accentColor: metric.accentColor, + tooltip: (metric as any).tooltip, + }; default: value = metric.calculate({ content, totalCount }); } @@ -720,7 +729,7 @@ export default function Approved() { 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 ( <> diff --git a/frontend/src/pages/Writer/Content.tsx b/frontend/src/pages/Writer/Content.tsx index 0be3e1c0..12338aee 100644 --- a/frontend/src/pages/Writer/Content.tsx +++ b/frontend/src/pages/Writer/Content.tsx @@ -46,6 +46,7 @@ export default function Content() { const [totalApproved, setTotalApproved] = useState(0); const [totalPublished, setTotalPublished] = useState(0); const [totalImagesCount, setTotalImagesCount] = useState(0); + const [generatedImagesCount, setGeneratedImagesCount] = useState(0); // Dynamic filter options (loaded from backend) const [sourceOptions, setSourceOptions] = useState | undefined>(undefined); @@ -122,7 +123,7 @@ export default function Content() { const loadTotalMetrics = useCallback(async () => { try { // 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) fetchContent({ page_size: 1, @@ -152,8 +153,10 @@ export default function Content() { site_id: activeSite?.id, status: 'published', }), - // Get actual total images count + // Get total images count 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); @@ -162,6 +165,7 @@ export default function Content() { setTotalApproved(approvedRes.count || 0); setTotalPublished(publishedRes.count || 0); setTotalImagesCount(imagesRes.count || 0); + setGeneratedImagesCount(generatedImagesRes.count || 0); } catch (error) { console.error('Error loading total metrics:', error); } @@ -324,9 +328,15 @@ export default function Content() { case 'Published': value = totalPublished; break; - case 'Total Images': + case 'Images': value = totalImagesCount; - break; + return { + label: metric.label, + displayValue: `${generatedImagesCount}/${totalImagesCount}`, + value, + accentColor: metric.accentColor, + tooltip: (metric as any).tooltip, + }; default: value = metric.calculate({ content, totalCount }); } @@ -338,7 +348,7 @@ export default function Content() { 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) => { if (action === 'view_on_wordpress') { diff --git a/frontend/src/pages/Writer/Images.tsx b/frontend/src/pages/Writer/Images.tsx index f83ce9ff..fe476b10 100644 --- a/frontend/src/pages/Writer/Images.tsx +++ b/frontend/src/pages/Writer/Images.tsx @@ -47,6 +47,7 @@ export default function Images() { const [totalApproved, setTotalApproved] = useState(0); const [totalPublished, setTotalPublished] = useState(0); const [totalImagesCount, setTotalImagesCount] = useState(0); + const [generatedImagesCount, setGeneratedImagesCount] = useState(0); // Footer widget specific counts (image-based) const [totalComplete, setTotalComplete] = useState(0); @@ -119,13 +120,14 @@ export default function Images() { const loadTotalMetrics = useCallback(async () => { try { // 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, status: 'draft', 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: 'published', 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); @@ -134,6 +136,7 @@ export default function Images() { setTotalApproved(approvedRes.count || 0); setTotalPublished(publishedRes.count || 0); setTotalImagesCount(imagesRes.count || 0); + setGeneratedImagesCount(generatedImagesRes.count || 0); } catch (error) { console.error('Error loading total metrics:', error); } @@ -553,7 +556,13 @@ export default function Images() { break; case 'Total Images': value = totalImagesCount; - break; + return { + label: metric.label, + displayValue: `${generatedImagesCount}/${totalImagesCount}`, + value, + accentColor: metric.accentColor, + tooltip: (metric as any).tooltip, + }; default: value = metric.calculate({ images, totalCount }); } @@ -567,7 +576,7 @@ export default function Images() { }); return baseMetrics; - }, [pageConfig?.headerMetrics, images, totalCount, totalContent, totalDraft, totalReview, totalApproved, totalPublished, totalImagesCount]); + }, [pageConfig?.headerMetrics, images, totalCount, totalContent, totalDraft, totalReview, totalApproved, totalPublished, totalImagesCount, generatedImagesCount]); return ( <> diff --git a/frontend/src/pages/Writer/Review.tsx b/frontend/src/pages/Writer/Review.tsx index 254751e6..39407662 100644 --- a/frontend/src/pages/Writer/Review.tsx +++ b/frontend/src/pages/Writer/Review.tsx @@ -7,7 +7,7 @@ import { useState, useEffect, useMemo, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import TablePageTemplate from '../../templates/TablePageTemplate'; - const { activeSector, sectors } = useSectorStore(); +import { fetchContent, fetchImages, fetchWriterContentFilterOptions, @@ -28,7 +28,7 @@ import PageHeader from '../../components/common/PageHeader'; export default function Review() { const toast = useToast(); const navigate = useNavigate(); - const { activeSector } = useSectorStore(); + const { activeSector, sectors } = useSectorStore(); const { activeSite } = useSiteStore(); // Data state @@ -42,6 +42,7 @@ export default function Review() { const [totalApproved, setTotalApproved] = useState(0); const [totalPublished, setTotalPublished] = useState(0); const [totalImagesCount, setTotalImagesCount] = useState(0); + const [generatedImagesCount, setGeneratedImagesCount] = useState(0); // Dynamic filter options (loaded from backend) const [siteStatusOptions, setSiteStatusOptions] = useState | undefined>(undefined); @@ -144,13 +145,14 @@ export default function Review() { const loadTotalMetrics = useCallback(async () => { try { // 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, status: 'draft', 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: 'published', 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); @@ -159,6 +161,7 @@ export default function Review() { setTotalApproved(approvedRes.count || 0); setTotalPublished(publishedRes.count || 0); setTotalImagesCount(imagesRes.count || 0); + setGeneratedImagesCount(generatedImagesRes.count || 0); } catch (error) { console.error('Error loading metrics:', error); } @@ -252,9 +255,14 @@ export default function Review() { case 'Published': value = totalPublished; break; - case 'Total Images': + case 'Images': value = totalImagesCount; - break; + return { + ...metric, + displayValue: `${generatedImagesCount}/${totalImagesCount}`, + value, + tooltip: (metric as any).tooltip, + }; default: value = metric.calculate({ content, totalCount }); } @@ -264,7 +272,7 @@ export default function Review() { 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 diff --git a/frontend/src/pages/Writer/Tasks.tsx b/frontend/src/pages/Writer/Tasks.tsx index 85fd5e01..2a70d592 100644 --- a/frontend/src/pages/Writer/Tasks.tsx +++ b/frontend/src/pages/Writer/Tasks.tsx @@ -55,6 +55,7 @@ export default function Tasks() { const [totalApproved, setTotalApproved] = useState(0); const [totalPublished, setTotalPublished] = useState(0); const [totalImagesCount, setTotalImagesCount] = useState(0); + const [generatedImagesCount, setGeneratedImagesCount] = useState(0); // Footer widget specific counts (task-based) const [totalQueued, setTotalQueued] = useState(0); @@ -163,7 +164,7 @@ export default function Tasks() { const loadTotalMetrics = useCallback(async () => { try { // 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) fetchContent({ page_size: 1, @@ -195,6 +196,8 @@ export default function Tasks() { }), // Get actual total images count 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); @@ -203,6 +206,7 @@ export default function Tasks() { setTotalApproved(approvedRes.count || 0); setTotalPublished(publishedRes.count || 0); setTotalImagesCount(imagesRes.count || 0); + setGeneratedImagesCount(generatedImagesRes.count || 0); } catch (error) { console.error('Error loading total metrics:', error); } @@ -454,9 +458,15 @@ export default function Tasks() { case 'Published': value = totalPublished; break; - case 'Total Images': + case 'Images': value = totalImagesCount; - break; + return { + label: metric.label, + displayValue: `${generatedImagesCount}/${totalImagesCount}`, + value, + accentColor: metric.accentColor, + tooltip: (metric as any).tooltip, + }; default: value = metric.calculate({ tasks, totalCount }); } @@ -468,7 +478,7 @@ export default function Tasks() { 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(() => { setFormData({