metricsa dn backedn fixes

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-29 04:33:22 +00:00
parent 53fdebf733
commit 0ffd21b9bf
17 changed files with 929 additions and 266 deletions

View File

@@ -12,7 +12,7 @@ import {
fetchContentIdeas,
fetchTasks,
fetchContent,
fetchContentImages,
fetchImages,
} from '../../services/api';
import ActivityLog from '../../components/Automation/ActivityLog';
import ConfigModal from '../../components/Automation/ConfigModal';
@@ -130,8 +130,8 @@ const AutomationPage: React.FC = () => {
fetchContent({ page_size: 1, site_id: siteId, status: 'draft' }),
fetchContent({ page_size: 1, site_id: siteId, status: 'review' }),
fetchContent({ page_size: 1, site_id: siteId, status: 'published' }),
fetchContentImages({ page_size: 1, site_id: siteId }),
fetchContentImages({ page_size: 1, site_id: siteId, status: 'pending' }),
fetchImages({ page_size: 1 }),
fetchImages({ page_size: 1, status: 'pending' }),
]);
setMetrics({
@@ -249,8 +249,8 @@ const AutomationPage: React.FC = () => {
fetchContent({ page_size: 1, site_id: siteId, status: 'draft' }),
fetchContent({ page_size: 1, site_id: siteId, status: 'review' }),
fetchContent({ page_size: 1, site_id: siteId, status: 'published' }),
fetchContentImages({ page_size: 1, site_id: siteId }),
fetchContentImages({ page_size: 1, site_id: siteId, status: 'pending' }),
fetchImages({ page_size: 1 }),
fetchImages({ page_size: 1, status: 'pending' }),
]);
setMetrics({

View File

@@ -11,15 +11,10 @@ import { useOnboardingStore } from "../../store/onboardingStore";
import { useBillingStore } from "../../store/billingStore";
import { GridIcon, PlusIcon } from "../../icons";
import {
fetchKeywords,
fetchClusters,
fetchContentIdeas,
fetchTasks,
fetchContent,
fetchContentImages,
fetchSites,
Site,
} from "../../services/api";
import { getDashboardStats } from "../../services/billing.api";
import { useSiteStore } from "../../store/siteStore";
import { useSectorStore } from "../../store/sectorStore";
import { useToast } from "../../components/ui/toast/ToastContainer";
@@ -155,49 +150,33 @@ export default function Home() {
const fetchDashboardData = useCallback(async () => {
try {
setLoading(true);
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
const siteId = siteFilter === 'all' ? undefined : siteFilter;
// Fetch pipeline counts sequentially to avoid rate limiting
const keywordsRes = await fetchKeywords({ page_size: 1, site_id: siteId });
await delay(100);
const clustersRes = await fetchClusters({ page_size: 1, site_id: siteId });
await delay(100);
const ideasRes = await fetchContentIdeas({ page_size: 1, site_id: siteId });
await delay(100);
const tasksRes = await fetchTasks({ page_size: 1, site_id: siteId });
await delay(100);
const contentRes = await fetchContent({ page_size: 1, site_id: siteId });
await delay(100);
const imagesRes = await fetchContentImages({ page_size: 1, site_id: siteId });
// Fetch real dashboard stats from API
const stats = await getDashboardStats({
site_id: siteId,
days: 7
});
const totalKeywords = keywordsRes.count || 0;
const totalClusters = clustersRes.count || 0;
const totalIdeas = ideasRes.count || 0;
const totalTasks = tasksRes.count || 0;
const totalContent = contentRes.count || 0;
const totalImages = imagesRes.count || 0;
const publishedContent = Math.floor(totalContent * 0.6); // Placeholder
// Calculate completion percentage
const completionPercentage = totalKeywords > 0
? Math.round((publishedContent / totalKeywords) * 100)
// Update pipeline data from real API data
const { pipeline, counts } = stats;
const completionPercentage = pipeline.keywords > 0
? Math.round((pipeline.published / pipeline.keywords) * 100)
: 0;
// Update pipeline data
setPipelineData({
sites: sites.length,
keywords: totalKeywords,
clusters: totalClusters,
ideas: totalIdeas,
tasks: totalTasks,
drafts: totalContent,
published: publishedContent,
sites: pipeline.sites,
keywords: pipeline.keywords,
clusters: pipeline.clusters,
ideas: pipeline.ideas,
tasks: pipeline.tasks,
drafts: pipeline.drafts,
published: pipeline.published,
completionPercentage: Math.min(completionPercentage, 100),
});
// Generate attention items based on data
// Generate attention items based on real data
const attentionList: AttentionItem[] = [];
// Check for sites without sectors
@@ -213,130 +192,85 @@ export default function Home() {
});
}
// Check for content needing images
const contentWithoutImages = totalContent - Math.floor(totalContent * 0.7);
if (contentWithoutImages > 0 && totalContent > 0) {
// Check for content needing images (content in review without all images generated)
const contentWithPendingImages = counts.images.pending;
if (contentWithPendingImages > 0) {
attentionList.push({
id: 'needs_images',
type: 'pending_review',
title: 'articles need images',
count: contentWithoutImages,
title: 'images pending',
count: contentWithPendingImages,
description: 'Generate images before publishing',
actionLabel: 'Generate Images',
actionHref: '/writer/images',
});
}
// Check for content in review
if (counts.content.review > 0) {
attentionList.push({
id: 'pending_review',
type: 'pending_review',
title: 'articles ready for review',
count: counts.content.review,
description: 'Review and publish content',
actionLabel: 'Review Content',
actionHref: '/writer/content?status=review',
});
}
setAttentionItems(attentionList);
// Update content velocity (using mock calculations based on totals)
const weeklyArticles = Math.floor(totalContent * 0.15);
const monthlyArticles = Math.floor(totalContent * 0.4);
setContentVelocity({
thisWeek: {
articles: weeklyArticles,
words: weeklyArticles * 1500,
images: Math.floor(totalImages * 0.15)
},
thisMonth: {
articles: monthlyArticles,
words: monthlyArticles * 1500,
images: Math.floor(totalImages * 0.4)
},
total: {
articles: totalContent,
words: totalContent * 1500,
images: totalImages
},
trend: totalContent > 0 ? Math.floor(Math.random() * 40) - 10 : 0,
});
// Update content velocity from real API data
setContentVelocity(stats.content_velocity);
// Generate mock recent activity based on actual data
const activityList: ActivityItem[] = [];
if (totalClusters > 0) {
activityList.push({
id: 'cluster_1',
type: 'clustering',
title: `Clustered ${Math.min(45, totalKeywords)} keywords → ${Math.min(8, totalClusters)} clusters`,
description: '',
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000),
href: '/planner/clusters',
});
}
if (totalContent > 0) {
activityList.push({
id: 'content_1',
type: 'content',
title: `Generated ${Math.min(5, totalContent)} articles`,
description: '',
timestamp: new Date(Date.now() - 4 * 60 * 60 * 1000),
href: '/writer/content',
});
}
if (totalImages > 0) {
activityList.push({
id: 'images_1',
type: 'images',
title: `Created ${Math.min(15, totalImages)} image prompts`,
description: '',
timestamp: new Date(Date.now() - 24 * 60 * 60 * 1000),
href: '/writer/images',
});
}
if (publishedContent > 0) {
activityList.push({
id: 'published_1',
type: 'published',
title: `Published article to WordPress`,
description: '',
timestamp: new Date(Date.now() - 24 * 60 * 60 * 1000),
href: '/writer/published',
});
}
if (totalKeywords > 0) {
activityList.push({
id: 'keywords_1',
type: 'keywords',
title: `Added ${Math.min(23, totalKeywords)} keywords`,
description: '',
timestamp: new Date(Date.now() - 48 * 60 * 60 * 1000),
href: '/planner/keywords',
});
}
// Update recent activity from real API data (convert timestamp strings to Date objects)
const activityList: ActivityItem[] = stats.recent_activity.map(item => ({
...item,
timestamp: new Date(item.timestamp),
}));
setRecentActivity(activityList);
// Update AI operations (mock data based on content created)
const clusteringOps = totalClusters > 0 ? Math.ceil(totalClusters / 3) : 0;
const ideasOps = totalIdeas > 0 ? Math.ceil(totalIdeas / 5) : 0;
const contentOps = totalContent;
const imageOps = totalImages > 0 ? Math.ceil(totalImages / 3) : 0;
// Update AI operations from real API data
// Map operation types to display types
const operationTypeMap: Record<string, string> = {
'clustering': 'clustering',
'idea_generation': 'ideas',
'content_generation': 'content',
'image_generation': 'images',
'image_prompt_extraction': 'images',
};
const mappedOperations = stats.ai_operations.operations.map(op => ({
type: operationTypeMap[op.type] || op.type,
count: op.count,
credits: op.credits,
}));
// Ensure all expected types exist
const expectedTypes = ['clustering', 'ideas', 'content', 'images'];
for (const type of expectedTypes) {
if (!mappedOperations.find(op => op.type === type)) {
mappedOperations.push({ type, count: 0, credits: 0 });
}
}
setAIOperations({
period: '7d',
operations: [
{ type: 'clustering', count: clusteringOps, credits: clusteringOps * 10 },
{ type: 'ideas', count: ideasOps, credits: ideasOps * 2 },
{ type: 'content', count: contentOps, credits: contentOps * 50 },
{ type: 'images', count: imageOps, credits: imageOps * 5 },
],
totals: {
count: clusteringOps + ideasOps + contentOps + imageOps,
credits: (clusteringOps * 10) + (ideasOps * 2) + (contentOps * 50) + (imageOps * 5),
successRate: 98.5,
avgCreditsPerOp: contentOps > 0 ? 18.6 : 0,
},
period: stats.ai_operations.period,
operations: mappedOperations,
totals: stats.ai_operations.totals,
});
// Set automation status (would come from API in real implementation)
// Set automation status (would come from automation API)
setAutomationData({
status: sites.length > 0 ? 'active' : 'not_configured',
schedule: sites.length > 0 ? 'Daily 9 AM' : undefined,
lastRun: sites.length > 0 ? {
lastRun: sites.length > 0 && counts.content.total > 0 ? {
timestamp: new Date(Date.now() - 12 * 60 * 60 * 1000),
clustered: Math.min(12, totalKeywords),
ideas: Math.min(8, totalIdeas),
content: Math.min(5, totalContent),
images: Math.min(15, totalImages),
clustered: pipeline.clusters,
ideas: pipeline.ideas,
content: counts.content.total,
images: counts.images.total,
success: true,
} : undefined,
nextRun: sites.length > 0 ? new Date(Date.now() + 12 * 60 * 60 * 1000) : undefined,
@@ -352,7 +286,7 @@ export default function Home() {
} finally {
setLoading(false);
}
}, [siteFilter, sites.length, toast]);
}, [siteFilter, sites, toast]);
// Fetch dashboard data when filter changes
useEffect(() => {

View File

@@ -7,6 +7,7 @@ import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import TablePageTemplate from '../../templates/TablePageTemplate';
import {
fetchClusters,
fetchImages,
createCluster,
updateCluster,
deleteCluster,
@@ -41,6 +42,7 @@ export default function Clusters() {
// Total counts for footer widget (not page-filtered)
const [totalWithIdeas, setTotalWithIdeas] = useState(0);
const [totalReady, setTotalReady] = useState(0);
const [totalImagesCount, setTotalImagesCount] = useState(0);
// Filter state
const [searchTerm, setSearchTerm] = useState('');
@@ -97,6 +99,10 @@ export default function Clusters() {
status: 'new',
});
setTotalReady(newRes.count || 0);
// Get actual total images count
const imagesRes = await fetchImages({ page_size: 1 });
setTotalImagesCount(imagesRes.count || 0);
} catch (error) {
console.error('Error loading total metrics:', error);
}
@@ -184,18 +190,17 @@ export default function Clusters() {
};
}, [loadClusters]);
// Debounced search
// Debounced search - reset to page 1 when search term changes
// Only depend on searchTerm to avoid pagination reset on page navigation
useEffect(() => {
const timer = setTimeout(() => {
if (currentPage === 1) {
loadClusters();
} else {
setCurrentPage(1);
}
// Always reset to page 1 when search changes
// The main useEffect will handle reloading when currentPage changes
setCurrentPage(1);
}, 500);
return () => clearTimeout(timer);
}, [searchTerm, currentPage, loadClusters]);
}, [searchTerm]);
// Reset to page 1 when pageSize changes
useEffect(() => {
@@ -380,16 +385,43 @@ export default function Clusters() {
handleRowAction,
]);
// Calculate header metrics
// Calculate header metrics - use totalWithIdeas/totalReady from API calls (not page data)
// This ensures metrics show correct totals across all pages, not just current page
const headerMetrics = useMemo(() => {
if (!pageConfig?.headerMetrics) return [];
return pageConfig.headerMetrics.map((metric) => ({
label: metric.label,
value: metric.calculate({ clusters, totalCount }),
accentColor: metric.accentColor,
tooltip: (metric as any).tooltip,
}));
}, [pageConfig?.headerMetrics, clusters, totalCount]);
// Override the calculate function to use pre-loaded totals instead of filtering page data
return pageConfig.headerMetrics.map((metric) => {
let value: number;
switch (metric.label) {
case 'Clusters':
value = totalCount || 0;
break;
case 'New':
// Use totalReady from loadTotalMetrics() (clusters without ideas)
value = totalReady;
break;
case 'Keywords':
// Sum of keywords across all clusters on current page (this is acceptable for display)
value = clusters.reduce((sum: number, c) => sum + (c.keywords_count || 0), 0);
break;
case 'Volume':
// Sum of volume across all clusters on current page (this is acceptable for display)
value = clusters.reduce((sum: number, c) => sum + (c.total_volume || 0), 0);
break;
default:
value = metric.calculate({ clusters, totalCount });
}
return {
label: metric.label,
value,
accentColor: metric.accentColor,
tooltip: (metric as any).tooltip,
};
});
}, [pageConfig?.headerMetrics, clusters, totalCount, totalReady, totalWithIdeas]);
const resetForm = useCallback(() => {
setFormData({
@@ -589,7 +621,7 @@ export default function Clusters() {
],
writerItems: [
{ label: 'Content Generated', value: 0, color: 'blue' },
{ label: 'Images Created', value: 0, color: 'purple' },
{ label: 'Images Created', value: totalImagesCount, color: 'purple' },
{ label: 'Published', value: 0, color: 'green' },
],
analyticsHref: '/account/usage',

View File

@@ -7,6 +7,7 @@ import { useState, useEffect, useMemo, useCallback } from 'react';
import TablePageTemplate from '../../templates/TablePageTemplate';
import {
fetchContentIdeas,
fetchImages,
createContentIdea,
updateContentIdea,
deleteContentIdea,
@@ -44,6 +45,7 @@ export default function Ideas() {
// Total counts for footer widget (not page-filtered)
const [totalInTasks, setTotalInTasks] = useState(0);
const [totalPending, setTotalPending] = useState(0);
const [totalImagesCount, setTotalImagesCount] = useState(0);
// Filter state
const [searchTerm, setSearchTerm] = useState('');
@@ -117,6 +119,10 @@ export default function Ideas() {
status: 'new',
});
setTotalPending(newRes.count || 0);
// Get actual total images count
const imagesRes = await fetchImages({ page_size: 1 });
setTotalImagesCount(imagesRes.count || 0);
} catch (error) {
console.error('Error loading total metrics:', error);
}
@@ -189,18 +195,17 @@ export default function Ideas() {
setCurrentPage(1);
}, [pageSize]);
// Debounced search
// Debounced search - reset to page 1 when search term changes
// Only depend on searchTerm to avoid pagination reset on page navigation
useEffect(() => {
const timer = setTimeout(() => {
if (currentPage === 1) {
loadIdeas();
} else {
setCurrentPage(1);
}
// Always reset to page 1 when search changes
// The main useEffect will handle reloading when currentPage changes
setCurrentPage(1);
}, 500);
return () => clearTimeout(timer);
}, [searchTerm, currentPage, loadIdeas]);
}, [searchTerm]);
// Handle sorting
const handleSort = (field: string, direction: 'asc' | 'desc') => {
@@ -289,16 +294,43 @@ export default function Ideas() {
});
}, [clusters, activeSector, formData, searchTerm, statusFilter, clusterFilter, structureFilter, typeFilter]);
// Calculate header metrics
// Calculate header metrics - use totalInTasks/totalPending from API calls (not page data)
// This ensures metrics show correct totals across all pages, not just current page
const headerMetrics = useMemo(() => {
if (!pageConfig?.headerMetrics) return [];
return pageConfig.headerMetrics.map((metric) => ({
label: metric.label,
value: metric.calculate({ ideas, totalCount }),
accentColor: metric.accentColor,
tooltip: (metric as any).tooltip,
}));
}, [pageConfig?.headerMetrics, ideas, totalCount]);
// Override the calculate function to use pre-loaded totals instead of filtering page data
return pageConfig.headerMetrics.map((metric) => {
let value: number;
switch (metric.label) {
case 'Ideas':
value = totalCount || 0;
break;
case 'New':
// Use totalPending from loadTotalMetrics() (ideas with status='new')
value = totalPending;
break;
case 'Queued':
// Use totalInTasks from loadTotalMetrics() (ideas with status='queued')
value = totalInTasks;
break;
case 'Completed':
// Calculate completed from totalCount - (totalPending + totalInTasks)
value = Math.max(0, totalCount - totalPending - totalInTasks);
break;
default:
value = metric.calculate({ ideas, totalCount });
}
return {
label: metric.label,
value,
accentColor: metric.accentColor,
tooltip: (metric as any).tooltip,
};
});
}, [pageConfig?.headerMetrics, ideas, totalCount, totalPending, totalInTasks]);
const resetForm = useCallback(() => {
setFormData({
@@ -522,7 +554,7 @@ export default function Ideas() {
],
writerItems: [
{ label: 'Content Generated', value: ideas.filter(i => i.status === 'completed').length, color: 'blue' },
{ label: 'Images Created', value: 0, color: 'purple' },
{ label: 'Images Created', value: totalImagesCount, color: 'purple' },
{ label: 'Published', value: 0, color: 'green' },
],
analyticsHref: '/account/usage',

View File

@@ -8,6 +8,7 @@ import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import TablePageTemplate from '../../templates/TablePageTemplate';
import {
fetchKeywords,
fetchImages,
createKeyword,
updateKeyword,
deleteKeyword,
@@ -50,6 +51,7 @@ export default function Keywords() {
const [totalClustered, setTotalClustered] = useState(0);
const [totalUnmapped, setTotalUnmapped] = useState(0);
const [totalVolume, setTotalVolume] = useState(0);
const [totalImagesCount, setTotalImagesCount] = useState(0);
// Filter state - match Keywords.tsx
const [searchTerm, setSearchTerm] = useState('');
@@ -141,6 +143,10 @@ export default function Keywords() {
// For now, we'll just calculate from current data or set to 0
// TODO: Backend should provide total volume as an aggregated metric
setTotalVolume(0);
// Get actual total images count
const imagesRes = await fetchImages({ page_size: 1 });
setTotalImagesCount(imagesRes.count || 0);
} catch (error) {
console.error('Error loading total metrics:', error);
}
@@ -524,16 +530,43 @@ export default function Keywords() {
activeSite,
]);
// Calculate header metrics from config (matching reference plugin KPIs from kpi-config.php)
// Calculate header metrics - use totalClustered/totalUnmapped from API calls (not page data)
// This ensures metrics show correct totals across all pages, not just current page
const headerMetrics = useMemo(() => {
if (!pageConfig?.headerMetrics) return [];
return pageConfig.headerMetrics.map((metric) => ({
label: metric.label,
value: metric.calculate({ keywords, totalCount, clusters }),
accentColor: metric.accentColor,
tooltip: (metric as any).tooltip, // Add tooltip support
}));
}, [pageConfig?.headerMetrics, keywords, totalCount, clusters]);
// Override the calculate function to use pre-loaded totals instead of filtering page data
return pageConfig.headerMetrics.map((metric) => {
let value: number;
switch (metric.label) {
case 'Keywords':
value = totalCount || 0;
break;
case 'Clustered':
// Use totalClustered from loadTotalMetrics() instead of filtering page data
value = totalClustered;
break;
case 'Unmapped':
// Use totalUnmapped from loadTotalMetrics() instead of filtering page data
value = totalUnmapped;
break;
case 'Volume':
// Use totalVolume from loadTotalMetrics() (if implemented) or keep original
value = totalVolume || keywords.reduce((sum: number, k) => sum + (k.volume || 0), 0);
break;
default:
value = metric.calculate({ keywords, totalCount, clusters });
}
return {
label: metric.label,
value,
accentColor: metric.accentColor,
tooltip: (metric as any).tooltip,
};
});
}, [pageConfig?.headerMetrics, keywords, totalCount, clusters, totalClustered, totalUnmapped, totalVolume]);
// Calculate workflow insights based on UX doc principles
const workflowStats = useMemo(() => {
@@ -819,7 +852,7 @@ export default function Keywords() {
],
writerItems: [
{ label: 'Content Generated', value: 0, color: 'blue' },
{ label: 'Images Created', value: 0, color: 'purple' },
{ label: 'Images Created', value: totalImagesCount, color: 'purple' },
{ label: 'Published', value: 0, color: 'green' },
],
creditsUsed: 0,

View File

@@ -8,6 +8,7 @@ import { useNavigate } from 'react-router-dom';
import TablePageTemplate from '../../templates/TablePageTemplate';
import {
fetchContent,
fetchImages,
Content,
ContentListResponse,
ContentFilters,
@@ -34,7 +35,12 @@ export default function Approved() {
// Data state
const [content, setContent] = useState<Content[]>([]);
const [loading, setLoading] = useState(true);
// Total counts for footer widget and header metrics (not page-filtered)
const [totalOnSite, setTotalOnSite] = useState(0);
const [totalPendingPublish, setTotalPendingPublish] = useState(0);
const [totalImagesCount, setTotalImagesCount] = useState(0);
// Filter state - default to approved status
const [searchTerm, setSearchTerm] = useState('');
const [publishStatusFilter, setPublishStatusFilter] = useState('');
@@ -50,6 +56,36 @@ export default function Approved() {
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
const [showContent, setShowContent] = useState(false);
// Load total metrics for footer widget and header metrics (not affected by pagination)
const loadTotalMetrics = useCallback(async () => {
try {
// Fetch all approved content to calculate totals
const data = await fetchContent({
status: 'published', // Backend uses 'published' for approved content
page_size: 1000, // Fetch enough to count
});
const allContent = data.results || [];
// Count by external_id presence
const onSite = allContent.filter(c => c.external_id).length;
const pending = allContent.filter(c => !c.external_id).length;
setTotalOnSite(onSite);
setTotalPendingPublish(pending);
// Get actual total images count
const imagesRes = await fetchImages({ page_size: 1 });
setTotalImagesCount(imagesRes.count || 0);
} catch (error) {
console.error('Error loading total metrics:', error);
}
}, []);
// Load total metrics on mount
useEffect(() => {
loadTotalMetrics();
}, [loadTotalMetrics]);
// Load content - filtered for approved status (API still uses 'published' internally)
const loadContent = useCallback(async () => {
setLoading(true);
@@ -137,18 +173,17 @@ export default function Approved() {
setCurrentPage(1);
}, [pageSize]);
// Debounced search
// Debounced search - reset to page 1 when search term changes
// Only depend on searchTerm to avoid pagination reset on page navigation
useEffect(() => {
const timer = setTimeout(() => {
if (currentPage === 1) {
loadContent();
} else {
setCurrentPage(1);
}
// Always reset to page 1 when search changes
// The main useEffect will handle reloading when currentPage changes
setCurrentPage(1);
}, 500);
return () => clearTimeout(timer);
}, [searchTerm, currentPage, loadContent]);
}, [searchTerm]);
// Handle sorting
const handleSort = (field: string, direction: 'asc' | 'desc') => {
@@ -292,15 +327,38 @@ export default function Approved() {
});
}, [searchTerm, publishStatusFilter, activeSector, navigate]);
// Calculate header metrics
// Calculate header metrics - use totals from API calls (not page data)
// This ensures metrics show correct totals across all pages, not just current page
const headerMetrics = useMemo(() => {
if (!pageConfig?.headerMetrics) return [];
return pageConfig.headerMetrics.map((metric) => ({
label: metric.label,
value: metric.calculate({ content, totalCount }),
accentColor: metric.accentColor,
}));
}, [pageConfig?.headerMetrics, content, totalCount]);
// Override the calculate function to use pre-loaded totals instead of filtering page data
return pageConfig.headerMetrics.map((metric) => {
let value: number;
switch (metric.label) {
case 'Approved':
value = totalCount || 0;
break;
case 'On Site':
// Use totalOnSite from loadTotalMetrics()
value = totalOnSite;
break;
case 'Pending':
// Use totalPendingPublish from loadTotalMetrics()
value = totalPendingPublish;
break;
default:
value = metric.calculate({ content, totalCount });
}
return {
label: metric.label,
value,
accentColor: metric.accentColor,
};
});
}, [pageConfig?.headerMetrics, content, totalCount, totalOnSite, totalPendingPublish]);
return (
<>
@@ -398,7 +456,7 @@ export default function Approved() {
fromHref: '/writer/content',
actionLabel: 'Generate Images',
toLabel: 'Images',
toValue: 0,
toValue: totalImagesCount,
toHref: '/writer/images',
progress: 0,
color: 'purple',
@@ -431,7 +489,7 @@ export default function Approved() {
],
writerItems: [
{ label: 'Content Generated', value: 0, color: 'blue' },
{ label: 'Images Created', value: 0, color: 'purple' },
{ label: 'Images Created', value: totalImagesCount, color: 'purple' },
{ label: 'Articles Published', value: totalCount, color: 'green' },
],
analyticsHref: '/account/usage',

View File

@@ -8,6 +8,7 @@ import { Link, useNavigate } from 'react-router-dom';
import TablePageTemplate from '../../templates/TablePageTemplate';
import {
fetchContent,
fetchImages,
Content as ContentType,
ContentFilters,
generateImagePrompts,
@@ -34,7 +35,13 @@ export default function Content() {
// Data state
const [content, setContent] = useState<ContentType[]>([]);
const [loading, setLoading] = useState(true);
// Total counts for footer widget and header metrics (not page-filtered)
const [totalDraft, setTotalDraft] = useState(0);
const [totalReview, setTotalReview] = useState(0);
const [totalPublished, setTotalPublished] = useState(0);
const [totalImagesCount, setTotalImagesCount] = useState(0);
// Filter state
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('draft');
@@ -55,6 +62,46 @@ export default function Content() {
const progressModal = useProgressModal();
const hasReloadedRef = useRef(false);
// Load total metrics for footer widget and header metrics (not affected by pagination)
const loadTotalMetrics = useCallback(async () => {
try {
// Get content with status='draft'
const draftRes = await fetchContent({
page_size: 1,
...(activeSector?.id && { sector_id: activeSector.id }),
status: 'draft',
});
setTotalDraft(draftRes.count || 0);
// Get content with status='review'
const reviewRes = await fetchContent({
page_size: 1,
...(activeSector?.id && { sector_id: activeSector.id }),
status: 'review',
});
setTotalReview(reviewRes.count || 0);
// Get content with status='published'
const publishedRes = await fetchContent({
page_size: 1,
...(activeSector?.id && { sector_id: activeSector.id }),
status: 'published',
});
setTotalPublished(publishedRes.count || 0);
// Get actual total images count
const imagesRes = await fetchImages({ page_size: 1 });
setTotalImagesCount(imagesRes.count || 0);
} catch (error) {
console.error('Error loading total metrics:', error);
}
}, [activeSector]);
// Load total metrics when sector changes
useEffect(() => {
loadTotalMetrics();
}, [loadTotalMetrics]);
// Load content - wrapped in useCallback
const loadContent = useCallback(async () => {
setLoading(true);
@@ -115,18 +162,17 @@ export default function Content() {
setCurrentPage(1);
}, [pageSize]);
// Debounced search
// Debounced search - reset to page 1 when search term changes
// Only depend on searchTerm to avoid pagination reset on page navigation
useEffect(() => {
const timer = setTimeout(() => {
if (currentPage === 1) {
loadContent();
} else {
setCurrentPage(1);
}
// Always reset to page 1 when search changes
// The main useEffect will handle reloading when currentPage changes
setCurrentPage(1);
}, 500);
return () => clearTimeout(timer);
}, [searchTerm, currentPage, loadContent]);
}, [searchTerm]);
// Handle sorting
const handleSort = (field: string, direction: 'asc' | 'desc') => {
@@ -160,16 +206,43 @@ export default function Content() {
handleRowClick,
]);
// Calculate header metrics
// Calculate header metrics - use totals from API calls (not page data)
// This ensures metrics show correct totals across all pages, not just current page
const headerMetrics = useMemo(() => {
if (!pageConfig?.headerMetrics) return [];
return pageConfig.headerMetrics.map((metric) => ({
label: metric.label,
value: metric.calculate({ content, totalCount }),
accentColor: metric.accentColor,
tooltip: (metric as any).tooltip,
}));
}, [pageConfig?.headerMetrics, content, totalCount]);
// Override the calculate function to use pre-loaded totals instead of filtering page data
return pageConfig.headerMetrics.map((metric) => {
let value: number;
switch (metric.label) {
case 'Content':
value = totalCount || 0;
break;
case 'Draft':
// Use totalDraft from loadTotalMetrics()
value = totalDraft;
break;
case 'In Review':
// Use totalReview from loadTotalMetrics()
value = totalReview;
break;
case 'Published':
// Use totalPublished from loadTotalMetrics()
value = totalPublished;
break;
default:
value = metric.calculate({ content, totalCount });
}
return {
label: metric.label,
value,
accentColor: metric.accentColor,
tooltip: (metric as any).tooltip,
};
});
}, [pageConfig?.headerMetrics, content, totalCount, totalDraft, totalReview, totalPublished]);
const handleRowAction = useCallback(async (action: string, row: ContentType) => {
if (action === 'view_on_wordpress') {
@@ -347,7 +420,7 @@ export default function Content() {
],
writerItems: [
{ label: 'Content Generated', value: totalCount, color: 'blue' },
{ label: 'Images Created', value: content.filter(c => c.has_generated_images).length, color: 'purple' },
{ label: 'Images Created', value: totalImagesCount, color: 'purple' },
{ label: 'Published', value: content.filter(c => c.status === 'published').length, color: 'green' },
],
analyticsHref: '/account/usage',

View File

@@ -8,6 +8,7 @@ import { Link } from 'react-router-dom';
import TablePageTemplate from '../../templates/TablePageTemplate';
import {
fetchContentImages,
fetchImages,
ContentImagesGroup,
ContentImagesResponse,
fetchImageGenerationSettings,
@@ -34,7 +35,13 @@ export default function Images() {
// Data state
const [images, setImages] = useState<ContentImagesGroup[]>([]);
const [loading, setLoading] = useState(true);
// Total counts for footer widget and header metrics (not page-filtered)
const [totalComplete, setTotalComplete] = useState(0);
const [totalPartial, setTotalPartial] = useState(0);
const [totalPending, setTotalPending] = useState(0);
const [totalImagesCount, setTotalImagesCount] = useState(0); // Actual images count
// Filter state
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('');
@@ -69,6 +76,49 @@ export default function Images() {
const [isImageModalOpen, setIsImageModalOpen] = useState(false);
const [modalImageUrl, setModalImageUrl] = useState<string | null>(null);
// Load total metrics for footer widget and header metrics (not affected by pagination)
const loadTotalMetrics = useCallback(async () => {
try {
// Fetch content-grouped images for status counts
const data: ContentImagesResponse = await fetchContentImages({});
const allImages = data.results || [];
// Count by overall_status (content-level status)
let complete = 0;
let partial = 0;
let pending = 0;
allImages.forEach(img => {
switch (img.overall_status) {
case 'complete':
complete++;
break;
case 'partial':
partial++;
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) {
console.error('Error loading total metrics:', error);
}
}, []);
// Load total metrics on mount
useEffect(() => {
loadTotalMetrics();
}, [loadTotalMetrics]);
// Load images - wrapped in useCallback
const loadImages = useCallback(async () => {
setLoading(true);
@@ -155,18 +205,17 @@ export default function Images() {
};
}, [loadImages]);
// Debounced search
// Debounced search - reset to page 1 when search term changes
// Only depend on searchTerm to avoid pagination reset on page navigation
useEffect(() => {
const timer = setTimeout(() => {
if (currentPage === 1) {
loadImages();
} else {
setCurrentPage(1);
}
// Always reset to page 1 when search changes
// The main useEffect will handle reloading when currentPage changes
setCurrentPage(1);
}, 500);
return () => clearTimeout(timer);
}, [searchTerm, currentPage, loadImages]);
}, [searchTerm]);
// Handle sorting
const handleSort = (field: string, direction: 'asc' | 'desc') => {
@@ -438,16 +487,54 @@ export default function Images() {
});
}, [searchTerm, statusFilter, maxInArticleImages, handleGenerateImages, handleImageClick]);
// Calculate header metrics
// Calculate header metrics - use totals from API calls (not page data)
// This ensures metrics show correct totals across all pages, not just current page
const headerMetrics = useMemo(() => {
if (!pageConfig?.headerMetrics) return [];
return pageConfig.headerMetrics.map((metric) => ({
label: metric.label,
value: metric.calculate({ images, totalCount }),
accentColor: metric.accentColor,
tooltip: (metric as any).tooltip,
}));
}, [pageConfig?.headerMetrics, images, totalCount]);
// Override the calculate function to use pre-loaded totals instead of filtering page data
// Also add a "Total Images" metric at the end
const baseMetrics = pageConfig.headerMetrics.map((metric) => {
let value: number;
switch (metric.label) {
case 'Content':
value = totalCount || 0;
break;
case 'Complete':
// Use totalComplete from loadTotalMetrics()
value = totalComplete;
break;
case 'Partial':
// Use totalPartial from loadTotalMetrics()
value = totalPartial;
break;
case 'Pending':
// Use totalPending from loadTotalMetrics()
value = totalPending;
break;
default:
value = metric.calculate({ images, totalCount });
}
return {
label: metric.label,
value,
accentColor: metric.accentColor,
tooltip: (metric as any).tooltip,
};
});
// 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;
}, [pageConfig?.headerMetrics, images, totalCount, totalComplete, totalPartial, totalPending, totalImagesCount]);
return (
<>
@@ -617,7 +704,7 @@ export default function Images() {
fromHref: '/writer/content',
actionLabel: 'Generate Images',
toLabel: 'Images',
toValue: totalCount,
toValue: totalImagesCount,
toHref: '/writer/images',
progress: 100,
color: 'purple',
@@ -650,7 +737,7 @@ export default function Images() {
],
writerItems: [
{ label: 'Content Generated', value: 0, color: 'blue' },
{ label: 'Images Created', value: totalCount, color: 'purple' },
{ label: 'Images Created', value: totalImagesCount, color: 'purple' },
{ label: 'Articles Published', value: 0, color: 'green' },
],
analyticsHref: '/account/usage',

View File

@@ -8,6 +8,7 @@ import { useNavigate } from 'react-router-dom';
import TablePageTemplate from '../../templates/TablePageTemplate';
import {
fetchContent,
fetchImages,
Content,
ContentListResponse,
ContentFilters,
@@ -31,6 +32,7 @@ export default function Review() {
// Data state
const [content, setContent] = useState<Content[]>([]);
const [loading, setLoading] = useState(true);
const [totalImagesCount, setTotalImagesCount] = useState(0);
// Filter state - default to review status
const [searchTerm, setSearchTerm] = useState('');
@@ -83,6 +85,19 @@ export default function Review() {
loadContent();
}, [loadContent]);
// Load total images count
useEffect(() => {
const loadImageCount = async () => {
try {
const imagesRes = await fetchImages({ page_size: 1 });
setTotalImagesCount(imagesRes.count || 0);
} catch (error) {
console.error('Error loading image count:', error);
}
};
loadImageCount();
}, []);
// Listen for site and sector changes and refresh data
useEffect(() => {
const handleSiteChange = () => {
@@ -494,7 +509,7 @@ export default function Review() {
fromHref: '/writer/content',
actionLabel: 'Generate Images',
toLabel: 'Images',
toValue: 0,
toValue: totalImagesCount,
toHref: '/writer/images',
progress: 0,
color: 'purple',
@@ -527,7 +542,7 @@ export default function Review() {
],
writerItems: [
{ label: 'Content Generated', value: 0, color: 'blue' },
{ label: 'Images Created', value: 0, color: 'purple' },
{ label: 'Images Created', value: totalImagesCount, color: 'purple' },
{ label: 'Articles Published', value: 0, color: 'green' },
],
analyticsHref: '/account/usage',

View File

@@ -8,6 +8,7 @@ import { Link } from 'react-router-dom';
import TablePageTemplate from '../../templates/TablePageTemplate';
import {
fetchTasks,
fetchImages,
createTask,
updateTask,
deleteTask,
@@ -42,6 +43,13 @@ export default function Tasks() {
const [tasks, setTasks] = useState<Task[]>([]);
const [clusters, setClusters] = useState<Cluster[]>([]);
const [loading, setLoading] = useState(true);
// Total counts for footer widget and header metrics (not page-filtered)
const [totalQueued, setTotalQueued] = useState(0);
const [totalProcessing, setTotalProcessing] = useState(0);
const [totalCompleted, setTotalCompleted] = useState(0);
const [totalFailed, setTotalFailed] = useState(0);
const [totalImagesCount, setTotalImagesCount] = useState(0);
// Filter state
const [searchTerm, setSearchTerm] = useState('');
@@ -97,6 +105,54 @@ export default function Tasks() {
loadClusters();
}, []);
// Load total metrics for footer widget and header metrics (not affected by pagination)
const loadTotalMetrics = useCallback(async () => {
try {
// Get tasks with status='queued'
const queuedRes = await fetchTasks({
page_size: 1,
...(activeSector?.id && { sector_id: activeSector.id }),
status: 'queued',
});
setTotalQueued(queuedRes.count || 0);
// Get tasks with status='in_progress'
const processingRes = await fetchTasks({
page_size: 1,
...(activeSector?.id && { sector_id: activeSector.id }),
status: 'in_progress',
});
setTotalProcessing(processingRes.count || 0);
// Get tasks with status='completed'
const completedRes = await fetchTasks({
page_size: 1,
...(activeSector?.id && { sector_id: activeSector.id }),
status: 'completed',
});
setTotalCompleted(completedRes.count || 0);
// Get tasks with status='failed'
const failedRes = await fetchTasks({
page_size: 1,
...(activeSector?.id && { sector_id: activeSector.id }),
status: 'failed',
});
setTotalFailed(failedRes.count || 0);
// Get actual total images count
const imagesRes = await fetchImages({ page_size: 1 });
setTotalImagesCount(imagesRes.count || 0);
} catch (error) {
console.error('Error loading total metrics:', error);
}
}, [activeSector]);
// Load total metrics when sector changes
useEffect(() => {
loadTotalMetrics();
}, [loadTotalMetrics]);
// Load tasks - wrapped in useCallback
const loadTasks = useCallback(async () => {
setLoading(true);
@@ -167,18 +223,17 @@ export default function Tasks() {
}, [pageSize]);
// Debounced search
// Debounced search - reset to page 1 when search term changes
// Only depend on searchTerm to avoid pagination reset on page navigation
useEffect(() => {
const timer = setTimeout(() => {
if (currentPage === 1) {
loadTasks();
} else {
setCurrentPage(1);
}
// Always reset to page 1 when search changes
// The main useEffect will handle reloading when currentPage changes
setCurrentPage(1);
}, 500);
return () => clearTimeout(timer);
}, [searchTerm, currentPage, loadTasks]);
}, [searchTerm]);
// Handle sorting
const handleSort = (field: string, direction: 'asc' | 'desc') => {
@@ -318,16 +373,47 @@ export default function Tasks() {
});
}, [clusters, activeSector, formData, searchTerm, statusFilter, clusterFilter, structureFilter, typeFilter, sourceFilter]);
// Calculate header metrics
// Calculate header metrics - use totals from API calls (not page data)
// This ensures metrics show correct totals across all pages, not just current page
const headerMetrics = useMemo(() => {
if (!pageConfig?.headerMetrics) return [];
return pageConfig.headerMetrics.map((metric) => ({
label: metric.label,
value: metric.calculate({ tasks, totalCount }),
accentColor: metric.accentColor,
tooltip: (metric as any).tooltip,
}));
}, [pageConfig?.headerMetrics, tasks, totalCount]);
// Override the calculate function to use pre-loaded totals instead of filtering page data
return pageConfig.headerMetrics.map((metric) => {
let value: number;
switch (metric.label) {
case 'Tasks':
value = totalCount || 0;
break;
case 'In Queue':
// Use totalQueued from loadTotalMetrics()
value = totalQueued;
break;
case 'Processing':
// Use totalProcessing from loadTotalMetrics()
value = totalProcessing;
break;
case 'Completed':
// Use totalCompleted from loadTotalMetrics()
value = totalCompleted;
break;
case 'Failed':
// Use totalFailed from loadTotalMetrics()
value = totalFailed;
break;
default:
value = metric.calculate({ tasks, totalCount });
}
return {
label: metric.label,
value,
accentColor: metric.accentColor,
tooltip: (metric as any).tooltip,
};
});
}, [pageConfig?.headerMetrics, tasks, totalCount, totalQueued, totalProcessing, totalCompleted, totalFailed]);
const resetForm = useCallback(() => {
setFormData({
@@ -507,7 +593,7 @@ export default function Tasks() {
fromHref: '/writer/content',
actionLabel: 'Generate Images',
toLabel: 'Images',
toValue: 0,
toValue: totalImagesCount,
toHref: '/writer/images',
progress: 0,
color: 'purple',
@@ -540,7 +626,7 @@ export default function Tasks() {
],
writerItems: [
{ label: 'Content Generated', value: tasks.filter(t => t.status === 'completed').length, color: 'blue' },
{ label: 'Images Created', value: 0, color: 'purple' },
{ label: 'Images Created', value: totalImagesCount, color: 'purple' },
{ label: 'Published', value: 0, color: 'green' },
],
analyticsHref: '/account/usage',

View File

@@ -1042,3 +1042,90 @@ export async function getPublicPlans(): Promise<Plan[]> {
export async function getUsageSummary(): Promise<UsageSummary> {
return fetchAPI('/v1/billing/usage-summary/');
}
// ============================================================================
// DASHBOARD STATS (Real data for home page)
// ============================================================================
export interface DashboardAIOperation {
type: string;
count: number;
credits: number;
}
export interface DashboardAIOperations {
period: string;
operations: DashboardAIOperation[];
totals: {
count: number;
credits: number;
successRate: number;
avgCreditsPerOp: number;
};
}
export interface DashboardActivityItem {
id: string;
type: string;
title: string;
description: string;
timestamp: string;
href: string;
}
export interface DashboardContentVelocity {
thisWeek: {
articles: number;
words: number;
images: number;
};
thisMonth: {
articles: number;
words: number;
images: number;
};
total: {
articles: number;
words: number;
images: number;
};
trend: number;
}
export interface DashboardPipeline {
sites: number;
keywords: number;
clusters: number;
ideas: number;
tasks: number;
drafts: number;
published: number;
}
export interface DashboardStats {
ai_operations: DashboardAIOperations;
recent_activity: DashboardActivityItem[];
content_velocity: DashboardContentVelocity;
pipeline: DashboardPipeline;
counts: {
content: {
total: number;
draft: number;
review: number;
published: number;
};
images: {
total: number;
generated: number;
pending: number;
};
};
}
export async function getDashboardStats(params?: { site_id?: number; days?: number }): Promise<DashboardStats> {
const searchParams = new URLSearchParams();
if (params?.site_id) searchParams.append('site_id', params.site_id.toString());
if (params?.days) searchParams.append('days', params.days.toString());
const query = searchParams.toString();
return fetchAPI(`/v1/account/dashboard/stats/${query ? `?${query}` : ''}`);
}