267 lines
10 KiB
TypeScript
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 };
|