master - part 2
This commit is contained in:
@@ -189,6 +189,12 @@
|
||||
/* Menu icon sizing - consistent across sidebar */
|
||||
@utility menu-item-icon-size {
|
||||
@apply w-5 h-5 flex-shrink-0;
|
||||
|
||||
/* Force SVG icons to inherit parent size */
|
||||
& svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@utility menu-item-icon-active {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import TablePageTemplate from '../../templates/TablePageTemplate';
|
||||
import {
|
||||
fetchClusters,
|
||||
fetchClustersSummary,
|
||||
fetchImages,
|
||||
createCluster,
|
||||
updateCluster,
|
||||
@@ -43,6 +44,8 @@ export default function Clusters() {
|
||||
const [totalWithIdeas, setTotalWithIdeas] = useState(0);
|
||||
const [totalReady, setTotalReady] = useState(0);
|
||||
const [totalImagesCount, setTotalImagesCount] = useState(0);
|
||||
const [totalVolume, setTotalVolume] = useState(0);
|
||||
const [totalKeywords, setTotalKeywords] = useState(0);
|
||||
|
||||
// Filter state
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
@@ -84,24 +87,31 @@ export default function Clusters() {
|
||||
// Load total metrics for footer widget (not affected by pagination)
|
||||
const loadTotalMetrics = useCallback(async () => {
|
||||
try {
|
||||
// Get clusters with status='mapped' (those that have ideas)
|
||||
const mappedRes = await fetchClusters({
|
||||
page_size: 1,
|
||||
...(activeSector?.id && { sector_id: activeSector.id }),
|
||||
status: 'mapped',
|
||||
});
|
||||
// Fetch summary metrics in parallel with status counts
|
||||
const [summaryRes, mappedRes, newRes, imagesRes] = await Promise.all([
|
||||
fetchClustersSummary(activeSector?.id),
|
||||
fetchClusters({
|
||||
page_size: 1,
|
||||
...(activeSector?.id && { sector_id: activeSector.id }),
|
||||
status: 'mapped',
|
||||
}),
|
||||
fetchClusters({
|
||||
page_size: 1,
|
||||
...(activeSector?.id && { sector_id: activeSector.id }),
|
||||
status: 'new',
|
||||
}),
|
||||
fetchImages({ page_size: 1 }),
|
||||
]);
|
||||
|
||||
// Set summary metrics
|
||||
setTotalVolume(summaryRes.total_volume || 0);
|
||||
setTotalKeywords(summaryRes.total_keywords || 0);
|
||||
|
||||
// Set status counts
|
||||
setTotalWithIdeas(mappedRes.count || 0);
|
||||
|
||||
// Get clusters with status='new' (those that are ready for ideas)
|
||||
const newRes = await fetchClusters({
|
||||
page_size: 1,
|
||||
...(activeSector?.id && { sector_id: activeSector.id }),
|
||||
status: 'new',
|
||||
});
|
||||
setTotalReady(newRes.count || 0);
|
||||
|
||||
// Get actual total images count
|
||||
const imagesRes = await fetchImages({ page_size: 1 });
|
||||
// Set images count
|
||||
setTotalImagesCount(imagesRes.count || 0);
|
||||
} catch (error) {
|
||||
console.error('Error loading total metrics:', error);
|
||||
@@ -403,12 +413,12 @@ export default function Clusters() {
|
||||
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);
|
||||
// Use totalKeywords from summary endpoint (aggregate across all clusters)
|
||||
value = totalKeywords;
|
||||
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);
|
||||
// Use totalVolume from summary endpoint (aggregate across all clusters)
|
||||
value = totalVolume;
|
||||
break;
|
||||
default:
|
||||
value = metric.calculate({ clusters, totalCount });
|
||||
@@ -421,7 +431,7 @@ export default function Clusters() {
|
||||
tooltip: (metric as any).tooltip,
|
||||
};
|
||||
});
|
||||
}, [pageConfig?.headerMetrics, clusters, totalCount, totalReady, totalWithIdeas]);
|
||||
}, [pageConfig?.headerMetrics, clusters, totalCount, totalReady, totalWithIdeas, totalVolume, totalKeywords]);
|
||||
|
||||
const resetForm = useCallback(() => {
|
||||
setFormData({
|
||||
|
||||
@@ -34,6 +34,11 @@ export default function Review() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [totalImagesCount, setTotalImagesCount] = useState(0);
|
||||
|
||||
// Total metrics for footer widget (not page-filtered)
|
||||
const [totalDrafts, setTotalDrafts] = useState(0);
|
||||
const [totalApproved, setTotalApproved] = useState(0);
|
||||
const [totalTasks, setTotalTasks] = useState(0);
|
||||
|
||||
// Filter state - default to review status
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState('review'); // Default to review
|
||||
@@ -85,19 +90,30 @@ 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();
|
||||
// Load total images count and other metrics for footer widget
|
||||
const loadTotalMetrics = useCallback(async () => {
|
||||
try {
|
||||
// Fetch counts in parallel for performance
|
||||
const [imagesRes, draftsRes, approvedRes, tasksRes] = await Promise.all([
|
||||
fetchImages({ page_size: 1 }),
|
||||
fetchContent({ page_size: 1, status: 'draft' }),
|
||||
fetchContent({ page_size: 1, status: 'approved' }),
|
||||
fetchAPI<{ count: number }>('/writer/tasks/?page_size=1'),
|
||||
]);
|
||||
|
||||
setTotalImagesCount(imagesRes.count || 0);
|
||||
setTotalDrafts(draftsRes.count || 0);
|
||||
setTotalApproved(approvedRes.count || 0);
|
||||
setTotalTasks(tasksRes.count || 0);
|
||||
} catch (error) {
|
||||
console.error('Error loading metrics:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadTotalMetrics();
|
||||
}, [loadTotalMetrics]);
|
||||
|
||||
// Listen for site and sector changes and refresh data
|
||||
useEffect(() => {
|
||||
const handleSiteChange = () => {
|
||||
@@ -494,24 +510,24 @@ export default function Review() {
|
||||
pipeline: [
|
||||
{
|
||||
fromLabel: 'Tasks',
|
||||
fromValue: 0,
|
||||
fromValue: totalTasks,
|
||||
fromHref: '/writer/tasks',
|
||||
actionLabel: 'Generate Content',
|
||||
toLabel: 'Drafts',
|
||||
toValue: 0,
|
||||
toValue: totalDrafts,
|
||||
toHref: '/writer/content',
|
||||
progress: 0,
|
||||
progress: totalTasks > 0 ? Math.round((totalDrafts / totalTasks) * 100) : 0,
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
fromLabel: 'Drafts',
|
||||
fromValue: 0,
|
||||
fromValue: totalDrafts,
|
||||
fromHref: '/writer/content',
|
||||
actionLabel: 'Generate Images',
|
||||
toLabel: 'Images',
|
||||
toValue: totalImagesCount,
|
||||
toHref: '/writer/images',
|
||||
progress: 0,
|
||||
progress: totalDrafts > 0 ? Math.round((totalImagesCount / totalDrafts) * 100) : 0,
|
||||
color: 'purple',
|
||||
},
|
||||
{
|
||||
@@ -520,9 +536,9 @@ export default function Review() {
|
||||
fromHref: '/writer/review',
|
||||
actionLabel: 'Review & Publish',
|
||||
toLabel: 'Published',
|
||||
toValue: 0,
|
||||
toHref: '/writer/published',
|
||||
progress: 0,
|
||||
toValue: totalApproved,
|
||||
toHref: '/writer/approved',
|
||||
progress: totalCount > 0 ? Math.round((totalApproved / (totalCount + totalApproved)) * 100) : 0,
|
||||
color: 'green',
|
||||
},
|
||||
],
|
||||
@@ -530,7 +546,7 @@ export default function Review() {
|
||||
{ label: 'Tasks', href: '/writer/tasks' },
|
||||
{ label: 'Content', href: '/writer/content' },
|
||||
{ label: 'Images', href: '/writer/images' },
|
||||
{ label: 'Published', href: '/writer/published' },
|
||||
{ label: 'Published', href: '/writer/approved' },
|
||||
],
|
||||
}}
|
||||
completion={{
|
||||
@@ -541,9 +557,9 @@ export default function Review() {
|
||||
{ label: 'Ideas Generated', value: 0, color: 'amber' },
|
||||
],
|
||||
writerItems: [
|
||||
{ label: 'Content Generated', value: 0, color: 'blue' },
|
||||
{ label: 'Content Generated', value: totalDrafts + totalCount + totalApproved, color: 'blue' },
|
||||
{ label: 'Images Created', value: totalImagesCount, color: 'purple' },
|
||||
{ label: 'Articles Published', value: 0, color: 'green' },
|
||||
{ label: 'Articles Published', value: totalApproved, color: 'green' },
|
||||
],
|
||||
analyticsHref: '/account/usage',
|
||||
}}
|
||||
|
||||
@@ -852,6 +852,35 @@ export async function bulkUpdateClustersStatus(ids: number[], status: string): P
|
||||
});
|
||||
}
|
||||
|
||||
export interface ClustersSummary {
|
||||
total_clusters: number;
|
||||
total_keywords: number;
|
||||
total_volume: number;
|
||||
}
|
||||
|
||||
export async function fetchClustersSummary(sectorId?: number): Promise<ClustersSummary> {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
// Auto-add site filter
|
||||
const activeSiteId = getActiveSiteId();
|
||||
if (activeSiteId) {
|
||||
params.append('site_id', activeSiteId.toString());
|
||||
}
|
||||
|
||||
// Add sector filter if provided or get active
|
||||
if (sectorId !== undefined) {
|
||||
params.append('sector_id', sectorId.toString());
|
||||
} else {
|
||||
const activeSectorId = getActiveSectorId();
|
||||
if (activeSectorId !== null && activeSectorId !== undefined) {
|
||||
params.append('sector_id', activeSectorId.toString());
|
||||
}
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
return fetchAPI(`/v1/planner/clusters/summary/${queryString ? `?${queryString}` : ''}`);
|
||||
}
|
||||
|
||||
export async function autoClusterKeywords(keywordIds: number[], sectorId?: number): Promise<{ success: boolean; task_id?: string; clusters_created?: number; keywords_updated?: number; message?: string; error?: string }> {
|
||||
const endpoint = `/v1/planner/keywords/auto_cluster/`;
|
||||
const requestBody = { ids: keywordIds, sector_id: sectorId };
|
||||
|
||||
Reference in New Issue
Block a user