many changes for modules widgets and colors and styling

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-31 23:52:43 +00:00
parent b61bd6e64d
commit 89b64cd737
34 changed files with 2450 additions and 1985 deletions

View File

@@ -0,0 +1,252 @@
/**
* useModuleStats Hook
*
* Centralized hook for fetching module-level statistics
* used in the Module Stats widget (widget 2) of the footer.
*
* IMPORTANT: Content table structure
* - Tasks is separate table (has status: queued, processing, completed, failed)
* - Content table has status field: 'draft', 'review', 'published' (approved)
* - Images is separate table linked to content
*
* Credits data comes from /v1/billing/credits/usage/summary/ endpoint
* which returns by_operation with operation types:
* - clustering: Keyword Clustering (Planner)
* - idea_generation: Content Ideas Generation (Planner)
* - content_generation: Content Generation (Writer)
* - image_generation: Image Generation (Writer)
*/
import { useState, useEffect, useCallback } from 'react';
import {
fetchKeywords,
fetchClusters,
fetchContentIdeas,
fetchTasks,
fetchContent,
fetchImages,
fetchAPI,
} from '../services/api';
import { useSiteStore } from '../store/siteStore';
import { useSectorStore } from '../store/sectorStore';
export interface PlannerModuleStats {
totalKeywords: number;
keywordsMapped: number; // Keywords with status='mapped' (assigned to clusters)
totalClusters: number;
totalIdeas: number;
ideasConverted: number; // Ideas that have been converted to tasks
}
export interface WriterModuleStats {
totalTasks: number;
tasksCompleted: number; // Tasks with status='completed'
contentDrafts: number; // Content with status='draft'
contentReview: number; // Content with status='review'
contentPublished: number; // Content with status='published' (approved)
totalContent: number; // All content regardless of status
totalImages: number;
}
export interface ModuleCredits {
// Planner AI function credits
clusteringCredits: number;
ideaGenerationCredits: number;
plannerTotal: number;
// Writer AI function credits
contentGenerationCredits: number;
imageGenerationCredits: number;
writerTotal: number;
}
export interface ModuleStatsData {
planner: PlannerModuleStats;
writer: WriterModuleStats;
credits: ModuleCredits;
loading: boolean;
error: string | null;
}
const defaultStats: ModuleStatsData = {
planner: {
totalKeywords: 0,
keywordsMapped: 0,
totalClusters: 0,
totalIdeas: 0,
ideasConverted: 0,
},
writer: {
totalTasks: 0,
tasksCompleted: 0,
contentDrafts: 0,
contentReview: 0,
contentPublished: 0,
totalContent: 0,
totalImages: 0,
},
credits: {
clusteringCredits: 0,
ideaGenerationCredits: 0,
plannerTotal: 0,
contentGenerationCredits: 0,
imageGenerationCredits: 0,
writerTotal: 0,
},
loading: true,
error: null,
};
export function useModuleStats() {
const [stats, setStats] = useState<ModuleStatsData>(defaultStats);
const { activeSite } = useSiteStore();
const { activeSector } = useSectorStore();
const loadStats = useCallback(async () => {
// Don't load if no active site - wait for site to be set
if (!activeSite?.id) {
setStats(prev => ({ ...prev, loading: false }));
return;
}
setStats(prev => ({ ...prev, loading: true, error: null }));
// Build common filters with explicit site/sector IDs
const baseFilters = {
page_size: 1,
site_id: activeSite.id,
...(activeSector?.id && { sector_id: activeSector.id }),
};
try {
// Fetch all stats in parallel for performance
const [
// Planner stats
keywordsRes,
keywordsMappedRes,
clustersRes,
ideasRes,
ideasQueuedRes,
ideasCompletedRes,
// Writer stats
tasksRes,
tasksCompletedRes,
contentDraftsRes,
contentReviewRes,
contentPublishedRes,
contentTotalRes,
imagesRes,
// Credits from billing summary
creditsRes,
] = await Promise.all([
// Total keywords
fetchKeywords({ ...baseFilters }),
// Keywords that are mapped to clusters
fetchKeywords({ ...baseFilters, status: 'mapped' }),
// Total clusters
fetchClusters({ ...baseFilters }),
// Total ideas
fetchContentIdeas({ ...baseFilters }),
// Ideas that are queued (converted to tasks, waiting)
fetchContentIdeas({ ...baseFilters, status: 'queued' }),
// Ideas that are completed (converted to tasks, done)
fetchContentIdeas({ ...baseFilters, status: 'completed' }),
// Total tasks
fetchTasks({ ...baseFilters }),
// Completed tasks
fetchTasks({ ...baseFilters, status: 'completed' }),
// Content with status='draft'
fetchContent({ ...baseFilters, status: 'draft' }),
// Content with status='review'
fetchContent({ ...baseFilters, status: 'review' }),
// Content with status='published' (approved)
fetchContent({ ...baseFilters, status: 'published' }),
// Total content (all statuses)
fetchContent({ ...baseFilters }),
// Total images
fetchImages({ ...baseFilters }),
// Credits usage from billing summary
fetchAPI('/v1/billing/credits/usage/summary/').catch(() => ({
data: { by_operation: {} }
})),
]);
// Parse credits response
const creditsData = creditsRes?.data || creditsRes || {};
const byOperation = creditsData.by_operation || {};
// Extract credits by operation type
const clusteringCredits = byOperation.clustering?.credits || 0;
const ideaCredits = (byOperation.idea_generation?.credits || 0) + (byOperation.ideas?.credits || 0);
const contentCredits = (byOperation.content_generation?.credits || 0) + (byOperation.content?.credits || 0);
const imageCredits = (byOperation.image_generation?.credits || 0) +
(byOperation.images?.credits || 0) +
(byOperation.image_prompt_extraction?.credits || 0);
// Ideas converted = queued + completed (ideas that have tasks created)
const ideasConverted = (ideasQueuedRes?.count || 0) + (ideasCompletedRes?.count || 0);
setStats({
planner: {
totalKeywords: keywordsRes?.count || 0,
keywordsMapped: keywordsMappedRes?.count || 0,
totalClusters: clustersRes?.count || 0,
totalIdeas: ideasRes?.count || 0,
ideasConverted: ideasConverted,
},
writer: {
totalTasks: tasksRes?.count || 0,
tasksCompleted: tasksCompletedRes?.count || 0,
contentDrafts: contentDraftsRes?.count || 0,
contentReview: contentReviewRes?.count || 0,
contentPublished: contentPublishedRes?.count || 0,
totalContent: contentTotalRes?.count || 0,
totalImages: imagesRes?.count || 0,
},
credits: {
clusteringCredits,
ideaGenerationCredits: ideaCredits,
plannerTotal: clusteringCredits + ideaCredits,
contentGenerationCredits: contentCredits,
imageGenerationCredits: imageCredits,
writerTotal: contentCredits + imageCredits,
},
loading: false,
error: null,
});
} catch (error: any) {
console.error('Error loading module stats:', error);
setStats(prev => ({
...prev,
loading: false,
error: error.message || 'Failed to load module stats',
}));
}
}, [activeSite?.id, activeSector?.id]);
// Load stats on mount and when dependencies change
useEffect(() => {
loadStats();
}, [loadStats]);
// Listen for data changes to refresh
useEffect(() => {
const handleRefresh = () => loadStats();
window.addEventListener('site-changed', handleRefresh);
window.addEventListener('sector-changed', handleRefresh);
return () => {
window.removeEventListener('site-changed', handleRefresh);
window.removeEventListener('sector-changed', handleRefresh);
};
}, [loadStats]);
// Expose refresh function
const refresh = useCallback(() => {
loadStats();
}, [loadStats]);
return { ...stats, refresh };
}
export default useModuleStats;

