header footer metrics update and credits by site fixes

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-12 05:28:36 +00:00
parent 95d8ade942
commit 368601f68c
12 changed files with 339 additions and 215 deletions

View File

@@ -296,24 +296,45 @@ export function createApprovedPageConfig(params: {
const headerMetrics: HeaderMetricConfig[] = [ const headerMetrics: HeaderMetricConfig[] = [
{ {
label: 'Approved', label: 'Content',
accentColor: 'green',
calculate: (data: { totalCount: number }) => data.totalCount,
tooltip: 'Total approved content ready for publishing.',
},
{
label: 'On Site',
accentColor: 'blue', accentColor: 'blue',
calculate: (data: { content: Content[] }) => calculate: (data: { totalCount: number }) => data.totalCount,
data.content.filter(c => c.external_id).length, tooltip: 'Total content items tracked. Overall volume across all stages.',
tooltip: 'Content published to your website.',
}, },
{ {
label: 'Pending', label: 'Draft',
accentColor: 'amber', accentColor: 'amber',
calculate: (data: { content: Content[] }) => calculate: (data: { content: Content[] }) =>
data.content.filter(c => !c.external_id).length, data.content.filter(c => c.status === 'draft').length,
tooltip: 'Approved content not yet published to site.', tooltip: 'Content written, images not generated. Generate images to move to review.',
},
{
label: 'In Review',
accentColor: 'purple',
calculate: (data: { content: Content[] }) =>
data.content.filter(c => c.status === 'review').length,
tooltip: 'Images generated, awaiting approval. Review and approve to publish.',
},
{
label: 'Approved',
accentColor: 'green',
calculate: (data: { content: Content[] }) =>
data.content.filter(c => c.status === 'approved').length,
tooltip: 'Approved content awaiting publishing. Publish to site when ready.',
},
{
label: 'Published',
accentColor: 'green',
calculate: (data: { content: Content[] }) =>
data.content.filter(c => c.status === 'published').length,
tooltip: 'Live content on your website. Successfully published and accessible.',
},
{
label: 'Total 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.',
}, },
]; ];

View File

@@ -456,28 +456,42 @@ export const createContentPageConfig = (
value: 0, value: 0,
accentColor: 'blue' as const, accentColor: 'blue' as const,
calculate: (data) => data.totalCount || 0, calculate: (data) => data.totalCount || 0,
tooltip: 'Total content pieces generated. Includes drafts, review, and published content.', tooltip: 'Total content items tracked. Overall volume across all stages.',
}, },
{ {
label: 'Draft', label: 'Draft',
value: 0, value: 0,
accentColor: 'amber' as const, accentColor: 'amber' as const,
calculate: (data) => data.content.filter((c: Content) => c.status === 'draft').length, calculate: (data) => data.content.filter((c: Content) => c.status === 'draft').length,
tooltip: 'Content in draft stage. Edit and refine before moving to review.', tooltip: 'Content written, images not generated. Generate images to move to review.',
}, },
{ {
label: 'In Review', label: 'In Review',
value: 0, value: 0,
accentColor: 'blue' as const, accentColor: 'purple' as const,
calculate: (data) => data.content.filter((c: Content) => c.status === 'review').length, calculate: (data) => data.content.filter((c: Content) => c.status === 'review').length,
tooltip: 'Content awaiting review and approval. Review for quality before publishing.', tooltip: 'Images generated, awaiting approval. Review and approve to publish.',
},
{
label: 'Approved',
value: 0,
accentColor: 'green' as const,
calculate: (data) => data.content.filter((c: Content) => c.status === 'approved').length,
tooltip: 'Approved content awaiting publishing. Publish to site when ready.',
}, },
{ {
label: 'Published', label: 'Published',
value: 0, value: 0,
accentColor: 'green' as const, accentColor: 'green' as const,
calculate: (data) => data.content.filter((c: Content) => c.status === 'published').length, calculate: (data) => data.content.filter((c: Content) => c.status === 'published').length,
tooltip: 'Published content ready for WordPress sync. Track your published library.', tooltip: 'Live content on your website. Successfully published and accessible.',
},
{
label: 'Total Images',
value: 0,
accentColor: 'blue' as const,
calculate: (data) => 0,
tooltip: 'Total images generated across all content. Tracks visual asset coverage.',
}, },
], ],
}; };

View File

