434 lines
15 KiB
TypeScript
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>
|
|
)}
|
|
</>
|
|
);
|
|
}
|