View File

@@ -1,390 +0,0 @@
/**
* useThreeWidgetFooter - Hook to build ThreeWidgetFooter props
*
* Provides helper functions to construct the three widgets:
* - Page Progress (current page metrics)
* - Module Stats (workflow pipeline)
* - Completion Stats (both modules summary)
*
* Usage:
* const footerProps = useThreeWidgetFooter({
* module: 'planner',
* currentPage: 'keywords',
* plannerData: { keywords: [...], clusters: [...] },
* completionData: { ... }
* });
*/
import { useMemo } from 'react';
import type {
ThreeWidgetFooterProps,
PageProgressWidget,
ModuleStatsWidget,
CompletionWidget,
SubmoduleColor,
} from '../components/dashboard/ThreeWidgetFooter';
// ============================================================================
// DATA INTERFACES
// ============================================================================
interface PlannerPageData {
keywords?: Array<{ cluster_id?: number | null; volume?: number }>;
clusters?: Array<{ ideas_count?: number; keywords_count?: number }>;
ideas?: Array<{ status?: string }>;
totalKeywords?: number;
totalClusters?: number;
totalIdeas?: number;
}
interface WriterPageData {
tasks?: Array<{ status?: string }>;
content?: Array<{ status?: string; has_generated_images?: boolean }>;
totalTasks?: number;
totalContent?: number;
totalPublished?: number;
}
interface CompletionData {
keywordsClustered?: number;
clustersCreated?: number;
ideasGenerated?: number;
contentGenerated?: number;
imagesCreated?: number;
articlesPublished?: number;
creditsUsed?: number;
totalOperations?: number;
}
interface UseThreeWidgetFooterOptions {
module: 'planner' | 'writer';
currentPage: 'keywords' | 'clusters' | 'ideas' | 'tasks' | 'content' | 'images' | 'review' | 'published';
plannerData?: PlannerPageData;
writerData?: WriterPageData;
completionData?: CompletionData;
}
// ============================================================================
// PLANNER PAGE PROGRESS BUILDERS
// ============================================================================
function buildKeywordsPageProgress(data: PlannerPageData): PageProgressWidget {
const keywords = data.keywords || [];
const totalKeywords = data.totalKeywords || keywords.length;
const clusteredCount = keywords.filter(k => k.cluster_id).length;
const unmappedCount = keywords.filter(k => !k.cluster_id).length;
const totalVolume = keywords.reduce((sum, k) => sum + (k.volume || 0), 0);
const clusteredPercent = totalKeywords > 0 ? Math.round((clusteredCount / totalKeywords) * 100) : 0;
return {
title: 'Page Progress',
metrics: [
{ label: 'Keywords', value: totalKeywords },
{ label: 'Clustered', value: clusteredCount, percentage: `${clusteredPercent}%` },
{ label: 'Unmapped', value: unmappedCount },
{ label: 'Volume', value: totalVolume >= 1000 ? `${(totalVolume / 1000).toFixed(1)}K` : totalVolume },
],
progress: {
value: clusteredPercent,
label: `${clusteredPercent}% Clustered`,
color: clusteredPercent >= 80 ? 'green' : 'blue',
},
hint: unmappedCount > 0 ? `${unmappedCount} keywords ready to cluster` : 'All keywords clustered!',
};
}
function buildClustersPageProgress(data: PlannerPageData): PageProgressWidget {
const clusters = data.clusters || [];
const totalClusters = data.totalClusters || clusters.length;
const withIdeas = clusters.filter(c => (c.ideas_count || 0) > 0).length;
const totalKeywords = clusters.reduce((sum, c) => sum + (c.keywords_count || 0), 0);
const readyClusters = clusters.filter(c => (c.ideas_count || 0) === 0).length;
const ideasPercent = totalClusters > 0 ? Math.round((withIdeas / totalClusters) * 100) : 0;
return {
title: 'Page Progress',
metrics: [
{ label: 'Clusters', value: totalClusters },
{ label: 'With Ideas', value: withIdeas, percentage: `${ideasPercent}%` },
{ label: 'Keywords', value: totalKeywords },
{ label: 'Ready', value: readyClusters },
],
progress: {
value: ideasPercent,
label: `${ideasPercent}% Have Ideas`,
color: ideasPercent >= 70 ? 'green' : 'blue',
},
hint: readyClusters > 0 ? `${readyClusters} clusters ready for idea generation` : 'All clusters have ideas!',
};
}
function buildIdeasPageProgress(data: PlannerPageData): PageProgressWidget {
const ideas = data.ideas || [];
const totalIdeas = data.totalIdeas || ideas.length;
const inTasks = ideas.filter(i => i.status === 'completed' || i.status === 'queued').length;
const pending = ideas.filter(i => i.status === 'new').length;
const convertedPercent = totalIdeas > 0 ? Math.round((inTasks / totalIdeas) * 100) : 0;
return {
title: 'Page Progress',
metrics: [
{ label: 'Ideas', value: totalIdeas },
{ label: 'In Tasks', value: inTasks, percentage: `${convertedPercent}%` },
{ label: 'Pending', value: pending },
{ label: 'From Clusters', value: data.totalClusters || 0 },
],
progress: {
value: convertedPercent,
label: `${convertedPercent}% Converted`,
color: convertedPercent >= 60 ? 'green' : 'blue',
},
hint: pending > 0 ? `${pending} ideas ready to become tasks` : 'All ideas converted!',
};
}
// ============================================================================
// WRITER PAGE PROGRESS BUILDERS
// ============================================================================
function buildTasksPageProgress(data: WriterPageData): PageProgressWidget {
const tasks = data.tasks || [];
const total = data.totalTasks || tasks.length;
const completed = tasks.filter(t => t.status === 'completed').length;
const queue = tasks.filter(t => t.status === 'queued').length;
const processing = tasks.filter(t => t.status === 'in_progress').length;
const completedPercent = total > 0 ? Math.round((completed / total) * 100) : 0;
return {
title: 'Page Progress',
metrics: [
{ label: 'Total', value: total },
{ label: 'Complete', value: completed, percentage: `${completedPercent}%` },
{ label: 'Queue', value: queue },
{ label: 'Processing', value: processing },
],
progress: {
value: completedPercent,
label: `${completedPercent}% Generated`,
color: completedPercent >= 60 ? 'green' : 'blue',
},
hint: queue > 0 ? `${queue} tasks in queue for content generation` : 'All tasks processed!',
};
}
function buildContentPageProgress(data: WriterPageData): PageProgressWidget {
const content = data.content || [];
const drafts = content.filter(c => c.status === 'draft').length;
const hasImages = content.filter(c => c.has_generated_images).length;
const ready = content.filter(c => c.status === 'review' || c.status === 'published').length;
const imagesPercent = drafts > 0 ? Math.round((hasImages / drafts) * 100) : 0;
return {
title: 'Page Progress',
metrics: [
{ label: 'Drafts', value: drafts },
{ label: 'Has Images', value: hasImages, percentage: `${imagesPercent}%` },
{ label: 'Total Words', value: '—' }, // Would need word count from API
{ label: 'Ready', value: ready },
],
progress: {
value: imagesPercent,
label: `${imagesPercent}% Have Images`,
color: imagesPercent >= 70 ? 'green' : 'blue',
},
hint: drafts - hasImages > 0 ? `${drafts - hasImages} drafts need images before review` : 'All drafts have images!',
};
}
// ============================================================================
// MODULE STATS BUILDERS
// ============================================================================
function buildPlannerModuleStats(data: PlannerPageData): ModuleStatsWidget {
const keywords = data.keywords || [];
const clusters = data.clusters || [];
const ideas = data.ideas || [];
const totalKeywords = data.totalKeywords || keywords.length;
const totalClusters = data.totalClusters || clusters.length;
const totalIdeas = data.totalIdeas || ideas.length;
const clusteredKeywords = keywords.filter(k => k.cluster_id).length;
const clustersWithIdeas = clusters.filter(c => (c.ideas_count || 0) > 0).length;
const ideasInTasks = ideas.filter(i => i.status === 'completed' || i.status === 'queued').length;
return {
title: 'Planner Module',
pipeline: [
{
fromLabel: 'Keywords',
fromValue: totalKeywords,
toLabel: 'Clusters',
toValue: totalClusters,
actionLabel: 'Auto Cluster',
progress: totalKeywords > 0 ? Math.round((clusteredKeywords / totalKeywords) * 100) : 0,
color: 'blue',
},
{
fromLabel: 'Clusters',
fromValue: totalClusters,
toLabel: 'Ideas',
toValue: totalIdeas,
actionLabel: 'Generate Ideas',
progress: totalClusters > 0 ? Math.round((clustersWithIdeas / totalClusters) * 100) : 0,
color: 'green',
},
{
fromLabel: 'Ideas',
fromValue: totalIdeas,
toLabel: 'Tasks',
toValue: ideasInTasks,
actionLabel: 'Create Tasks',
progress: totalIdeas > 0 ? Math.round((ideasInTasks / totalIdeas) * 100) : 0,
color: 'amber',
},
],
links: [
{ label: 'Keywords', href: '/planner/keywords' },
{ label: 'Clusters', href: '/planner/clusters' },
{ label: 'Ideas', href: '/planner/ideas' },
],
};
}
function buildWriterModuleStats(data: WriterPageData): ModuleStatsWidget {
const tasks = data.tasks || [];
const content = data.content || [];
const totalTasks = data.totalTasks || tasks.length;
const completedTasks = tasks.filter(t => t.status === 'completed').length;
const drafts = content.filter(c => c.status === 'draft').length;
const withImages = content.filter(c => c.has_generated_images).length;
const ready = content.filter(c => c.status === 'review').length;
const published = data.totalPublished || content.filter(c => c.status === 'published').length;
return {
title: 'Writer Module',
pipeline: [
{
fromLabel: 'Tasks',
fromValue: totalTasks,
toLabel: 'Drafts',
toValue: drafts,
actionLabel: 'Generate Content',
progress: totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0,
color: 'blue',
},
{
fromLabel: 'Drafts',
fromValue: drafts,
toLabel: 'Images',
toValue: withImages,
actionLabel: 'Generate Images',
progress: drafts > 0 ? Math.round((withImages / drafts) * 100) : 0,
color: 'purple',
},
{
fromLabel: 'Ready',
fromValue: ready,
toLabel: 'Published',
toValue: published,
actionLabel: 'Review & Publish',
progress: ready > 0 ? Math.round((published / (ready + published)) * 100) : 0,
color: 'green',
},
],
links: [
{ label: 'Tasks', href: '/writer/tasks' },
{ label: 'Content', href: '/writer/content' },
{ label: 'Images', href: '/writer/images' },
{ label: 'Published', href: '/writer/published' },
],
};
}
// ============================================================================
// COMPLETION STATS BUILDER
// ============================================================================
function buildCompletionStats(data: CompletionData): CompletionWidget {
return {
title: 'Workflow Completion',
plannerItems: [
{ label: 'Keywords Clustered', value: data.keywordsClustered || 0, color: 'blue' },
{ label: 'Clusters Created', value: data.clustersCreated || 0, color: 'green' },
{ label: 'Ideas Generated', value: data.ideasGenerated || 0, color: 'amber' },
],
writerItems: [
{ label: 'Content Generated', value: data.contentGenerated || 0, color: 'blue' },
{ label: 'Images Created', value: data.imagesCreated || 0, color: 'purple' },
{ label: 'Articles Published', value: data.articlesPublished || 0, color: 'green' },
],
creditsUsed: data.creditsUsed,
operationsCount: data.totalOperations,
analyticsHref: '/account/usage',
};
}
// ============================================================================
// MAIN HOOK
// ============================================================================
export function useThreeWidgetFooter(options: UseThreeWidgetFooterOptions): ThreeWidgetFooterProps {
const { module, currentPage, plannerData = {}, writerData = {}, completionData = {} } = options;
return useMemo(() => {
// Build page progress based on current page
let pageProgress: PageProgressWidget;
if (module === 'planner') {
switch (currentPage) {
case 'keywords':
pageProgress = buildKeywordsPageProgress(plannerData);
break;
case 'clusters':
pageProgress = buildClustersPageProgress(plannerData);
break;
case 'ideas':
pageProgress = buildIdeasPageProgress(plannerData);
break;
default:
pageProgress = buildKeywordsPageProgress(plannerData);
}
} else {
switch (currentPage) {
case 'tasks':
pageProgress = buildTasksPageProgress(writerData);
break;
case 'content':
case 'images':
case 'review':
pageProgress = buildContentPageProgress(writerData);
break;
default:
pageProgress = buildTasksPageProgress(writerData);
}
}
// Build module stats
const moduleStats = module === 'planner'
? buildPlannerModuleStats(plannerData)
: buildWriterModuleStats(writerData);
// Build completion stats
const completion = buildCompletionStats(completionData);
// Determine submodule color based on current page
let submoduleColor: SubmoduleColor = 'blue';
if (currentPage === 'clusters') submoduleColor = 'green';
if (currentPage === 'ideas') submoduleColor = 'amber';
if (currentPage === 'images') submoduleColor = 'purple';
return {
pageProgress,
moduleStats,
completion,
submoduleColor,
};
}, [module, currentPage, plannerData, writerData, completionData]);
}
export default useThreeWidgetFooter;