@@ -207,28 +207,42 @@ export const createImagesPageConfig = (
value: 0, value: 0,
accentColor: 'blue' as const, accentColor: 'blue' as const,
calculate: (data) => data.totalCount || 0, calculate: (data) => data.totalCount || 0,
tooltip: 'Total content pieces with image generation. Track image coverage across all content.', tooltip: 'Total content items tracked. Overall volume across all stages.',
}, },
{ {
label: 'Complete', label: 'Draft',
value: 0,
accentColor: 'green' as const,
calculate: (data) => data.images.filter((i: ContentImagesGroup) => i.overall_status === 'complete').length,
tooltip: 'Content with all images generated. Ready for publishing with full visual coverage.',
},
{
label: 'Partial',
value: 0,
accentColor: 'blue' as const,
calculate: (data) => data.images.filter((i: ContentImagesGroup) => i.overall_status === 'partial').length,
tooltip: 'Content with some images missing. Generate remaining images to complete visual assets.',
},
{
label: 'Pending',
value: 0, value: 0,
accentColor: 'amber' as const, accentColor: 'amber' as const,
calculate: (data) => data.images.filter((i: ContentImagesGroup) => i.overall_status === 'pending').length, calculate: (data) => data.images.filter((i: ContentImagesGroup) => i.content_status === 'draft').length,
tooltip: 'Content waiting for image generation. Queue these to start creating visual assets.', tooltip: 'Content written, images not generated. Generate images to move to review.',
},
{
label: 'In Review',
value: 0,
accentColor: 'purple' as const,
calculate: (data) => data.images.filter((i: ContentImagesGroup) => i.content_status === 'review').length,
tooltip: 'Images generated, awaiting approval. Review and approve to publish.',
},
{
label: 'Approved',
value: 0,
accentColor: 'green' as const,
calculate: (data) => data.images.filter((i: ContentImagesGroup) => i.content_status === 'approved').length,
tooltip: 'Approved content awaiting publishing. Publish to site when ready.',
},
{
label: 'Published',
value: 0,
accentColor: 'green' as const,
calculate: (data) => data.images.filter((i: ContentImagesGroup) => i.content_status === 'published').length,
tooltip: 'Live content on your website. Successfully published and accessible.',
},
{
label: 'Total 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.',
}, },
], ],
maxInArticleImages: maxImages, maxInArticleImages: maxImages,

View File

@@ -259,28 +259,40 @@ export function createReviewPageConfig(params: {
], ],
headerMetrics: [ headerMetrics: [
{ {
label: 'Ready', label: 'Content',
accentColor: 'blue', accentColor: 'blue',
calculate: ({ totalCount }) => totalCount, calculate: ({ totalCount }) => totalCount,
tooltip: 'Content ready for final review. Review quality, SEO, and images before publishing.', tooltip: 'Total content items tracked. Overall volume across all stages.',
}, },
{ {
label: 'Images', label: 'Draft',
accentColor: 'green',
calculate: ({ content }) => content.filter(c => c.has_generated_images).length,
tooltip: 'Content with generated images. Visual assets complete and ready for review.',
},
{
label: 'Optimized',
accentColor: 'purple',
calculate: ({ content }) => content.filter(c => c.optimization_scores && c.optimization_scores.overall_score >= 80).length,
tooltip: 'Content with high SEO optimization scores (80%+). Well-optimized for search engines.',
},
{
label: 'Sync Ready',
accentColor: 'amber', accentColor: 'amber',
calculate: ({ content }) => content.filter(c => c.has_generated_images && c.optimization_scores && c.optimization_scores.overall_score >= 70).length, calculate: ({ content }) => content.filter(c => c.status === 'draft').length,
tooltip: 'Content ready for WordPress sync. Has images and good optimization score.', tooltip: 'Content written, images not generated. Generate images to move to review.',
},
{
label: 'In Review',
accentColor: 'purple',
calculate: ({ content }) => content.filter(c => c.status === 'review').length,
tooltip: 'Images generated, awaiting approval. Review and approve to publish.',
},
{
label: 'Approved',
accentColor: 'green',
calculate: ({ content }) => content.filter(c => c.status === 'approved').length,
tooltip: 'Approved content awaiting publishing. Publish to site when ready.',
},
{
label: 'Published',
accentColor: 'green',
calculate: ({ content }) => content.filter(c => c.status === 'published').length,
tooltip: 'Live content on your website. Successfully published and accessible.',
},
{
label: 'Total Images',
accentColor: 'blue',
calculate: ({ content }) => content.filter(c => c.has_generated_images).length,
tooltip: 'Total images generated across all content. Tracks visual asset coverage.',
}, },
], ],
}; };

View File

