From 2175458186bd2a2c87342f853378eb7ce7b4beb1 Mon Sep 17 00:00:00 2001 From: Lucas Santana Date: Fri, 7 Feb 2025 11:04:49 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20implementa=20store=20global=20de=20m?= =?UTF-8?q?=C3=A9tricas=20e=20corrige=20processamento=20de=20dados?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adiciona store global usando Zustand para gerenciamento de métricas - Implementa funções específicas para atualização de métricas - Corrige processamento de métricas semanais - Melhora manipulação de estados e performance - Resolve problema de dados vazios nos gráficos --- package-lock.json | 32 ++- package.json | 3 +- src/components/dashboard/MetricsChart.tsx | 35 +-- .../dashboard/WritingMetricsChart.tsx | 8 +- .../StudentDashboardPage.tsx | 265 ++++++++++++++---- src/stores/metricsStore.ts | 106 +++++++ 6 files changed, 365 insertions(+), 84 deletions(-) create mode 100644 src/stores/metricsStore.ts diff --git a/package-lock.json b/package-lock.json index 906dcf9..175e08e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,7 +56,8 @@ "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "uuid": "^11.0.3", - "vitest": "^2.1.8" + "vitest": "^2.1.8", + "zustand": "^5.0.3" }, "devDependencies": { "@eslint/js": "^9.9.1", @@ -11042,6 +11043,35 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zustand": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", + "integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 87d96f7..f7a7efe 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,8 @@ "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "uuid": "^11.0.3", - "vitest": "^2.1.8" + "vitest": "^2.1.8", + "zustand": "^5.0.3" }, "devDependencies": { "@eslint/js": "^9.9.1", diff --git a/src/components/dashboard/MetricsChart.tsx b/src/components/dashboard/MetricsChart.tsx index 14755cc..cba5f32 100644 --- a/src/components/dashboard/MetricsChart.tsx +++ b/src/components/dashboard/MetricsChart.tsx @@ -1,18 +1,7 @@ import React from 'react'; import { Calendar, HelpCircle } from 'lucide-react'; import { Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Bar, ComposedChart } from 'recharts'; - -interface WeeklyMetrics { - week: string; - fluency: number; - pronunciation: number; - accuracy: number; - comprehension: number; - wordsPerMinute: number; - pauses: number; - errors: number; - minutesRead: number; -} +import type { WeeklyReadingMetrics } from '@/types/metrics'; interface MetricConfig { key: string; @@ -44,11 +33,11 @@ const TIME_FILTERS: TimeFilterOption[] = [ ]; interface MetricsChartProps { - data: WeeklyMetrics[]; + data: WeeklyReadingMetrics[]; className?: string; } -export function MetricsChart({ data, className = '' }: MetricsChartProps) { +export function MetricsChart({ data = [], className = '' }: MetricsChartProps) { const [visibleMetrics, setVisibleMetrics] = React.useState>( new Set(METRICS_CONFIG.map(metric => metric.key)) ); @@ -66,7 +55,9 @@ export function MetricsChart({ data, className = '' }: MetricsChartProps) { }); }; - const filterDataByTime = (data: WeeklyMetrics[]): WeeklyMetrics[] => { + const filterDataByTime = (data: WeeklyReadingMetrics[]): WeeklyReadingMetrics[] => { + if (!data || !Array.isArray(data)) return []; + if (timeFilter === 'all') return data; const months = TIME_FILTERS.find(f => f.value === timeFilter)?.months || 12; @@ -74,21 +65,23 @@ export function MetricsChart({ data, className = '' }: MetricsChartProps) { cutoffDate.setMonth(cutoffDate.getMonth() - months); return data.filter(item => { + if (!item?.week) return false; const [year, week] = item.week.split('-W').map(Number); + if (!year || !week) return false; const itemDate = new Date(year, 0, 1 + (week - 1) * 7); return itemDate >= cutoffDate; }); }; - const filteredData = filterDataByTime(data); + const filteredData = React.useMemo(() => filterDataByTime(data), [data, timeFilter]); return (
-