View File

@@ -0,0 +1,294 @@
/**
* useWorkflowStats Hook
*
* Centralized hook for fetching workflow statistics across
* Planner and Writer modules with time-based filtering.
*
* This provides consistent data for the WorkflowCompletionWidget
* across all pages.
*
* IMPORTANT: Content table structure
* - Tasks is separate table
* - Content table has status field: 'draft', 'review', 'published' (approved)
* - Images is separate table linked to content
*
* Credits data comes from /v1/billing/credits/usage/summary/ endpoint
* which returns by_operation with operation types:
* - clustering: Keyword Clustering
* - idea_generation: Content Ideas Generation
* - content_generation: Content Generation
* - image_generation: Image Generation
*/
import { useState, useEffect, useCallback } from 'react';
import {
fetchKeywords,
fetchClusters,
fetchContentIdeas,
fetchTasks,
fetchContent,
fetchImages,
fetchAPI,
} from '../services/api';
import { useSiteStore } from '../store/siteStore';
import { useSectorStore } from '../store/sectorStore';
// Time filter options (in days)
export type TimeFilter = 'today' | '7' | '30' | '90' | 'all';
export interface CreditsBreakdown {
clustering: number;
ideaGeneration: number;
contentGeneration: number;
imageGeneration: number;
total: number;
}
export interface WorkflowStats {
// Planner Module Stats
planner: {
keywordsClustered: number;
totalKeywords: number;
clustersCreated: number;
ideasGenerated: number;
};
// Writer Module Stats
writer: {
tasksTotal: number;
contentDrafts: number; // Content with status='draft'
contentReview: number; // Content with status='review'
contentPublished: number; // Content with status='published' (approved)
imagesCreated: number;
};
// Credit consumption stats - detailed breakdown by operation
credits: {
// Planner module credits
plannerCreditsUsed: number; // clustering + idea_generation
clusteringCredits: number; // Just clustering
ideaGenerationCredits: number; // Just idea generation
// Writer module credits
writerCreditsUsed: number; // content_generation + image_generation
contentGenerationCredits: number;
imageGenerationCredits: number;
// Total
totalCreditsUsed: number;
};
// Loading state
loading: boolean;
error: string | null;
}
const defaultStats: WorkflowStats = {
planner: {
keywordsClustered: 0,
totalKeywords: 0,
clustersCreated: 0,
ideasGenerated: 0,
},
writer: {
tasksTotal: 0,
contentDrafts: 0,
contentReview: 0,
contentPublished: 0,
imagesCreated: 0,
},
credits: {
plannerCreditsUsed: 0,
clusteringCredits: 0,
ideaGenerationCredits: 0,
writerCreditsUsed: 0,
contentGenerationCredits: 0,
imageGenerationCredits: 0,
totalCreditsUsed: 0,
},
loading: true,
error: null,
};
// Calculate the date filter based on time range
function getDateFilter(timeFilter: TimeFilter): string | undefined {
if (timeFilter === 'all') return undefined;
const now = new Date();
let startDate: Date;
switch (timeFilter) {
case 'today':
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
break;
case '7':
startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
break;
case '30':
startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
break;
case '90':
startDate = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000);
break;
default:
return undefined;
}
return startDate.toISOString();
}
export function useWorkflowStats(timeFilter: TimeFilter = 'all') {
const [stats, setStats] = useState<WorkflowStats>(defaultStats);
const { activeSite } = useSiteStore();
const { activeSector } = useSectorStore();
const loadStats = useCallback(async () => {
// Don't load if no active site - wait for site to be set
if (!activeSite?.id) {
setStats(prev => ({ ...prev, loading: false }));
return;
}
setStats(prev => ({ ...prev, loading: true, error: null }));
try {
// Build date filter query param
const dateFilter = getDateFilter(timeFilter);
const dateParam = dateFilter ? `&created_at__gte=${dateFilter.split('T')[0]}` : '';
// Build site/sector params for direct API calls
const siteParam = `&site_id=${activeSite.id}`;
const sectorParam = activeSector?.id ? `&sector_id=${activeSector.id}` : '';
const baseParams = `${siteParam}${sectorParam}`;
// Build common filters for fetch* functions
const baseFilters = {
page_size: 1,
site_id: activeSite.id,
...(activeSector?.id && { sector_id: activeSector.id }),
};
// Fetch all stats in parallel for performance
// Note: page_size=1 is used to just get the count, not actual data
const [
// Planner stats - these APIs support created_at__gte filter
keywordsRes,
keywordsClusteredRes,
clustersRes,
ideasRes,
// Writer stats
tasksRes,
contentDraftRes,
contentReviewRes,
contentPublishedRes,
imagesRes,
// Credits stats from billing summary endpoint
creditsRes,
] = await Promise.all([
// Total keywords (with date filter via direct API call)
dateFilter
? fetchAPI(`/v1/planner/keywords/?page_size=1${baseParams}${dateParam}`)
: fetchKeywords({ ...baseFilters }),
// Keywords that are clustered (status='mapped')
dateFilter
? fetchAPI(`/v1/planner/keywords/?page_size=1&status=mapped${baseParams}${dateParam}`)
: fetchKeywords({ ...baseFilters, status: 'mapped' }),
// Total clusters
dateFilter
? fetchAPI(`/v1/planner/clusters/?page_size=1${baseParams}${dateParam}`)
: fetchClusters({ ...baseFilters }),
// Total ideas
dateFilter
? fetchAPI(`/v1/planner/ideas/?page_size=1${baseParams}${dateParam}`)
: fetchContentIdeas({ ...baseFilters }),
// Total tasks
dateFilter
? fetchAPI(`/v1/writer/tasks/?page_size=1${baseParams}${dateParam}`)
: fetchTasks({ ...baseFilters }),
// Content with status='draft'
dateFilter
? fetchAPI(`/v1/writer/content/?page_size=1&status=draft${baseParams}${dateParam}`)
: fetchContent({ ...baseFilters, status: 'draft' }),
// Content with status='review'
dateFilter
? fetchAPI(`/v1/writer/content/?page_size=1&status=review${baseParams}${dateParam}`)
: fetchContent({ ...baseFilters, status: 'review' }),
// Content with status='published' (approved)
dateFilter
? fetchAPI(`/v1/writer/content/?page_size=1&status=published${baseParams}${dateParam}`)
: fetchContent({ ...baseFilters, status: 'published' }),
// Total images
dateFilter
? fetchAPI(`/v1/writer/images/?page_size=1${baseParams}${dateParam}`)
: fetchImages({ ...baseFilters }),
// Credits usage from billing summary endpoint - includes by_operation breakdown
dateFilter
? fetchAPI(`/v1/billing/credits/usage/summary/?start_date=${dateFilter}`)
: fetchAPI('/v1/billing/credits/usage/summary/').catch(() => ({
data: { total_credits_used: 0, by_operation: {} }
})),
]);
// Parse credits response - extract by_operation data
const creditsData = creditsRes?.data || creditsRes || {};
const byOperation = creditsData.by_operation || {};
// Extract credits by operation type
// Planner operations: clustering, idea_generation (also 'ideas' legacy)
const clusteringCredits = byOperation.clustering?.credits || 0;
const ideaCredits = (byOperation.idea_generation?.credits || 0) + (byOperation.ideas?.credits || 0);
// Writer operations: content_generation, image_generation (also 'content', 'images' legacy)
const contentCredits = (byOperation.content_generation?.credits || 0) + (byOperation.content?.credits || 0);
const imageCredits = (byOperation.image_generation?.credits || 0) +
(byOperation.images?.credits || 0) +
(byOperation.image_prompt_extraction?.credits || 0);
const plannerTotal = clusteringCredits + ideaCredits;
const writerTotal = contentCredits + imageCredits;
setStats({
planner: {
totalKeywords: keywordsRes?.count || 0,
keywordsClustered: keywordsClusteredRes?.count || 0,
clustersCreated: clustersRes?.count || 0,
ideasGenerated: ideasRes?.count || 0,
},
writer: {
tasksTotal: tasksRes?.count || 0,
contentDrafts: contentDraftRes?.count || 0,
contentReview: contentReviewRes?.count || 0,
contentPublished: contentPublishedRes?.count || 0,
imagesCreated: imagesRes?.count || 0,
},
credits: {
plannerCreditsUsed: plannerTotal,
clusteringCredits: clusteringCredits,
ideaGenerationCredits: ideaCredits,
writerCreditsUsed: writerTotal,
contentGenerationCredits: contentCredits,
imageGenerationCredits: imageCredits,
totalCreditsUsed: creditsData.total_credits_used || plannerTotal + writerTotal,
},
loading: false,
error: null,
});
} catch (error: any) {
console.error('Error loading workflow stats:', error);
setStats(prev => ({
...prev,
loading: false,
error: error.message || 'Failed to load workflow stats',
}));
}
}, [activeSite?.id, activeSector?.id, timeFilter]);
// Load stats on mount and when dependencies change
useEffect(() => {
loadStats();
}, [loadStats]);
// Expose refresh function
const refresh = useCallback(() => {
loadStats();
}, [loadStats]);
return { ...stats, refresh };
}
export default useWorkflowStats;