File size: 7,237 Bytes
4b1a31e 36fa73c 4b1a31e 36fa73c 4b1a31e 36fa73c 4b1a31e 36fa73c 4b1a31e 36fa73c 4b1a31e |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
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);
// Filter Toggle State
const [activeFilters, setActiveFilters] = useState<Classification[]>([
Classification.HIGH,
Classification.MEDIUM,
Classification.LOW,
Classification.UNCLASSIFIED
]);
// Fetch from DB for the "True" state
const fetchDbInnovations = async () => {
setLoading(true);
const results = await backendService.fetchClassifiedInnovations();
// Transform ResultResponse -> Innovation
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) => {
// Find the innovation
const inv = dbInnovations.find(i => i.id === Number(id));
if (inv && inv.id) {
// Optimistic update
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]
);
};
// Filter for Display (High, Medium, Low, Unclassified)
const displayInnovations = dbInnovations.filter(i =>
activeFilters.includes(i.classification as Classification)
);
// Stats Logic - Use DB data for charts to be consistent with the list below
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] },
];
// Mock context data (unchanged)
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; |