Evolução das Métricas por Semana

-

Acompanhe seu progresso ao longo do tempo

+

Evolução da Leitura por Semana

+

Acompanhe seu progresso na leitura ao longo do tempo

{/* Filtro de Período */} @@ -191,9 +184,7 @@ export function MetricsChart({ data, className = '' }: MetricsChartProps) { accuracy: 'Precisão', comprehension: 'Compreensão', wordsPerMinute: 'Palavras/Min', - pauses: 'Pausas', - errors: 'Erros', - minutesRead: 'Minutos Lidos' + minutesRead: 'Minutos Lendo' }; return [value, metricNames[name] || name]; }} @@ -233,7 +224,7 @@ export function MetricsChart({ data, className = '' }: MetricsChartProps) { >( new Set(METRICS_CONFIG.map(metric => metric.key)) ); @@ -57,6 +57,8 @@ export function WritingMetricsChart({ data, className = '' }: WritingMetricsChar }; const filterDataByTime = (data: WeeklyWritingMetrics[]): WeeklyWritingMetrics[] => { + if (!data || !Array.isArray(data)) return []; + if (timeFilter === 'all') return data; const months = TIME_FILTERS.find(f => f.value === timeFilter)?.months || 12; @@ -64,13 +66,15 @@ export function WritingMetricsChart({ data, className = '' }: WritingMetricsChar cutoffDate.setMonth(cutoffDate.getMonth() - months); return data.filter(item => { + if (!item?.week) return false; const [year, week] = item.week.split('-W').map(Number); + if (!year || !week) return false; const itemDate = new Date(year, 0, 1 + (week - 1) * 7); return itemDate >= cutoffDate; }); }; - const filteredData = filterDataByTime(data); + const filteredData = React.useMemo(() => filterDataByTime(data), [data, timeFilter]); return (
diff --git a/src/pages/student-dashboard/StudentDashboardPage.tsx b/src/pages/student-dashboard/StudentDashboardPage.tsx index 1b3c365..3ac787d 100644 --- a/src/pages/student-dashboard/StudentDashboardPage.tsx +++ b/src/pages/student-dashboard/StudentDashboardPage.tsx @@ -7,6 +7,7 @@ import { MetricsChart } from '@/components/dashboard/MetricsChart'; import { DashboardMetrics } from '@/components/dashboard/DashboardMetrics'; import { WritingMetricsSection } from '@/components/dashboard/WritingMetricsSection'; import { WritingMetricsChart } from '@/components/dashboard/WritingMetricsChart'; +import { useMetricsStore } from '@/stores/metricsStore'; import type { DashboardMetrics as DashboardMetricsType, DashboardWeeklyMetrics, @@ -49,41 +50,64 @@ interface WeeklyData { minutesRead: number; } +interface EssayAnalysis { + id: string; + created_at: string; + overall_score: number; + suggestions: string; + essay_analysis_scores: Array<{ + adequacy: number; + coherence: number; + cohesion: number; + vocabulary: number; + grammar: number; + }>; + essay_analysis_feedback: Array<{ + structure_feedback: string; + content_feedback: string; + language_feedback: string; + }>; +} + +interface ProcessedEssayAnalysis { + id: string; + created_at: string; + overall_score: number; + essay_id: string; + scores: { + adequacy: number; + coherence: number; + cohesion: number; + vocabulary: number; + grammar: number; + }; + feedback: { + structure_feedback: string; + content_feedback: string; + language_feedback: string; + }; +} + export function StudentDashboardPage() { const navigate = useNavigate(); const [student, setStudent] = React.useState(null); - const [metrics, setMetrics] = React.useState({ - reading: { - totalStories: 0, - averageReadingFluency: 0, - totalReadingTime: 0, - currentLevel: 1, - averagePronunciation: 0, - averageAccuracy: 0, - averageComprehension: 0, - averageWordsPerMinute: 0, - averagePauses: 0, - averageErrors: 0 - }, - writing: { - totalEssays: 0, - averageScore: 0, - totalEssaysTime: 0, - currentWritingLevel: 1, - averageAdequacy: 0, - averageCoherence: 0, - averageCohesion: 0, - averageVocabulary: 0, - averageGrammar: 0 - } - }); - const [weeklyMetrics, setWeeklyMetrics] = React.useState({ - reading: [], - writing: [] - }); - const [loading, setLoading] = React.useState(true); - const [error, setError] = React.useState(null); const [recentStories, setRecentStories] = React.useState([]); + + const { + metrics, + weeklyMetrics, + loading, + error, + setMetrics, + setWeeklyMetrics, + updateReadingMetrics, + updateWritingMetrics, + updateWeeklyReadingMetrics, + updateWeeklyWritingMetrics, + setLoading, + setError, + resetMetrics + } = useMetricsStore(); const processWeeklyMetrics = (recordings: Recording[]) => { const weeklyData = recordings.reduce((acc: { [key: string]: WeeklyData }, recording) => { @@ -132,9 +156,54 @@ export function StudentDashboardPage() { .sort((a, b) => a.week.localeCompare(b.week)); }; + const processWeeklyWritingMetrics = (analyses: ProcessedEssayAnalysis[]) => { + const weeklyData = analyses.reduce((acc: { [key: string]: any }, analysis) => { + const date = new Date(analysis.created_at); + const week = `${date.getFullYear()}-W${Math.ceil((date.getDate() + date.getDay()) / 7)}`; + + if (!acc[week]) { + acc[week] = { + count: 0, + score: 0, + adequacy: 0, + coherence: 0, + cohesion: 0, + vocabulary: 0, + grammar: 0, + minutesWriting: 0 + }; + } + + acc[week].count += 1; + acc[week].score += analysis.overall_score; + acc[week].adequacy += analysis.scores.adequacy; + acc[week].coherence += analysis.scores.coherence; + acc[week].cohesion += analysis.scores.cohesion; + acc[week].vocabulary += analysis.scores.vocabulary; + acc[week].grammar += analysis.scores.grammar; + acc[week].minutesWriting += 30; // Tempo médio estimado por redação + + return acc; + }, {}); + + return Object.entries(weeklyData) + .map(([week, data]: [string, any]) => ({ + week, + score: Math.round(data.score / data.count), + adequacy: Math.round(data.adequacy / data.count), + coherence: Math.round(data.coherence / data.count), + cohesion: Math.round(data.cohesion / data.count), + vocabulary: Math.round(data.vocabulary / data.count), + grammar: Math.round(data.grammar / data.count), + minutesWriting: data.minutesWriting + })) + .sort((a, b) => a.week.localeCompare(b.week)); + }; + React.useEffect(() => { const fetchDashboardData = async () => { try { + setLoading(true); const { data: { session } } = await supabase.auth.getSession(); if (!session?.user?.id) return; @@ -175,10 +244,8 @@ export function StudentDashboardPage() { // Processar métricas semanais const weeklyData = processWeeklyMetrics(recordings); - setWeeklyMetrics({ - reading: weeklyData, - writing: [] - }); + // Atualizar métricas semanais de leitura + updateWeeklyReadingMetrics(weeklyData); // Buscar histórias recentes com a capa definida const { data: stories, error: storiesError } = await supabase @@ -237,31 +304,108 @@ export function StudentDashboardPage() { errors: 0 }); - // Calcular médias - setMetrics({ - reading: { - totalStories: allStoriesData.length, - averageReadingFluency: Math.round(metricsSum.fluency / totalRecordings), - totalReadingTime: recordings.length * 2, - currentLevel: Math.ceil(metricsSum.fluency / (totalRecordings * 20)), - averagePronunciation: Math.round(metricsSum.pronunciation / totalRecordings), - averageAccuracy: Math.round(metricsSum.accuracy / totalRecordings), - averageComprehension: Math.round(metricsSum.comprehension / totalRecordings), - averageWordsPerMinute: Math.round(metricsSum.wordsPerMinute / totalRecordings), - averagePauses: Math.round(metricsSum.pauses / totalRecordings), - averageErrors: Math.round(metricsSum.errors / totalRecordings) + // Atualizar métricas de leitura + updateReadingMetrics({ + totalStories: allStoriesData.length, + averageReadingFluency: Math.round(metricsSum.fluency / totalRecordings), + totalReadingTime: recordings.length * 2, + currentLevel: Math.ceil(metricsSum.fluency / (totalRecordings * 20)), + averagePronunciation: Math.round(metricsSum.pronunciation / totalRecordings), + averageAccuracy: Math.round(metricsSum.accuracy / totalRecordings), + averageComprehension: Math.round(metricsSum.comprehension / totalRecordings), + averageWordsPerMinute: Math.round(metricsSum.wordsPerMinute / totalRecordings), + averagePauses: Math.round(metricsSum.pauses / totalRecordings), + averageErrors: Math.round(metricsSum.errors / totalRecordings) + }); + } + + // Buscar todas as redações do aluno + const { data: essays, error: essaysError } = await supabase + .from('student_essays') + .select(` + id, + created_at, + status, + essay_analyses( + id, + overall_score, + suggestions, + created_at, + essay_analysis_scores( + adequacy, + coherence, + cohesion, + vocabulary, + grammar + ), + essay_analysis_feedback( + structure_feedback, + content_feedback, + language_feedback + ) + ) + `) + .eq('student_id', session.user.id) + .eq('status', 'completed'); + + if (essaysError) throw essaysError; + + // Processar métricas semanais de escrita + const analyses = essays?.flatMap(essay => + essay.essay_analyses?.map(analysis => ({ + id: analysis.id, + created_at: analysis.created_at, + overall_score: analysis.overall_score, + essay_id: essay.id, + scores: analysis.essay_analysis_scores?.[0] || { + adequacy: 0, + coherence: 0, + cohesion: 0, + vocabulary: 0, + grammar: 0 }, - writing: { - totalEssays: 0, - averageScore: 0, - totalEssaysTime: 0, - currentWritingLevel: 1, - averageAdequacy: 0, - averageCoherence: 0, - averageCohesion: 0, - averageVocabulary: 0, - averageGrammar: 0 + feedback: analysis.essay_analysis_feedback?.[0] || { + structure_feedback: '', + content_feedback: '', + language_feedback: '' } + })) + ).filter(Boolean) || []; + + const weeklyWritingData = processWeeklyWritingMetrics(analyses); + // Atualizar métricas semanais de escrita + updateWeeklyWritingMetrics(weeklyWritingData); + + // Calcular métricas gerais de escrita + if (analyses && analyses.length > 0) { + const totalAnalyses = analyses.length; + const metricsSum = analyses.reduce((acc, analysis) => ({ + score: acc.score + analysis.overall_score, + adequacy: acc.adequacy + analysis.scores.adequacy, + coherence: acc.coherence + analysis.scores.coherence, + cohesion: acc.cohesion + analysis.scores.cohesion, + vocabulary: acc.vocabulary + analysis.scores.vocabulary, + grammar: acc.grammar + analysis.scores.grammar + }), { + score: 0, + adequacy: 0, + coherence: 0, + cohesion: 0, + vocabulary: 0, + grammar: 0 + }); + + // Atualizar métricas de escrita + updateWritingMetrics({ + totalEssays: essays?.length || 0, + averageScore: Math.round(metricsSum.score / totalAnalyses), + totalEssaysTime: totalAnalyses * 30, + currentWritingLevel: Math.ceil(metricsSum.score / (totalAnalyses * 20)), + averageAdequacy: Math.round(metricsSum.adequacy / totalAnalyses), + averageCoherence: Math.round(metricsSum.coherence / totalAnalyses), + averageCohesion: Math.round(metricsSum.cohesion / totalAnalyses), + averageVocabulary: Math.round(metricsSum.vocabulary / totalAnalyses), + averageGrammar: Math.round(metricsSum.grammar / totalAnalyses) }); } @@ -274,6 +418,11 @@ export function StudentDashboardPage() { }; fetchDashboardData(); + + // Limpar métricas ao desmontar + return () => { + resetMetrics(); + }; }, []); if (loading) { diff --git a/src/stores/metricsStore.ts b/src/stores/metricsStore.ts new file mode 100644 index 0000000..a48cff6 --- /dev/null +++ b/src/stores/metricsStore.ts @@ -0,0 +1,106 @@ +import { create } from 'zustand'; +import type { + DashboardMetrics, + DashboardWeeklyMetrics, + ReadingMetrics, + WritingMetrics, + WeeklyReadingMetrics, + WeeklyWritingMetrics +} from '@/types/metrics'; + +interface MetricsState { + metrics: DashboardMetrics; + weeklyMetrics: DashboardWeeklyMetrics; + loading: boolean; + error: string | null; + setMetrics: (metrics: DashboardMetrics) => void; + setWeeklyMetrics: (weeklyMetrics: DashboardWeeklyMetrics) => void; + updateReadingMetrics: (readingMetrics: ReadingMetrics) => void; + updateWritingMetrics: (writingMetrics: WritingMetrics) => void; + updateWeeklyReadingMetrics: (weeklyReadingMetrics: WeeklyReadingMetrics[]) => void; + updateWeeklyWritingMetrics: (weeklyWritingMetrics: WeeklyWritingMetrics[]) => void; + setLoading: (loading: boolean) => void; + setError: (error: string | null) => void; + resetMetrics: () => void; +} + +const initialMetrics: DashboardMetrics = { + reading: { + totalStories: 0, + averageReadingFluency: 0, + totalReadingTime: 0, + currentLevel: 1, + averagePronunciation: 0, + averageAccuracy: 0, + averageComprehension: 0, + averageWordsPerMinute: 0, + averagePauses: 0, + averageErrors: 0 + }, + writing: { + totalEssays: 0, + averageScore: 0, + totalEssaysTime: 0, + currentWritingLevel: 1, + averageAdequacy: 0, + averageCoherence: 0, + averageCohesion: 0, + averageVocabulary: 0, + averageGrammar: 0 + } +}; + +const initialWeeklyMetrics: DashboardWeeklyMetrics = { + reading: [], + writing: [] +}; + +export const useMetricsStore = create((set) => ({ + metrics: initialMetrics, + weeklyMetrics: initialWeeklyMetrics, + loading: true, + error: null, + + setMetrics: (metrics) => set({ metrics }), + + setWeeklyMetrics: (weeklyMetrics) => set({ weeklyMetrics }), + + updateReadingMetrics: (readingMetrics) => set((state) => ({ + metrics: { + ...state.metrics, + reading: readingMetrics + } + })), + + updateWritingMetrics: (writingMetrics) => set((state) => ({ + metrics: { + ...state.metrics, + writing: writingMetrics + } + })), + + updateWeeklyReadingMetrics: (weeklyReadingMetrics) => set((state) => ({ + weeklyMetrics: { + ...state.weeklyMetrics, + reading: weeklyReadingMetrics + } + })), + + updateWeeklyWritingMetrics: (weeklyWritingMetrics) => set((state) => ({ + weeklyMetrics: { + ...state.weeklyMetrics, + writing: weeklyWritingMetrics + } + })), + + setLoading: (loading) => set({ loading }), + + setError: (error) => set({ error }), + + resetMetrics: () => set({ + metrics: initialMetrics, + weeklyMetrics: initialWeeklyMetrics, + loading: false, + error: null + }) +})); \ No newline at end of file