@@ -454,39 +454,46 @@ export const createTasksPageConfig = (
], ],
headerMetrics: [ headerMetrics: [
{ {
label: 'Tasks', label: 'Content',
value: 0, value: 0,
accentColor: 'blue' as const, accentColor: 'blue' as const,
calculate: (data) => data.totalCount || 0, calculate: (data) => data.totalCount || 0,
tooltip: 'Total content generation tasks. Tasks process ideas into written content automatically.', tooltip: 'Total content items tracked. Overall volume across all stages.',
}, },
{ {
label: 'In Queue', label: 'Draft',
value: 0, value: 0,
accentColor: 'amber' as const, accentColor: 'amber' as const,
calculate: (data) => data.tasks.filter((t: Task) => t.status === 'queued').length, calculate: (data) => data.tasks.filter((t: Task) => t.status === 'draft').length,
tooltip: 'Tasks queued for processing. These will be picked up by the content generation system.', tooltip: 'Content written, images not generated. Generate images to move to review.',
}, },
{ {
label: 'Processing', label: 'In Review',
value: 0, value: 0,
accentColor: 'blue' as const, accentColor: 'purple' as const,
calculate: (data) => data.tasks.filter((t: Task) => t.status === 'in_progress').length, calculate: (data) => data.tasks.filter((t: Task) => t.status === 'review').length,
tooltip: 'Tasks currently being processed. Content is being generated by AI right now.', tooltip: 'Images generated, awaiting approval. Review and approve to publish.',
}, },
{ {
label: 'Completed', label: 'Approved',
value: 0, value: 0,
accentColor: 'green' as const, accentColor: 'green' as const,
calculate: (data) => data.tasks.filter((t: Task) => t.status === 'completed').length, calculate: (data) => data.tasks.filter((t: Task) => t.status === 'approved').length,
tooltip: 'Successfully completed tasks. Generated content is ready for review and publishing.', tooltip: 'Approved content awaiting publishing. Publish to site when ready.',
}, },
{ {
label: 'Failed', label: 'Published',
value: 0, value: 0,
accentColor: 'red' as const, accentColor: 'green' as const,
calculate: (data) => data.tasks.filter((t: Task) => t.status === 'failed').length, calculate: (data) => data.tasks.filter((t: Task) => t.status === 'published').length,
tooltip: 'Failed tasks that need attention. Review error logs and retry or modify the task.', tooltip: 'Live content on your website. Successfully published and accessible.',
},
{
label: 'Total Images',
value: 0,
accentColor: 'blue' as const,
calculate: (data) => 0,
tooltip: 'Total images generated across all content. Tracks visual asset coverage.',
}, },
], ],
}; };

View File

@@ -165,7 +165,7 @@ export function useModuleStats() {
// Total images // Total images
fetchImages({ ...baseFilters }), fetchImages({ ...baseFilters }),
// Credits usage from billing summary // Credits usage from billing summary
fetchAPI('/v1/billing/credits/usage/summary/').catch(() => ({ fetchAPI(`/v1/billing/credits/usage/summary/?site_id=${activeSite.id}${activeSector?.id ? `&sector_id=${activeSector.id}` : ''}`).catch(() => ({
data: { by_operation: {} } data: { by_operation: {} }
})), })),
]); ]);

View File

