metricsa dn backedn fixes
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user