Files
igny8/frontend/src/components/dashboard/WorkflowCompletionWidget.tsx
2026-01-01 21:42:04 +00:00

267 lines
10 KiB
TypeScript

/**
* WorkflowCompletionWidget - Standardized Workflow Stats Widget
*
* A centralized widget component that displays consistent workflow completion
* stats across all Planner and Writer module pages.
*
* Features:
* - Unified data fetching via useWorkflowStats hook
* - Time-based filtering (Today, 7, 30, 90 days)
* - Credits consumption display
* - Consistent appearance across all pages
*
* IMPORTANT: Uses "Content Pages" not "Articles" per design requirements
*/
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { Card } from '../ui/card/Card';
import Button from '../ui/button/Button';
import { ChevronRightIcon } from '@heroicons/react/24/solid';
import { useWorkflowStats, TimeFilter } from '../../hooks/useWorkflowStats';
import { WORKFLOW_COLORS } from '../../config/colors.config';
// ============================================================================
// TYPE DEFINITIONS
// ============================================================================
export type SubmoduleColor = 'blue' | 'green' | 'amber' | 'purple';
export interface WorkflowCompletionWidgetProps {
/** Show credits consumption section */
showCredits?: boolean;
/** Link to analytics page */
analyticsHref?: string;
/** Additional CSS classes */
className?: string;
}
// ============================================================================
// TIME FILTER COMPONENT
// ============================================================================
interface TimeFilterProps {
value: TimeFilter;
onChange: (value: TimeFilter) => void;
}
function TimeFilterButtons({ value, onChange }: TimeFilterProps) {
const options: { value: TimeFilter; label: string }[] = [
{ value: 'today', label: 'Today' },
{ value: '7', label: '7d' },
{ value: '30', label: '30d' },
{ value: '90', label: '90d' },
];
return (
<div className="flex items-center gap-1 bg-gray-100 dark:bg-gray-800 rounded-lg p-0.5">
{options.map((option) => (
<Button
key={option.value}
onClick={() => onChange(option.value)}
variant={value === option.value ? 'primary' : 'ghost'}
tone={value === option.value ? 'brand' : 'neutral'}
size="xs"
>
{option.label}
</Button>
))}
</div>
);
}
// ============================================================================
// COMPLETION ITEM COMPONENT
// ============================================================================
interface CompletionItemProps {
label: string;
value: number;
barColor: string;
maxValue: number;
isLast: boolean;
}
function CompletionItem({ label, value, barColor, maxValue, isLast }: CompletionItemProps) {
const barWidth = maxValue > 0 ? (value / maxValue) * 100 : 0;
const prefix = isLast ? '└─' : '├─';
return (
<div className="flex items-center gap-2 py-1">
{/* Tree prefix */}
<span className="text-[color:var(--color-text-dim)] dark:text-gray-500 font-mono text-xs w-5 flex-shrink-0">
{prefix}
</span>
{/* Label */}
<span className="text-sm text-[color:var(--color-text)] dark:text-gray-300 flex-1 truncate">
{label}
</span>
{/* Progress bar */}
<div className="w-16 h-2.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden flex-shrink-0">
<div
className="h-full rounded-full transition-all duration-500"
style={{
backgroundColor: barColor,
width: `${Math.min(100, barWidth)}%`
}}
/>
</div>
{/* Value */}
<span className="text-sm font-bold text-[color:var(--color-text)] dark:text-white tabular-nums w-10 text-right flex-shrink-0">
{value.toLocaleString()}
</span>
</div>
);
}
// ============================================================================
// MAIN COMPONENT
// ============================================================================
export default function WorkflowCompletionWidget({
showCredits = true,
analyticsHref = '/account/usage',
className = '',
}: WorkflowCompletionWidgetProps) {
const [timeFilter, setTimeFilter] = useState<TimeFilter>('30');
const { planner, writer, credits, loading } = useWorkflowStats(timeFilter);
// Define planner items with unified colors from config
// WORKFLOW_COLORS contains CSS variable names, so wrap with var()
const plannerItems = [
{ label: 'Keywords Clustered', value: planner.keywordsClustered, barColor: `var(${WORKFLOW_COLORS.planner.keywordsClustered})` },
{ label: 'Clusters Created', value: planner.clustersCreated, barColor: `var(${WORKFLOW_COLORS.planner.clustersCreated})` },
{ label: 'Ideas Generated', value: planner.ideasGenerated, barColor: `var(${WORKFLOW_COLORS.planner.ideasGenerated})` },
];
// Define writer items - using "Content Pages" not "Articles"
// Total content = drafts + review + approved + published
const totalContent = writer.contentDrafts + writer.contentReview + writer.contentPublished;
const writerItems = [
{ label: 'Content Pages', value: totalContent, barColor: `var(${WORKFLOW_COLORS.writer.contentPages})` },
{ label: 'Images Created', value: writer.imagesCreated, barColor: `var(${WORKFLOW_COLORS.writer.imagesCreated})` },
{ label: 'Pages Published', value: writer.contentPublished, barColor: `var(${WORKFLOW_COLORS.writer.pagesPublished})` },
];
// Calculate max value for proportional bars (across both columns)
const allValues = [...plannerItems, ...writerItems].map(i => i.value);
const maxValue = Math.max(...allValues, 1);
return (
<Card className={`p-5 bg-white dark:bg-gray-900 border border-[color:var(--color-stroke)] dark:border-gray-700 min-w-0 ${className}`}>
{/* Header with Time Filter */}
<div className="flex items-center justify-between mb-4 gap-2">
<h3 className="text-sm font-semibold text-[color:var(--color-text)] dark:text-gray-200 uppercase tracking-wide truncate">
Workflow Completion
</h3>
<TimeFilterButtons value={timeFilter} onChange={setTimeFilter} />
</div>
{/* Loading State */}
{loading ? (
<div className="flex items-center justify-center py-8">
<div className="animate-pulse flex flex-col items-center gap-2">
<div className="w-8 h-8 rounded-full bg-gray-200 dark:bg-gray-700" />
<div className="text-sm text-gray-400">Loading stats...</div>
</div>
</div>
) : (
<>
{/* Two-column layout: Planner | Writer */}
<div className="grid grid-cols-2 gap-4 mb-4">
{/* Planner Column */}
<div className="min-w-0">
<div className="text-xs font-bold uppercase tracking-wide mb-2" style={{ color: 'var(--color-primary)' }}>
Planner
</div>
<div className="space-y-0.5">
{plannerItems.map((item, idx) => (
<CompletionItem
key={item.label}
label={item.label}
value={item.value}
barColor={item.barColor}
maxValue={maxValue}
isLast={idx === plannerItems.length - 1}
/>
))}
</div>
</div>
{/* Writer Column */}
<div className="min-w-0">
<div className="text-xs font-bold uppercase tracking-wide mb-2" style={{ color: 'var(--color-success)' }}>
Writer
</div>
<div className="space-y-0.5">
{writerItems.map((item, idx) => (
<CompletionItem
key={item.label}
label={item.label}
value={item.value}
barColor={item.barColor}
maxValue={maxValue}
isLast={idx === writerItems.length - 1}
/>
))}
</div>
</div>
</div>
{/* Credits Used Section - Always show */}
{showCredits && (
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 pt-3 border-t border-[color:var(--color-stroke)] dark:border-gray-800 text-sm">
<span className="text-[color:var(--color-text-dim)] dark:text-gray-400">
Credits Used:{' '}
<strong className="text-[color:var(--color-text)] dark:text-white font-bold">
{credits.totalCreditsUsed.toLocaleString()}
</strong>
</span>
{credits.plannerCreditsUsed > 0 && (
<>
<span className="text-[color:var(--color-stroke)] dark:text-gray-600"></span>
<span className="text-[color:var(--color-text-dim)] dark:text-gray-400">
Planner:{' '}
<strong className="font-bold" style={{ color: 'var(--color-primary)' }}>
{credits.plannerCreditsUsed.toLocaleString()}
</strong>
</span>
</>
)}
{credits.writerCreditsUsed > 0 && (
<>
<span className="text-[color:var(--color-stroke)] dark:text-gray-600"></span>
<span className="text-[color:var(--color-text-dim)] dark:text-gray-400">
Writer:{' '}
<strong className="font-bold" style={{ color: 'var(--color-success)' }}>
{credits.writerCreditsUsed.toLocaleString()}
</strong>
</span>
</>
)}
</div>
)}
{/* Analytics Link */}
{analyticsHref && (
<div className="pt-3 mt-3 border-t border-[color:var(--color-stroke)] dark:border-gray-800">
<Link
to={analyticsHref}
className="text-sm font-medium hover:underline flex items-center gap-1 text-brand-500"
>
View Full Analytics
<ChevronRightIcon className="w-4 h-4" />
</Link>
</div>
)}
</>
)}
</Card>
);
}
export { WorkflowCompletionWidget };