@@ -22,6 +22,7 @@ import { FileIcon, CheckCircleIcon, BoltIcon } from '../../icons';
import { RocketLaunchIcon } from '@heroicons/react/24/outline'; import { RocketLaunchIcon } from '@heroicons/react/24/outline';
import { createApprovedPageConfig } from '../../config/pages/approved.config'; import { createApprovedPageConfig } from '../../config/pages/approved.config';
import { useSectorStore } from '../../store/sectorStore'; import { useSectorStore } from '../../store/sectorStore';
import { useSiteStore } from '../../store/siteStore';
import { usePageSizeStore } from '../../store/pageSizeStore'; import { usePageSizeStore } from '../../store/pageSizeStore';
import PageHeader from '../../components/common/PageHeader'; import PageHeader from '../../components/common/PageHeader';
import StandardThreeWidgetFooter from '../../components/dashboard/StandardThreeWidgetFooter'; import StandardThreeWidgetFooter from '../../components/dashboard/StandardThreeWidgetFooter';
@@ -30,6 +31,7 @@ export default function Approved() {
const toast = useToast(); const toast = useToast();
const navigate = useNavigate(); const navigate = useNavigate();
const { activeSector } = useSectorStore(); const { activeSector } = useSectorStore();
const { activeSite } = useSiteStore();
const { pageSize } = usePageSizeStore(); const { pageSize } = usePageSizeStore();
// Data state // Data state
@@ -37,8 +39,11 @@ export default function Approved() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
// Total counts for footer widget and header metrics (not page-filtered) // Total counts for footer widget and header metrics (not page-filtered)
const [totalOnSite, setTotalOnSite] = useState(0); const [totalContent, setTotalContent] = useState(0);
const [totalPendingPublish, setTotalPendingPublish] = useState(0); const [totalDraft, setTotalDraft] = useState(0);
const [totalReview, setTotalReview] = useState(0);
const [totalApproved, setTotalApproved] = useState(0);
const [totalPublished, setTotalPublished] = useState(0);
const [totalImagesCount, setTotalImagesCount] = useState(0); const [totalImagesCount, setTotalImagesCount] = useState(0);
// Filter state - default to approved status // Filter state - default to approved status
@@ -59,27 +64,26 @@ export default function Approved() {
// Load total metrics for footer widget and header metrics (not affected by pagination) // Load total metrics for footer widget and header metrics (not affected by pagination)
const loadTotalMetrics = useCallback(async () => { const loadTotalMetrics = useCallback(async () => {
try { try {
// Fetch all approved+published content to calculate totals // Fetch counts in parallel for performance
const data = await fetchContent({ const [allRes, draftRes, reviewRes, approvedRes, publishedRes, imagesRes] = await Promise.all([
status__in: 'approved,published', // Both approved and published content fetchContent({ page_size: 1, site_id: activeSite?.id }),
page_size: 1000, // Fetch enough to count 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 }),
const allContent = data.results || []; fetchContent({ page_size: 1, status: 'published', site_id: activeSite?.id }),
// Count by external_id presence fetchImages({ page_size: 1, site_id: activeSite?.id }),
const onSite = allContent.filter(c => c.external_id).length; ]);
const pending = allContent.filter(c => !c.external_id).length;
setTotalContent(allRes.count || 0);
setTotalOnSite(onSite); setTotalDraft(draftRes.count || 0);
setTotalPendingPublish(pending); setTotalReview(reviewRes.count || 0);
setTotalApproved(approvedRes.count || 0);
// Get actual total images count setTotalPublished(publishedRes.count || 0);
const imagesRes = await fetchImages({ page_size: 1 });
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);
} }
}, []); }, [activeSite]);
// Load total metrics on mount // Load total metrics on mount
useEffect(() => { useEffect(() => {
@@ -342,16 +346,23 @@ export default function Approved() {
let value: number; let value: number;
switch (metric.label) { switch (metric.label) {
case 'Content':
value = totalContent || 0;
break;
case 'Draft':
value = totalDraft;
break;
case 'In Review':
value = totalReview;
break;
case 'Approved': case 'Approved':
value = totalCount || 0; value = totalApproved;
break; break;
case 'On Site': case 'Published':
// Use totalOnSite from loadTotalMetrics() value = totalPublished;
value = totalOnSite;
break; break;
case 'Pending': case 'Total Images':
// Use totalPendingPublish from loadTotalMetrics() value = totalImagesCount;
value = totalPendingPublish;
break; break;
default: default:
value = metric.calculate({ content, totalCount }); value = metric.calculate({ content, totalCount });
@@ -361,9 +372,10 @@ export default function Approved() {
label: metric.label, label: metric.label,
value, value,
accentColor: metric.accentColor, accentColor: metric.accentColor,
tooltip: (metric as any).tooltip,
}; };
}); });
}, [pageConfig?.headerMetrics, content, totalCount, totalOnSite, totalPendingPublish]); }, [pageConfig?.headerMetrics, content, totalCount, totalContent, totalDraft, totalReview, totalApproved, totalPublished, totalImagesCount]);
return ( return (
<> <>

View File

@@ -39,8 +39,10 @@ export default function Content() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
// Total counts for footer widget and header metrics (not page-filtered) // Total counts for footer widget and header metrics (not page-filtered)
const [totalContent, setTotalContent] = useState(0);
const [totalDraft, setTotalDraft] = useState(0); const [totalDraft, setTotalDraft] = useState(0);
const [totalReview, setTotalReview] = useState(0); const [totalReview, setTotalReview] = 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);
@@ -68,7 +70,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, publishedRes, imagesRes] = await Promise.all([ const [allRes, draftRes, reviewRes, approvedRes, publishedRes, imagesRes] = await Promise.all([
// Get all content (site-wide) // Get all content (site-wide)
fetchContent({ fetchContent({
page_size: 1, page_size: 1,
@@ -86,19 +88,26 @@ export default function Content() {
site_id: activeSite?.id, site_id: activeSite?.id,
status: 'review', status: 'review',
}), }),
// Get content with status='approved' or 'published' (ready for publishing or on site) // Get content with status='approved'
fetchContent({ fetchContent({
page_size: 1, page_size: 1,
site_id: activeSite?.id, site_id: activeSite?.id,
status__in: 'approved,published', status: 'approved',
}),
// Get content with status='published'
fetchContent({
page_size: 1,
site_id: activeSite?.id,
status: 'published',
}), }),
// Get actual total images count // Get actual total images count
fetchImages({ page_size: 1 }), fetchImages({ page_size: 1, site_id: activeSite?.id }),
]); ]);
setTotalCount(allRes.count || 0); setTotalContent(allRes.count || 0);
setTotalDraft(draftRes.count || 0); setTotalDraft(draftRes.count || 0);
setTotalReview(reviewRes.count || 0); setTotalReview(reviewRes.count || 0);
setTotalApproved(approvedRes.count || 0);
setTotalPublished(publishedRes.count || 0); setTotalPublished(publishedRes.count || 0);
setTotalImagesCount(imagesRes.count || 0); setTotalImagesCount(imagesRes.count || 0);
} catch (error) { } catch (error) {
@@ -226,20 +235,23 @@ export default function Content() {
switch (metric.label) { switch (metric.label) {
case 'Content': case 'Content':
value = totalCount || 0; value = totalContent || 0;
break; break;
case 'Draft': case 'Draft':
// Use totalDraft from loadTotalMetrics()
value = totalDraft; value = totalDraft;
break; break;
case 'In Review': case 'In Review':
// Use totalReview from loadTotalMetrics()
value = totalReview; value = totalReview;
break; break;
case 'Approved':
value = totalApproved;
break;
case 'Published': case 'Published':
// Use totalPublished from loadTotalMetrics()
value = totalPublished; value = totalPublished;
break; break;
case 'Total Images':
value = totalImagesCount;
break;
default: default:
value = metric.calculate({ content, totalCount }); value = metric.calculate({ content, totalCount });
} }
@@ -251,7 +263,7 @@ export default function Content() {
tooltip: (metric as any).tooltip, tooltip: (metric as any).tooltip,
}; };
}); });
}, [pageConfig?.headerMetrics, content, totalCount, totalDraft, totalReview, totalPublished]); }, [pageConfig?.headerMetrics, content, totalContent, totalDraft, totalReview, totalApproved, totalPublished, totalImagesCount]);
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

