Sync: IrisTEA - Plataforma de Chás Premium
This commit is contained in:
321
src/app/dashboard/relatorios/page.tsx
Normal file
321
src/app/dashboard/relatorios/page.tsx
Normal file
@@ -0,0 +1,321 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user