2
This commit is contained in:
387
frontend/src/components/dashboard/ThreeWidgetFooter.tsx
Normal file
387
frontend/src/components/dashboard/ThreeWidgetFooter.tsx
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
/**
|
||||||
|
* ThreeWidgetFooter - 3-Column Layout for Table Page Footers
|
||||||
|
*
|
||||||
|
* Design from Section 3 of COMPREHENSIVE-AUDIT-REPORT.md:
|
||||||
|
* ┌─────────────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
* │ WIDGET 1: PAGE METRICS │ WIDGET 2: MODULE STATS │ WIDGET 3: COMPLETION │
|
||||||
|
* │ (Current Page Progress) │ (Full Module Overview) │ (Both Modules Stats) │
|
||||||
|
* │ ~33.3% width │ ~33.3% width │ ~33.3% width │
|
||||||
|
* └─────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*
|
||||||
|
* STYLING: Uses CSS tokens from styles/tokens.css:
|
||||||
|
* - --color-primary: Brand blue for primary actions/bars
|
||||||
|
* - --color-success: Green for success states
|
||||||
|
* - --color-warning: Amber for warnings
|
||||||
|
* - --color-purple: Purple accent
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Card } from '../ui/card/Card';
|
||||||
|
import { LightBulbIcon, ChevronRightIcon } from '@heroicons/react/24/solid';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TYPE DEFINITIONS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/** Submodule color type - matches headerMetrics accentColor */
|
||||||
|
export type SubmoduleColor = 'blue' | 'green' | 'amber' | 'purple';
|
||||||
|
|
||||||
|
/** Widget 1: Page Progress - metrics in 2x2 grid + progress bar + hint */
|
||||||
|
export interface PageProgressWidget {
|
||||||
|
title: string;
|
||||||
|
metrics: Array<{ label: string; value: string | number; percentage?: string }>;
|
||||||
|
progress: { value: number; label: string; color?: SubmoduleColor };
|
||||||
|
hint?: string;
|
||||||
|
/** The submodule's accent color - progress bar uses this */
|
||||||
|
submoduleColor?: SubmoduleColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Widget 2: Module Stats - Pipeline flow with arrows and progress bars */
|
||||||
|
export interface ModulePipelineRow {
|
||||||
|
fromLabel: string;
|
||||||
|
fromValue: number;
|
||||||
|
fromHref?: string;
|
||||||
|
actionLabel: string;
|
||||||
|
toLabel: string;
|
||||||
|
toValue: number;
|
||||||
|
toHref?: string;
|
||||||
|
progress: number; // 0-100
|
||||||
|
/** Color for this pipeline row's progress bar */
|
||||||
|
color?: SubmoduleColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModuleStatsWidget {
|
||||||
|
title: string;
|
||||||
|
pipeline: ModulePipelineRow[];
|
||||||
|
links: Array<{ label: string; href: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Widget 3: Completion - Tree structure with bars for both modules */
|
||||||
|
export interface CompletionItem {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
color?: SubmoduleColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompletionWidget {
|
||||||
|
title: string;
|
||||||
|
plannerItems: CompletionItem[];
|
||||||
|
writerItems: CompletionItem[];
|
||||||
|
creditsUsed?: number;
|
||||||
|
operationsCount?: number;
|
||||||
|
analyticsHref?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Main component props */
|
||||||
|
export interface ThreeWidgetFooterProps {
|
||||||
|
pageProgress: PageProgressWidget;
|
||||||
|
moduleStats: ModuleStatsWidget;
|
||||||
|
completion: CompletionWidget;
|
||||||
|
submoduleColor?: SubmoduleColor;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// COLOR UTILITIES
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const getProgressBarStyle = (color: SubmoduleColor = 'blue'): React.CSSProperties => {
|
||||||
|
const colorMap: Record<SubmoduleColor, string> = {
|
||||||
|
blue: 'var(--color-primary)',
|
||||||
|
green: 'var(--color-success)',
|
||||||
|
amber: 'var(--color-warning)',
|
||||||
|
purple: 'var(--color-purple)',
|
||||||
|
};
|
||||||
|
return { backgroundColor: colorMap[color] };
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// WIDGET 1: PAGE PROGRESS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
function PageProgressCard({ widget, submoduleColor = 'blue' }: { widget: PageProgressWidget; submoduleColor?: SubmoduleColor }) {
|
||||||
|
const progressColor = widget.submoduleColor || widget.progress.color || submoduleColor;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-5 bg-white dark:bg-gray-900 border border-[color:var(--color-stroke)] dark:border-gray-700">
|
||||||
|
{/* Header */}
|
||||||
|
<h3 className="text-sm font-semibold text-[color:var(--color-text)] dark:text-gray-200 uppercase tracking-wide mb-4">
|
||||||
|
{widget.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* 2x2 Metrics Grid */}
|
||||||
|
<div className="grid grid-cols-2 gap-x-6 gap-y-3 mb-5">
|
||||||
|
{widget.metrics.slice(0, 4).map((metric, idx) => (
|
||||||
|
<div key={idx} className="flex items-baseline justify-between">
|
||||||
|
<span className="text-sm text-[color:var(--color-text-dim)] dark:text-gray-400">{metric.label}</span>
|
||||||
|
<div className="flex items-baseline gap-1.5">
|
||||||
|
<span className="text-lg font-bold text-[color:var(--color-text)] dark:text-white tabular-nums">
|
||||||
|
{typeof metric.value === 'number' ? metric.value.toLocaleString() : metric.value}
|
||||||
|
</span>
|
||||||
|
{metric.percentage && (
|
||||||
|
<span className="text-xs text-[color:var(--color-text-dim)] dark:text-gray-400">({metric.percentage})</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress Bar */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full transition-all duration-500"
|
||||||
|
style={{
|
||||||
|
...getProgressBarStyle(progressColor),
|
||||||
|
width: `${Math.min(100, Math.max(0, widget.progress.value))}%`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center mt-2">
|
||||||
|
<span className="text-sm font-medium text-[color:var(--color-text)] dark:text-gray-300">{widget.progress.label}</span>
|
||||||
|
<span className="text-sm font-bold text-[color:var(--color-text)] dark:text-white">{widget.progress.value}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hint with icon */}
|
||||||
|
{widget.hint && (
|
||||||
|
<div className="flex items-start gap-2 pt-3 border-t border-[color:var(--color-stroke)] dark:border-gray-800">
|
||||||
|
<LightBulbIcon className="w-5 h-5 flex-shrink-0 mt-0.5 text-warning-500" />
|
||||||
|
<span className="text-sm font-medium text-brand-500">{widget.hint}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// WIDGET 2: MODULE STATS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
function ModuleStatsCard({ widget }: { widget: ModuleStatsWidget }) {
|
||||||
|
return (
|
||||||
|
<Card className="p-5 bg-white dark:bg-gray-900 border border-[color:var(--color-stroke)] dark:border-gray-700">
|
||||||
|
{/* Header */}
|
||||||
|
<h3 className="text-sm font-semibold text-[color:var(--color-text)] dark:text-gray-200 uppercase tracking-wide mb-4">
|
||||||
|
{widget.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Pipeline Rows */}
|
||||||
|
<div className="space-y-4 mb-4">
|
||||||
|
{widget.pipeline.map((row, idx) => (
|
||||||
|
<div key={idx}>
|
||||||
|
{/* Row header: FromLabel Value ► ToLabel Value */}
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
{/* From side */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{row.fromHref ? (
|
||||||
|
<Link
|
||||||
|
to={row.fromHref}
|
||||||
|
className="text-sm font-medium hover:underline text-brand-500"
|
||||||
|
>
|
||||||
|
{row.fromLabel}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm font-medium text-[color:var(--color-text)] dark:text-gray-300">{row.fromLabel}</span>
|
||||||
|
)}
|
||||||
|
<span className="text-lg font-bold tabular-nums text-brand-500">
|
||||||
|
{row.fromValue}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Arrow icon */}
|
||||||
|
<ChevronRightIcon
|
||||||
|
className="w-6 h-6 flex-shrink-0 mx-2 text-brand-500"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* To side */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{row.toHref ? (
|
||||||
|
<Link
|
||||||
|
to={row.toHref}
|
||||||
|
className="text-sm font-medium hover:underline text-brand-500"
|
||||||
|
>
|
||||||
|
{row.toLabel}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm font-medium text-[color:var(--color-text)] dark:text-gray-300">{row.toLabel}</span>
|
||||||
|
)}
|
||||||
|
<span className="text-lg font-bold text-[color:var(--color-text)] dark:text-white tabular-nums">
|
||||||
|
{row.toValue}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress bar */}
|
||||||
|
<div className="h-2.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full transition-all duration-500"
|
||||||
|
style={{
|
||||||
|
...getProgressBarStyle(row.color || 'blue'),
|
||||||
|
width: `${Math.min(100, Math.max(0, row.progress))}%`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation Links */}
|
||||||
|
<div className="flex flex-wrap gap-3 pt-3 border-t border-[color:var(--color-stroke)] dark:border-gray-800">
|
||||||
|
{widget.links.map((link, idx) => (
|
||||||
|
<Link
|
||||||
|
key={idx}
|
||||||
|
to={link.href}
|
||||||
|
className="text-sm font-medium hover:underline flex items-center gap-1 text-brand-500"
|
||||||
|
>
|
||||||
|
<ChevronRightIcon className="w-4 h-4" />
|
||||||
|
<span>{link.label}</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// WIDGET 3: COMPLETION
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
function CompletionCard({ widget }: { widget: CompletionWidget }) {
|
||||||
|
// Calculate max for proportional bars (across both columns)
|
||||||
|
const allValues = [...widget.plannerItems, ...widget.writerItems].map(i => i.value);
|
||||||
|
const maxValue = Math.max(...allValues, 1);
|
||||||
|
|
||||||
|
const renderItem = (item: CompletionItem, isLast: boolean) => {
|
||||||
|
const barWidth = (item.value / maxValue) * 100;
|
||||||
|
const prefix = isLast ? '└─' : '├─';
|
||||||
|
const color = item.color || 'blue';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={item.label} 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">{item.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={{
|
||||||
|
...getProgressBarStyle(color),
|
||||||
|
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">
|
||||||
|
{item.value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-5 bg-white dark:bg-gray-900 border border-[color:var(--color-stroke)] dark:border-gray-700">
|
||||||
|
{/* Header */}
|
||||||
|
<h3 className="text-sm font-semibold text-[color:var(--color-text)] dark:text-gray-200 uppercase tracking-wide mb-4">
|
||||||
|
{widget.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Two-column layout: Planner | Writer */}
|
||||||
|
<div className="grid grid-cols-2 gap-6 mb-4">
|
||||||
|
{/* Planner Column */}
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-bold uppercase tracking-wide mb-2 text-brand-500">
|
||||||
|
Planner
|
||||||
|
</div>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
{widget.plannerItems.map((item, idx) =>
|
||||||
|
renderItem(item, idx === widget.plannerItems.length - 1)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Writer Column */}
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-bold uppercase tracking-wide mb-2 text-success-500">
|
||||||
|
Writer
|
||||||
|
</div>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
{widget.writerItems.map((item, idx) =>
|
||||||
|
renderItem(item, idx === widget.writerItems.length - 1)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer Stats - Credits Used & Operations */}
|
||||||
|
{(widget.creditsUsed !== undefined || widget.operationsCount !== undefined) && (
|
||||||
|
<div className="flex items-center gap-4 pt-3 border-t border-[color:var(--color-stroke)] dark:border-gray-800 text-sm">
|
||||||
|
{widget.creditsUsed !== undefined && (
|
||||||
|
<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">{widget.creditsUsed.toLocaleString()}</strong>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{widget.creditsUsed !== undefined && widget.operationsCount !== undefined && (
|
||||||
|
<span className="text-[color:var(--color-stroke)] dark:text-gray-600">│</span>
|
||||||
|
)}
|
||||||
|
{widget.operationsCount !== undefined && (
|
||||||
|
<span className="text-[color:var(--color-text-dim)] dark:text-gray-400">
|
||||||
|
Operations: <strong className="text-[color:var(--color-text)] dark:text-white font-bold">{widget.operationsCount}</strong>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Analytics Link */}
|
||||||
|
{widget.analyticsHref && (
|
||||||
|
<div className="pt-3 mt-3 border-t border-[color:var(--color-stroke)] dark:border-gray-800">
|
||||||
|
<Link
|
||||||
|
to={widget.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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// MAIN COMPONENT
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export default function ThreeWidgetFooter({
|
||||||
|
pageProgress,
|
||||||
|
moduleStats,
|
||||||
|
completion,
|
||||||
|
submoduleColor = 'blue',
|
||||||
|
className = '',
|
||||||
|
}: ThreeWidgetFooterProps) {
|
||||||
|
return (
|
||||||
|
<div className={`mt-8 pt-6 border-t border-gray-200 dark:border-gray-700 ${className}`}>
|
||||||
|
{/*
|
||||||
|
* Widget widths adjusted per requirements:
|
||||||
|
* - Widget 1 (Page Progress): 28.3% (reduced by 5%)
|
||||||
|
* - Widget 2 (Module Stats): 28.3% (reduced by 5%)
|
||||||
|
* - Widget 3 (Workflow Completion): 43.4% (increased by 10%)
|
||||||
|
*/}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-[28.3%_28.3%_43.4%] gap-4">
|
||||||
|
<PageProgressCard widget={pageProgress} submoduleColor={submoduleColor} />
|
||||||
|
<ModuleStatsCard widget={moduleStats} />
|
||||||
|
<CompletionCard widget={completion} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also export sub-components for flexibility
|
||||||
|
export { PageProgressCard, ModuleStatsCard, CompletionCard };
|
||||||
Reference in New Issue
Block a user