@@ -9,6 +9,7 @@ import TablePageTemplate from '../../templates/TablePageTemplate';
import { import {
fetchContentImages, fetchContentImages,
fetchImages, fetchImages,
fetchContent,
ContentImagesGroup, ContentImagesGroup,
ContentImagesResponse, ContentImagesResponse,
fetchImageGenerationSettings, fetchImageGenerationSettings,
@@ -28,19 +29,28 @@ import SingleRecordStatusUpdateModal from '../../components/common/SingleRecordS
import PageHeader from '../../components/common/PageHeader'; import PageHeader from '../../components/common/PageHeader';
import { Modal } from '../../components/ui/modal'; import { Modal } from '../../components/ui/modal';
import StandardThreeWidgetFooter from '../../components/dashboard/StandardThreeWidgetFooter'; import StandardThreeWidgetFooter from '../../components/dashboard/StandardThreeWidgetFooter';
import { useSiteStore } from '../../store/siteStore';
export default function Images() { export default function Images() {
const toast = useToast(); const toast = useToast();
const { activeSite } = useSiteStore();
// Data state // Data state
const [images, setImages] = useState<ContentImagesGroup[]>([]); const [images, setImages] = useState<ContentImagesGroup[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
// Total counts for footer widget and header metrics (not page-filtered) // Total counts for footer widget and header metrics (not page-filtered)
const [totalContent, setTotalContent] = useState(0);
const [totalDraft, setTotalDraft] = useState(0);
const [totalReview, setTotalReview] = useState(0);
const [totalApproved, setTotalApproved] = useState(0);
const [totalPublished, setTotalPublished] = useState(0);
const [totalImagesCount, setTotalImagesCount] = useState(0);
// Footer widget specific counts (image-based)
const [totalComplete, setTotalComplete] = useState(0); const [totalComplete, setTotalComplete] = useState(0);
const [totalPartial, setTotalPartial] = useState(0); const [totalPartial, setTotalPartial] = useState(0);
const [totalPending, setTotalPending] = useState(0); const [totalPending, setTotalPending] = useState(0);
const [totalImagesCount, setTotalImagesCount] = useState(0); // Actual images count
// Filter state // Filter state
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
@@ -79,40 +89,26 @@ export default function Images() {
// Load total metrics for footer widget and header metrics (not affected by pagination) // Load total metrics for footer widget and header metrics (not affected by pagination)
const loadTotalMetrics = useCallback(async () => { const loadTotalMetrics = useCallback(async () => {
try { try {
// Fetch content-grouped images for status counts // Fetch counts in parallel for performance
const data: ContentImagesResponse = await fetchContentImages({}); const [allRes, draftRes, reviewRes, approvedRes, publishedRes, imagesRes] = await Promise.all([
const allImages = data.results || []; fetchContent({ page_size: 1, site_id: activeSite?.id }),
fetchContent({ page_size: 1, status: 'draft', site_id: activeSite?.id }),
// Count by overall_status (content-level status) fetchContent({ page_size: 1, status: 'review', site_id: activeSite?.id }),
let complete = 0; fetchContent({ page_size: 1, status: 'approved', site_id: activeSite?.id }),
let partial = 0; fetchContent({ page_size: 1, status: 'published', site_id: activeSite?.id }),
let pending = 0; fetchImages({ page_size: 1, site_id: activeSite?.id }),
]);
allImages.forEach(img => {
switch (img.overall_status) { setTotalContent(allRes.count || 0);
case 'complete': setTotalDraft(draftRes.count || 0);
complete++; setTotalReview(reviewRes.count || 0);
break; setTotalApproved(approvedRes.count || 0);
case 'partial': setTotalPublished(publishedRes.count || 0);
partial++; setTotalImagesCount(imagesRes.count || 0);
break;
case 'pending':
pending++;
break;
}
});
setTotalComplete(complete);
setTotalPartial(partial);
setTotalPending(pending);
// Fetch ACTUAL total images count from the images endpoint
const imagesData = await fetchImages({ page_size: 1 });
setTotalImagesCount(imagesData.count || 0);
} catch (error) { } catch (error) {
console.error('Error loading total metrics:', error); console.error('Error loading total metrics:', error);
} }
}, []); }, [activeSite]);
// Load total metrics on mount // Load total metrics on mount
useEffect(() => { useEffect(() => {
@@ -502,19 +498,22 @@ export default function Images() {
switch (metric.label) { switch (metric.label) {
case 'Content': case 'Content':
value = totalCount || 0; value = totalContent || 0;
break; break;
case 'Complete': case 'Draft':
// Use totalComplete from loadTotalMetrics() value = totalDraft;
value = totalComplete;
break; break;
case 'Partial': case 'In Review':
// Use totalPartial from loadTotalMetrics() value = totalReview;
value = totalPartial;
break; break;
case 'Pending': case 'Approved':
// Use totalPending from loadTotalMetrics() value = totalApproved;
value = totalPending; break;
case 'Published':
value = totalPublished;
break;
case 'Total Images':
value = totalImagesCount;
break; break;
default: default:
value = metric.calculate({ images, totalCount }); value = metric.calculate({ images, totalCount });
@@ -528,16 +527,8 @@ export default function Images() {
}; };
}); });
// Add total images count metric
baseMetrics.push({
label: 'Total Images',
value: totalImagesCount,
accentColor: 'purple' as const,
tooltip: 'Total number of images across all content',
});
return baseMetrics; return baseMetrics;
}, [pageConfig?.headerMetrics, images, totalCount, totalComplete, totalPartial, totalPending, totalImagesCount]); }, [pageConfig?.headerMetrics, images, totalCount, totalContent, totalDraft, totalReview, totalApproved, totalPublished, totalImagesCount]);
return ( return (
<> <>

View File

@@ -19,6 +19,7 @@ import { CheckCircleIcon } from '../../icons';
import { ClipboardDocumentCheckIcon } from '@heroicons/react/24/outline'; import { ClipboardDocumentCheckIcon } from '@heroicons/react/24/outline';
import { createReviewPageConfig } from '../../config/pages/review.config'; import { createReviewPageConfig } from '../../config/pages/review.config';
import { useSectorStore } from '../../store/sectorStore'; import { useSectorStore } from '../../store/sectorStore';
import { useSiteStore } from '../../store/siteStore';
import { usePageSizeStore } from '../../store/pageSizeStore'; import { usePageSizeStore } from '../../store/pageSizeStore';
import PageHeader from '../../components/common/PageHeader'; import PageHeader from '../../components/common/PageHeader';
import StandardThreeWidgetFooter from '../../components/dashboard/StandardThreeWidgetFooter'; import StandardThreeWidgetFooter from '../../components/dashboard/StandardThreeWidgetFooter';
@@ -27,17 +28,20 @@ export default function Review() {
const toast = useToast(); const toast = useToast();
const navigate = useNavigate(); const navigate = useNavigate();
const { activeSector } = useSectorStore(); const { activeSector } = useSectorStore();
const { activeSite } = useSiteStore();
const { pageSize } = usePageSizeStore(); const { pageSize } = usePageSizeStore();
// Data state // Data state
const [content, setContent] = useState<Content[]>([]); const [content, setContent] = useState<Content[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [totalImagesCount, setTotalImagesCount] = useState(0);
// Total metrics for footer widget (not page-filtered) // Total metrics for footer widget and header metrics (not page-filtered)
const [totalDrafts, setTotalDrafts] = useState(0); const [totalContent, setTotalContent] = useState(0);
const [totalDraft, setTotalDraft] = useState(0);
const [totalReview, setTotalReview] = useState(0);
const [totalApproved, setTotalApproved] = useState(0); const [totalApproved, setTotalApproved] = useState(0);
const [totalTasks, setTotalTasks] = useState(0); const [totalPublished, setTotalPublished] = useState(0);
const [totalImagesCount, setTotalImagesCount] = useState(0);
// Filter state - default to review status // Filter state - default to review status
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
@@ -94,21 +98,25 @@ 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 [imagesRes, draftsRes, approvedRes, tasksRes] = await Promise.all([ const [allRes, draftRes, reviewRes, approvedRes, publishedRes, imagesRes] = await Promise.all([
fetchImages({ page_size: 1 }), fetchContent({ page_size: 1, site_id: activeSite?.id }),
fetchContent({ page_size: 1, status: 'draft' }), fetchContent({ page_size: 1, status: 'draft', site_id: activeSite?.id }),
fetchContent({ page_size: 1, status: 'approved' }), fetchContent({ page_size: 1, status: 'review', site_id: activeSite?.id }),
fetchAPI<{ count: number }>('/writer/tasks/?page_size=1'), 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 }),
]); ]);
setTotalImagesCount(imagesRes.count || 0); setTotalContent(allRes.count || 0);
setTotalDrafts(draftsRes.count || 0); setTotalDraft(draftRes.count || 0);
setTotalReview(reviewRes.count || 0);
setTotalApproved(approvedRes.count || 0); setTotalApproved(approvedRes.count || 0);
setTotalTasks(tasksRes.count || 0); setTotalPublished(publishedRes.count || 0);
setTotalImagesCount(imagesRes.count || 0);
} catch (error) { } catch (error) {
console.error('Error loading metrics:', error); console.error('Error loading metrics:', error);
} }
}, []); }, [activeSite]);
useEffect(() => { useEffect(() => {
loadTotalMetrics(); loadTotalMetrics();
@@ -161,12 +169,37 @@ export default function Review() {
// Header metrics (calculated from loaded data) // Header metrics (calculated from loaded data)
const headerMetrics = useMemo(() => const headerMetrics = useMemo(() =>
pageConfig.headerMetrics.map(metric => ({ pageConfig.headerMetrics.map(metric => {
...metric, let value: number;
value: metric.calculate({ content, totalCount }), switch (metric.label) {
tooltip: (metric as any).tooltip, case 'Content':
})), value = totalContent || 0;
[pageConfig.headerMetrics, content, totalCount] break;
case 'Draft':
value = totalDraft;
break;
case 'In Review':
value = totalReview;
break;
case 'Approved':
value = totalApproved;
break;
case 'Published':
value = totalPublished;
break;
case 'Total Images':
value = totalImagesCount;
break;
default:
value = metric.calculate({ content, totalCount });
}
return {
...metric,
value,
tooltip: (metric as any).tooltip,
};
}),
[pageConfig.headerMetrics, content, totalCount, totalContent, totalDraft, totalReview, totalApproved, totalPublished, totalImagesCount]
); );
// Export handler // Export handler

View File

@@ -9,6 +9,7 @@ import TablePageTemplate from '../../templates/TablePageTemplate';
import { import {
fetchTasks, fetchTasks,
fetchImages, fetchImages,
fetchContent,
createTask, createTask,
updateTask, updateTask,
deleteTask, deleteTask,
@@ -47,11 +48,17 @@ export default function Tasks() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
// Total counts for footer widget and header metrics (not page-filtered) // Total counts for footer widget and header metrics (not page-filtered)
const [totalContent, setTotalContent] = useState(0);
const [totalDraft, setTotalDraft] = useState(0);
const [totalReview, setTotalReview] = useState(0);
const [totalApproved, setTotalApproved] = useState(0);
const [totalPublished, setTotalPublished] = useState(0);
const [totalImagesCount, setTotalImagesCount] = useState(0);
// Footer widget specific counts (task-based)
const [totalQueued, setTotalQueued] = useState(0); const [totalQueued, setTotalQueued] = useState(0);
const [totalProcessing, setTotalProcessing] = useState(0); const [totalProcessing, setTotalProcessing] = useState(0);
const [totalCompleted, setTotalCompleted] = useState(0); const [totalCompleted, setTotalCompleted] = useState(0);
const [totalFailed, setTotalFailed] = useState(0);
const [totalImagesCount, setTotalImagesCount] = useState(0);
// Filter state // Filter state
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
@@ -111,45 +118,45 @@ 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, queuedRes, processingRes, completedRes, failedRes, imagesRes] = await Promise.all([ const [allRes, draftRes, reviewRes, approvedRes, publishedRes, imagesRes] = await Promise.all([
// Get all tasks (site-wide) // Get all content (site-wide)
fetchTasks({ fetchContent({
page_size: 1, page_size: 1,
site_id: activeSite?.id, site_id: activeSite?.id,
}), }),
// Get tasks with status='queued' // Get content with status='draft'
fetchTasks({ fetchContent({
page_size: 1, page_size: 1,
site_id: activeSite?.id, site_id: activeSite?.id,
status: 'queued', status: 'draft',
}), }),
// Get tasks with status='in_progress' // Get content with status='review'
fetchTasks({ fetchContent({
page_size: 1, page_size: 1,
site_id: activeSite?.id, site_id: activeSite?.id,
status: 'in_progress', status: 'review',
}), }),
// Get tasks with status='completed' // Get content with status='approved'
fetchTasks({ fetchContent({
page_size: 1, page_size: 1,
site_id: activeSite?.id, site_id: activeSite?.id,
status: 'completed', status: 'approved',
}), }),
// Get tasks with status='failed' // Get content with status='published'
fetchTasks({ fetchContent({
page_size: 1, page_size: 1,
site_id: activeSite?.id, site_id: activeSite?.id,
status: 'failed', status: 'published',
}), }),
// Get actual total images count // Get actual total images count
fetchImages({ page_size: 1 }), fetchImages({ page_size: 1, site_id: activeSite?.id }),
]); ]);
setTotalCount(allRes.count || 0); setTotalContent(allRes.count || 0);
setTotalQueued(queuedRes.count || 0); setTotalDraft(draftRes.count || 0);
setTotalProcessing(processingRes.count || 0); setTotalReview(reviewRes.count || 0);
setTotalCompleted(completedRes.count || 0); setTotalApproved(approvedRes.count || 0);
setTotalFailed(failedRes.count || 0); setTotalPublished(publishedRes.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);
@@ -384,24 +391,23 @@ export default function Tasks() {
let value: number; let value: number;
switch (metric.label) { switch (metric.label) {
case 'Tasks': case 'Content':
value = totalCount || 0; value = totalContent || 0;
break; break;
case 'In Queue': case 'Draft':
// Use totalQueued from loadTotalMetrics() value = totalDraft;
value = totalQueued;
break; break;
case 'Processing': case 'In Review':
// Use totalProcessing from loadTotalMetrics() value = totalReview;
value = totalProcessing;
break; break;
case 'Completed': case 'Approved':
// Use totalCompleted from loadTotalMetrics() value = totalApproved;
value = totalCompleted;
break; break;
case 'Failed': case 'Published':
// Use totalFailed from loadTotalMetrics() value = totalPublished;
value = totalFailed; break;
case 'Total Images':
value = totalImagesCount;
break; break;
default: default:
value = metric.calculate({ tasks, totalCount }); value = metric.calculate({ tasks, totalCount });
@@ -414,7 +420,7 @@ export default function Tasks() {
tooltip: (metric as any).tooltip, tooltip: (metric as any).tooltip,
}; };
}); });
}, [pageConfig?.headerMetrics, tasks, totalCount, totalQueued, totalProcessing, totalCompleted, totalFailed]); }, [pageConfig?.headerMetrics, tasks, totalContent, totalDraft, totalReview, totalApproved, totalPublished, totalImagesCount]);
const resetForm = useCallback(() => { const resetForm = useCallback(() => {
setFormData({ setFormData({

View File

@@ -1425,6 +1425,7 @@ export interface ImageListResponse {
export interface ImageFilters { export interface ImageFilters {
content_id?: number; content_id?: number;
task_id?: number; task_id?: number;
site_id?: number;
image_type?: string; image_type?: string;
status?: string; status?: string;
ordering?: string; ordering?: string;
@@ -1502,6 +1503,7 @@ export async function fetchImages(filters: ImageFilters = {}): Promise<ImageList
const params = new URLSearchParams(); const params = new URLSearchParams();
if (filters.content_id) params.append('content_id', filters.content_id.toString()); if (filters.content_id) params.append('content_id', filters.content_id.toString());
if (filters.task_id) params.append('task_id', filters.task_id.toString()); if (filters.task_id) params.append('task_id', filters.task_id.toString());
if (filters.site_id) params.append('site_id', filters.site_id.toString());
if (filters.image_type) params.append('image_type', filters.image_type); if (filters.image_type) params.append('image_type', filters.image_type);
if (filters.status) params.append('status', filters.status); if (filters.status) params.append('status', filters.status);
if (filters.ordering) params.append('ordering', filters.ordering); if (filters.ordering) params.append('ordering', filters.ordering);