tbaels fitlers and plnaner writer other fixes
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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.',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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.',,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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.',,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.`,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user