Files
igny8/frontend/src/pages/Dashboard/Home.tsx
2026-01-05 08:17:56 +00:00

434 lines
15 KiB
TypeScript

/**
* Dashboard Home - Compact, information-dense dashboard
* Shows workflow pipeline, quick actions, AI operations, recent activity,
* content velocity, and automation status
*/
import React, { useEffect, useState, useCallback } from "react";
import PageMeta from "../../components/common/PageMeta";
import WorkflowGuide from "../../components/onboarding/WorkflowGuide";
import { useOnboardingStore } from "../../store/onboardingStore";
import { useBillingStore } from "../../store/billingStore";
import { GridIcon, PlusIcon } from "../../icons";
import {
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";
import Button from "../../components/ui/button/Button";
import { useAuthStore } from "../../store/authStore";
import { usePageContext } from "../../context/PageContext";
// Dashboard Widgets
import NeedsAttentionBar, { AttentionItem } from "../../components/dashboard/NeedsAttentionBar";
import WorkflowPipelineWidget, { PipelineData } from "../../components/dashboard/WorkflowPipelineWidget";
import QuickActionsWidget from "../../components/dashboard/QuickActionsWidget";
import AIOperationsWidget, { AIOperationsData } from "../../components/dashboard/AIOperationsWidget";
import RecentActivityWidget, { ActivityItem } from "../../components/dashboard/RecentActivityWidget";
import ContentVelocityWidget, { ContentVelocityData } from "../../components/dashboard/ContentVelocityWidget";
import AutomationStatusWidget, { AutomationData } from "../../components/dashboard/AutomationStatusWidget";
import SitesOverviewWidget from "../../components/dashboard/SitesOverviewWidget";
import CreditsUsageWidget from "../../components/dashboard/CreditsUsageWidget";
import AccountInfoWidget from "../../components/dashboard/AccountInfoWidget";
import { getSubscriptions, Subscription } from "../../services/billing.api";
export default function Home() {
const toast = useToast();
const { activeSite, setActiveSite, loadActiveSite } = useSiteStore();
const { activeSector } = useSectorStore();
const { isGuideDismissed, showGuide, loadFromBackend } = useOnboardingStore();
const { user } = useAuthStore();
const { balance, loadBalance } = useBillingStore();
const { setPageInfo } = usePageContext();
// Core state
const [sites, setSites] = useState<Site[]>([]);
const [sitesLoading, setSitesLoading] = useState(true);
const [siteFilter, setSiteFilter] = useState<'all' | number>('all');
const [showAddSite, setShowAddSite] = useState(false);
const [loading, setLoading] = useState(true);
const [subscription, setSubscription] = useState<Subscription | null>(null);
// Dashboard data state
const [attentionItems, setAttentionItems] = useState<AttentionItem[]>([]);
const [pipelineData, setPipelineData] = useState<PipelineData>({
sites: 0,
keywords: 0,
clusters: 0,
ideas: 0,
tasks: 0,
drafts: 0,
published: 0,
completionPercentage: 0,
});
const [aiOperations, setAIOperations] = useState<AIOperationsData>({
period: '7d',
operations: [
{ type: 'clustering', count: 0, credits: 0 },
{ type: 'ideas', count: 0, credits: 0 },
{ type: 'content', count: 0, credits: 0 },
{ type: 'images', count: 0, credits: 0 },
],
totals: { count: 0, credits: 0, successRate: 100, avgCreditsPerOp: 0 },
});
const [recentActivity, setRecentActivity] = useState<ActivityItem[]>([]);
const [contentVelocity, setContentVelocity] = useState<ContentVelocityData>({
thisWeek: { articles: 0, words: 0, images: 0 },
thisMonth: { articles: 0, words: 0, images: 0 },
total: { articles: 0, words: 0, images: 0 },
trend: 0,
});
const [automationData, setAutomationData] = useState<AutomationData>({
status: 'not_configured',
});
// Plan limits
const maxSites = (user?.account as any)?.plan?.max_sites || (user as any)?.account?.plan?.max_sites || 0;
const canAddMoreSites = maxSites === 0 || sites.length < maxSites;
// Set page info for AppHeader
useEffect(() => {
setPageInfo({
title: 'Dashboard',
badge: { icon: <GridIcon className="w-4 h-4" />, color: 'blue' },
siteFilter: siteFilter,
onSiteFilterChange: (value) => {
setSiteFilter(value);
if (typeof value === 'number') {
const site = sites.find(s => s.id === value);
if (site) {
setActiveSite(site);
}
}
},
});
return () => setPageInfo(null);
}, [setPageInfo, sites, siteFilter, setActiveSite]);
// Load initial data
useEffect(() => {
loadSites();
loadBalance();
loadSubscription();
loadFromBackend().catch(() => {});
}, [loadFromBackend, loadBalance]);
// Load subscription info
const loadSubscription = async () => {
try {
const { results } = await getSubscriptions();
// Get the active subscription
const activeSubscription = results.find(s => s.status === 'active') || results[0] || null;
setSubscription(activeSubscription);
} catch (error) {
console.error('Failed to load subscription:', error);
}
};
// Load active site if not set
useEffect(() => {
if (!activeSite && sites.length > 0) {
loadActiveSite();
}
}, [sites, activeSite, loadActiveSite]);
// Set initial site filter
useEffect(() => {
if (sites.length === 0) {
setSiteFilter('all');
} else if (sites.length === 1 && sites[0]) {
setSiteFilter(sites[0].id);
}
}, [sites.length]);
// Show guide logic
useEffect(() => {
if (sites.length === 0 || showAddSite) {
showGuide();
}
}, [sites.length, showAddSite, showGuide]);
const loadSites = async () => {
try {
setSitesLoading(true);
const response = await fetchSites();
const activeSites = (response.results || []).filter(site => site.is_active);
setSites(activeSites);
} catch (error: any) {
console.error('Failed to load sites:', error);
toast.error(`Failed to load sites: ${error.message}`);
} finally {
setSitesLoading(false);
}
};
const fetchDashboardData = useCallback(async () => {
try {
setLoading(true);
const siteId = siteFilter === 'all' ? undefined : siteFilter;
// Fetch real dashboard stats from API
const stats = await getDashboardStats({
site_id: siteId,
days: 7
});
// Update pipeline data from real API data
const { pipeline, counts } = stats;
const completionPercentage = pipeline.keywords > 0
? Math.round((pipeline.published / pipeline.keywords) * 100)
: 0;
setPipelineData({
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 real data
const attentionList: AttentionItem[] = [];
// Check for sites without sectors
const sitesWithoutSectors = sites.filter(s => !s.active_sectors_count || s.active_sectors_count === 0);
if (sitesWithoutSectors.length > 0) {
attentionList.push({
id: 'setup_incomplete',
type: 'setup_incomplete',
title: 'Setup Incomplete',
description: `${sitesWithoutSectors.length} site(s) need industry & sectors configured`,
actionLabel: 'Complete Setup',
actionHref: '/sites',
});
}
// 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: '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 from real API data
setContentVelocity(stats.content_velocity);
// 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 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: stats.ai_operations.period,
operations: mappedOperations,
totals: stats.ai_operations.totals,
});
// 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 && counts.content.total > 0 ? {
timestamp: new Date(Date.now() - 12 * 60 * 60 * 1000),
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,
});
} catch (error: any) {
if (error?.status === 429) {
setTimeout(() => fetchDashboardData(), 2000);
} else {
console.error('Error fetching dashboard data:', error);
toast.error(`Failed to load dashboard: ${error.message}`);
}
} finally {
setLoading(false);
}
}, [siteFilter, sites, toast]);
// Fetch dashboard data when filter changes
useEffect(() => {
if (!sitesLoading) {
fetchDashboardData();
}
}, [siteFilter, sitesLoading, fetchDashboardData]);
const handleAddSiteClick = () => {
setShowAddSite(true);
showGuide();
};
const handleSiteAdded = () => {
setShowAddSite(false);
loadSites();
fetchDashboardData();
};
const handleDismissAttention = (id: string) => {
setAttentionItems(items => items.filter(item => item.id !== id));
};
const handlePeriodChange = (period: '7d' | '30d' | '90d') => {
setAIOperations(prev => ({ ...prev, period }));
// In real implementation, would refetch data for new period
};
const handleRunAutomation = () => {
toast.info('Starting automation run...');
// In real implementation, would trigger automation via API
};
return (
<>
<PageMeta
title="Dashboard - IGNY8"
description="IGNY8 AI-Powered Content Creation Dashboard"
/>
{/* Welcome/Guide Screen - Shows for new users or when adding site */}
{(sites.length === 0 || showAddSite) && (
<div className="mb-6">
<WorkflowGuide onSiteAdded={handleSiteAdded} />
</div>
)}
{/* Main Dashboard Content */}
{sites.length > 0 && !showAddSite && (
<div className="space-y-5">
{/* Needs Attention Bar */}
<NeedsAttentionBar
items={attentionItems}
onDismiss={handleDismissAttention}
/>
{/* Row 1: Sites Overview + Credits + Account (3 columns) */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-5">
<SitesOverviewWidget
sites={sites}
loading={sitesLoading}
onAddSite={canAddMoreSites ? handleAddSiteClick : undefined}
maxSites={maxSites}
/>
<CreditsUsageWidget
balance={balance}
aiOperations={{
total: aiOperations.totals.count,
period: aiOperations.period === '7d' ? 'Last 7 days' : aiOperations.period === '30d' ? 'Last 30 days' : 'Last 90 days',
}}
loading={loading}
/>
<AccountInfoWidget
balance={balance}
subscription={subscription}
plan={subscription?.plan && typeof subscription.plan === 'object' ? subscription.plan : null}
userPlan={(user?.account as any)?.plan}
loading={loading}
/>
</div>
{/* Row 2: Workflow Pipeline (full width) */}
<WorkflowPipelineWidget data={pipelineData} loading={loading} />
{/* Row 3: Quick Actions (full width) */}
<QuickActionsWidget />
{/* Row 4: AI Operations + Recent Activity */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
<AIOperationsWidget
data={aiOperations}
onPeriodChange={handlePeriodChange}
loading={loading}
/>
<RecentActivityWidget activities={recentActivity} loading={loading} />
</div>
{/* Row 5: Content Velocity + Automation Status */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
<ContentVelocityWidget data={contentVelocity} loading={loading} />
<AutomationStatusWidget
data={automationData}
onRunNow={handleRunAutomation}
loading={loading}
/>
</div>
{/* Add Site Button - Floating */}
{canAddMoreSites && (
<div className="fixed bottom-6 right-6 z-50">
<Button
onClick={handleAddSiteClick}
variant="primary"
size="lg"
startIcon={<PlusIcon className="w-5 h-5" />}
className="shadow-lg hover:shadow-xl transition-shadow"
>
Add Site
</Button>
</div>
)}
</div>
)}
</>
);
}