322 lines
12 KiB
TypeScript
322 lines
12 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import Link from 'next/link';
|
|
import { useSession } from 'next-auth/react';
|
|
import {
|
|
ArrowLeft, FileText, Download, Calendar, TrendingUp,
|
|
BarChart3, PieChart, Loader2
|
|
} from 'lucide-react';
|
|
import {
|
|
LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend,
|
|
ResponsiveContainer, RadarChart, PolarGrid, PolarAngleAxis,
|
|
PolarRadiusAxis, Radar, AreaChart, Area
|
|
} from 'recharts';
|
|
|
|
interface ProgressData {
|
|
progressByCategory: Record<string, { dates: string[]; values: number[] }>;
|
|
currentProgress: { category: string; value: number }[];
|
|
child?: { id: string; name: string };
|
|
}
|
|
|
|
const categoryLabels: Record<string, string> = {
|
|
comunicacao: 'Comunicação',
|
|
habilidades_sociais: 'Habilidades Sociais',
|
|
autonomia: 'Autonomia',
|
|
regulacao_emocional: 'Regulação Emocional',
|
|
};
|
|
|
|
const categoryColors: Record<string, string> = {
|
|
comunicacao: '#3b82f6',
|
|
habilidades_sociais: '#22c55e',
|
|
autonomia: '#eab308',
|
|
regulacao_emocional: '#a855f7',
|
|
};
|
|
|
|
export default function RelatoriosPage() {
|
|
const { data: session } = useSession();
|
|
const [loading, setLoading] = useState(true);
|
|
const [data, setData] = useState<ProgressData | null>(null);
|
|
const [period, setPeriod] = useState<'7' | '30' | '90'>('30');
|
|
|
|
useEffect(() => {
|
|
fetchProgress();
|
|
}, [period]);
|
|
|
|
const fetchProgress = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await fetch(`/api/progress?days=${period}`);
|
|
if (response.ok) {
|
|
const progressData = await response.json();
|
|
setData(progressData);
|
|
}
|
|
} catch (error) {
|
|
console.error('Erro ao buscar progresso:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const generatePDF = async () => {
|
|
// Importar dinamicamente para evitar SSR issues
|
|
const html2canvas = (await import('html2canvas')).default;
|
|
const { jsPDF } = await import('jspdf');
|
|
|
|
const content = document.getElementById('report-content');
|
|
if (!content) return;
|
|
|
|
const canvas = await html2canvas(content, { scale: 2 });
|
|
const imgData = canvas.toDataURL('image/png');
|
|
|
|
const pdf = new jsPDF('p', 'mm', 'a4');
|
|
const imgWidth = 210;
|
|
const pageHeight = 295;
|
|
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
|
let heightLeft = imgHeight;
|
|
let position = 0;
|
|
|
|
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
|
|
heightLeft -= pageHeight;
|
|
|
|
while (heightLeft >= 0) {
|
|
position = heightLeft - imgHeight;
|
|
pdf.addPage();
|
|
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
|
|
heightLeft -= pageHeight;
|
|
}
|
|
|
|
pdf.save(`relatorio-iris-${new Date().toISOString().split('T')[0]}.pdf`);
|
|
};
|
|
|
|
// Preparar dados para gráficos
|
|
const prepareLineChartData = () => {
|
|
if (!data) return [];
|
|
|
|
const allDates = new Set<string>();
|
|
Object.values(data.progressByCategory).forEach(cat => {
|
|
cat.dates.forEach(d => allDates.add(d));
|
|
});
|
|
|
|
const sortedDates = Array.from(allDates).sort();
|
|
|
|
return sortedDates.map(date => {
|
|
const point: Record<string, any> = { date: date.slice(5) }; // MM-DD format
|
|
|
|
Object.entries(data.progressByCategory).forEach(([cat, catData]) => {
|
|
const idx = catData.dates.indexOf(date);
|
|
point[cat] = idx !== -1 ? catData.values[idx] : null;
|
|
});
|
|
|
|
return point;
|
|
});
|
|
};
|
|
|
|
const prepareRadarData = () => {
|
|
if (!data) return [];
|
|
|
|
return data.currentProgress.map(p => ({
|
|
category: categoryLabels[p.category] || p.category,
|
|
value: p.value,
|
|
fullMark: 100,
|
|
}));
|
|
};
|
|
|
|
const lineChartData = prepareLineChartData();
|
|
const radarData = prepareRadarData();
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50">
|
|
{/* Header */}
|
|
<header className="bg-white border-b border-gray-200 px-4 lg:px-8 py-4">
|
|
<div className="max-w-7xl mx-auto flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<Link href="/dashboard" className="text-gray-400 hover:text-gray-600">
|
|
<ArrowLeft className="w-6 h-6" />
|
|
</Link>
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-xl bg-indigo-100 text-indigo-600 flex items-center justify-center">
|
|
<FileText className="w-5 h-5" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-xl font-bold text-gray-900">Relatórios</h1>
|
|
<p className="text-sm text-gray-500">
|
|
{data?.child ? `Progresso de ${data.child.name}` : 'Acompanhamento de progresso'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4">
|
|
{/* Period selector */}
|
|
<div className="flex bg-gray-100 rounded-lg p-1">
|
|
{[
|
|
{ value: '7', label: '7 dias' },
|
|
{ value: '30', label: '30 dias' },
|
|
{ value: '90', label: '90 dias' },
|
|
].map(p => (
|
|
<button
|
|
key={p.value}
|
|
onClick={() => setPeriod(p.value as any)}
|
|
className={`px-3 py-1.5 text-sm rounded-md transition ${
|
|
period === p.value
|
|
? 'bg-white shadow text-indigo-600'
|
|
: 'text-gray-600 hover:text-gray-900'
|
|
}`}
|
|
>
|
|
{p.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<button
|
|
onClick={generatePDF}
|
|
className="flex items-center gap-2 bg-indigo-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-indigo-700 transition"
|
|
>
|
|
<Download className="w-4 h-4" />
|
|
Exportar PDF
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="max-w-7xl mx-auto p-4 lg:p-8" id="report-content">
|
|
{loading ? (
|
|
<div className="flex justify-center py-20">
|
|
<Loader2 className="w-12 h-12 text-indigo-500 animate-spin" />
|
|
</div>
|
|
) : (
|
|
<div className="space-y-8">
|
|
{/* Current Progress Cards */}
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{data?.currentProgress.map(p => (
|
|
<div key={p.category} className="bg-white rounded-xl shadow-sm p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<span className="text-sm font-medium text-gray-500">
|
|
{categoryLabels[p.category]}
|
|
</span>
|
|
<TrendingUp
|
|
className="w-5 h-5"
|
|
style={{ color: categoryColors[p.category] }}
|
|
/>
|
|
</div>
|
|
<div className="text-3xl font-bold text-gray-900 mb-2">
|
|
{p.value}%
|
|
</div>
|
|
<div className="h-2 bg-gray-200 rounded-full">
|
|
<div
|
|
className="h-2 rounded-full transition-all"
|
|
style={{
|
|
width: `${p.value}%`,
|
|
backgroundColor: categoryColors[p.category],
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Charts */}
|
|
<div className="grid lg:grid-cols-2 gap-8">
|
|
{/* Line Chart */}
|
|
<div className="bg-white rounded-xl shadow-sm p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-6 flex items-center gap-2">
|
|
<BarChart3 className="w-5 h-5 text-indigo-600" />
|
|
Evolução ao Longo do Tempo
|
|
</h3>
|
|
|
|
{lineChartData.length > 0 ? (
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<AreaChart data={lineChartData}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis dataKey="date" />
|
|
<YAxis domain={[0, 100]} />
|
|
<Tooltip />
|
|
<Legend />
|
|
{Object.keys(categoryLabels).map(cat => (
|
|
<Area
|
|
key={cat}
|
|
type="monotone"
|
|
dataKey={cat}
|
|
name={categoryLabels[cat]}
|
|
stroke={categoryColors[cat]}
|
|
fill={categoryColors[cat]}
|
|
fillOpacity={0.1}
|
|
connectNulls
|
|
/>
|
|
))}
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
) : (
|
|
<div className="h-[300px] flex items-center justify-center text-gray-500">
|
|
Sem dados para o período selecionado
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Radar Chart */}
|
|
<div className="bg-white rounded-xl shadow-sm p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-6 flex items-center gap-2">
|
|
<PieChart className="w-5 h-5 text-indigo-600" />
|
|
Visão Geral das Áreas
|
|
</h3>
|
|
|
|
{radarData.length > 0 ? (
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<RadarChart data={radarData}>
|
|
<PolarGrid />
|
|
<PolarAngleAxis dataKey="category" />
|
|
<PolarRadiusAxis domain={[0, 100]} />
|
|
<Radar
|
|
name="Progresso"
|
|
dataKey="value"
|
|
stroke="#6366f1"
|
|
fill="#6366f1"
|
|
fillOpacity={0.5}
|
|
/>
|
|
</RadarChart>
|
|
</ResponsiveContainer>
|
|
) : (
|
|
<div className="h-[300px] flex items-center justify-center text-gray-500">
|
|
Sem dados para exibir
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Summary */}
|
|
<div className="bg-white rounded-xl shadow-sm p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
|
Resumo do Período
|
|
</h3>
|
|
<div className="prose prose-sm max-w-none text-gray-600">
|
|
<p>
|
|
Nos últimos <strong>{period} dias</strong>, observamos progresso em todas as áreas de desenvolvimento.
|
|
</p>
|
|
{data?.currentProgress && data.currentProgress.length > 0 && (
|
|
<>
|
|
<p>
|
|
A área com melhor desempenho é <strong>
|
|
{categoryLabels[data.currentProgress.reduce((a, b) =>
|
|
a.value > b.value ? a : b
|
|
).category]}
|
|
</strong> com {Math.max(...data.currentProgress.map(p => p.value))}% de progresso.
|
|
</p>
|
|
<p>
|
|
Recomendamos focar em <strong>
|
|
{categoryLabels[data.currentProgress.reduce((a, b) =>
|
|
a.value < b.value ? a : b
|
|
).category]}
|
|
</strong> nas próximas sessões para equilibrar o desenvolvimento.
|
|
</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|