|
|
import React, { useEffect, useState } from 'react'; |
|
|
import { PieChart, Pie, Cell, ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, Legend } from 'recharts'; |
|
|
import { Classification, Innovation, StatsData, ResultResponse } from '../types'; |
|
|
import { COLOR_MAP } from '../constants'; |
|
|
import { backendService } from '../services/backendService'; |
|
|
import InnovationCard from './InnovationCard'; |
|
|
import { CircleDashed } from 'lucide-react'; |
|
|
|
|
|
interface StatsDashboardProps { |
|
|
innovations: Innovation[]; |
|
|
isVisible: boolean; |
|
|
} |
|
|
|
|
|
const StatsDashboard: React.FC<StatsDashboardProps> = ({ isVisible }) => { |
|
|
const [dbInnovations, setDbInnovations] = useState<ResultResponse[]>([]); |
|
|
const [loading, setLoading] = useState(false); |
|
|
|
|
|
|
|
|
const [activeFilters, setActiveFilters] = useState<Classification[]>([ |
|
|
Classification.HIGH, |
|
|
Classification.MEDIUM, |
|
|
Classification.LOW, |
|
|
Classification.UNCLASSIFIED |
|
|
]); |
|
|
|
|
|
|
|
|
const fetchDbInnovations = async () => { |
|
|
setLoading(true); |
|
|
const results = await backendService.fetchClassifiedInnovations(); |
|
|
|
|
|
|
|
|
const transformed: ResultResponse[] = results.map((r: any) => ({ |
|
|
id: r.id, |
|
|
file_name: r.file_name, |
|
|
content: r.content, |
|
|
context: r.context || "N/A", |
|
|
problem: r.problem || "N/A", |
|
|
methodology: r.methodology || "N/A", |
|
|
classification: r.classification, |
|
|
})); |
|
|
|
|
|
setDbInnovations(transformed); |
|
|
setLoading(false); |
|
|
}; |
|
|
|
|
|
useEffect(() => { |
|
|
if (isVisible) { |
|
|
fetchDbInnovations(); |
|
|
} |
|
|
}, [isVisible]); |
|
|
|
|
|
|
|
|
const handleClassify = async (id: string, classification: Classification) => { |
|
|
|
|
|
const inv = dbInnovations.find(i => i.id === Number(id)); |
|
|
if (inv && inv.id) { |
|
|
|
|
|
setDbInnovations(prev => prev.map(i => i.id === Number(id) ? { ...i, classification } : i)); |
|
|
await backendService.saveClassification(Number(id), classification); |
|
|
} |
|
|
}; |
|
|
|
|
|
const toggleFilter = (cls: Classification) => { |
|
|
setActiveFilters(prev => |
|
|
prev.includes(cls) ? prev.filter(c => c !== cls) : [...prev, cls] |
|
|
); |
|
|
}; |
|
|
|
|
|
|
|
|
const displayInnovations = dbInnovations.filter(i => |
|
|
activeFilters.includes(i.classification as Classification) |
|
|
); |
|
|
|
|
|
|
|
|
const statsSource = dbInnovations.length > 0 ? dbInnovations : []; |
|
|
|
|
|
const data: StatsData[] = [ |
|
|
{ name: 'High Priority', value: statsSource.filter(i => i.classification === Classification.HIGH).length, fill: COLOR_MAP[Classification.HIGH] }, |
|
|
{ name: 'Medium Priority', value: statsSource.filter(i => i.classification === Classification.MEDIUM).length, fill: COLOR_MAP[Classification.MEDIUM] }, |
|
|
{ name: 'Low Priority', value: statsSource.filter(i => i.classification === Classification.LOW).length, fill: COLOR_MAP[Classification.LOW] }, |
|
|
{ name: 'Rejected', value: statsSource.filter(i => i.classification === Classification.DELETE).length, fill: COLOR_MAP[Classification.DELETE] }, |
|
|
{ name: 'Unclassified', value: statsSource.filter(i => i.classification === Classification.UNCLASSIFIED).length, fill: COLOR_MAP[Classification.UNCLASSIFIED] }, |
|
|
]; |
|
|
|
|
|
|
|
|
const contextData = [ |
|
|
{ name: 'Network Opt', count: 12 }, |
|
|
{ name: 'Security', count: 8 }, |
|
|
{ name: 'QoS', count: 15 }, |
|
|
{ name: 'New Use Cases', count: 5 }, |
|
|
]; |
|
|
|
|
|
return ( |
|
|
<div className="space-y-8"> |
|
|
{/* Charts Section */} |
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> |
|
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-slate-200"> |
|
|
<h3 className="text-lg font-semibold text-slate-800 mb-4">Classification Status</h3> |
|
|
<div className="h-64"> |
|
|
<ResponsiveContainer width="100%" height="100%"> |
|
|
<PieChart> |
|
|
<Pie |
|
|
data={data} |
|
|
cx="50%" |
|
|
cy="50%" |
|
|
innerRadius={60} |
|
|
outerRadius={80} |
|
|
paddingAngle={5} |
|
|
dataKey="value" |
|
|
> |
|
|
{data.map((entry, index) => ( |
|
|
<Cell key={`cell-${index}`} fill={entry.fill} /> |
|
|
))} |
|
|
</Pie> |
|
|
<Tooltip /> |
|
|
<Legend /> |
|
|
</PieChart> |
|
|
</ResponsiveContainer> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-slate-200"> |
|
|
<h3 className="text-lg font-semibold text-slate-800 mb-4">Innovation Contexts (Trends)</h3> |
|
|
<div className="h-64"> |
|
|
<ResponsiveContainer width="100%" height="100%"> |
|
|
<BarChart data={contextData}> |
|
|
<XAxis dataKey="name" fontSize={12} tickLine={false} axisLine={false} /> |
|
|
<YAxis hide /> |
|
|
<Tooltip cursor={{ fill: 'transparent' }} /> |
|
|
<Bar dataKey="count" fill="#3b82f6" radius={[4, 4, 0, 0]} /> |
|
|
</BarChart> |
|
|
</ResponsiveContainer> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{/* Classified List Section */} |
|
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-slate-200"> |
|
|
<div className="flex flex-col md:flex-row items-center justify-between mb-6 gap-4"> |
|
|
<h3 className="text-lg font-semibold text-slate-800">Classified Innovations</h3> |
|
|
|
|
|
<div className="flex items-center space-x-2 flex-wrap gap-y-2 justify-center"> |
|
|
{[Classification.HIGH, Classification.MEDIUM, Classification.LOW, Classification.UNCLASSIFIED].map(cls => ( |
|
|
<button |
|
|
key={cls} |
|
|
onClick={() => toggleFilter(cls)} |
|
|
className={`px-3 py-1.5 rounded text-xs font-medium border transition-colors flex items-center |
|
|
${activeFilters.includes(cls) |
|
|
? `bg-slate-800 text-white border-slate-800` |
|
|
: `bg-white text-slate-600 border-slate-300 hover:bg-slate-50` |
|
|
}`} |
|
|
> |
|
|
<span |
|
|
className="w-2 h-2 rounded-full mr-1.5" |
|
|
style={{ backgroundColor: COLOR_MAP[cls] }} |
|
|
></span> |
|
|
{cls} |
|
|
</button> |
|
|
))} |
|
|
<button onClick={fetchDbInnovations} className="text-sm text-slate-500 hover:text-blue-600 flex items-center ml-4"> |
|
|
{loading && <CircleDashed className="w-4 h-4 mr-1 animate-spin" />} |
|
|
Refresh |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{displayInnovations.length === 0 ? ( |
|
|
<p className="text-center text-slate-500 py-8">No items (High, Medium, Low, Unclassified) found.</p> |
|
|
) : ( |
|
|
<div className="space-y-4"> |
|
|
{displayInnovations.map(inv => ( |
|
|
<InnovationCard |
|
|
key={inv.id} |
|
|
innovation={inv} |
|
|
onClassify={handleClassify} |
|
|
/> |
|
|
))} |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
export default StatsDashboard; |