From 5a8a97e9b4cc5138baddb4ec886ee463b38fe30a Mon Sep 17 00:00:00 2001 From: guilherme Date: Thu, 11 Dec 2025 11:11:54 -0300 Subject: [PATCH] feat: implement login slide and metrics hooks, add API token handling --- src/App.tsx | 310 +------------- src/app/hooks/useMetrics.ts | 18 + src/app/services/index.ts | 6 + src/views/components/login-slide.tsx | 148 +++++++ src/views/components/retrospective-slides.tsx | 402 ++++++++++++++++++ src/views/components/ui/input.tsx | 21 + 6 files changed, 604 insertions(+), 301 deletions(-) create mode 100644 src/app/hooks/useMetrics.ts create mode 100644 src/views/components/login-slide.tsx create mode 100644 src/views/components/retrospective-slides.tsx create mode 100644 src/views/components/ui/input.tsx diff --git a/src/App.tsx b/src/App.tsx index 1e40497..03936be 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,307 +1,15 @@ -import logoAImg from '@/assets/images/a-agape.png'; -import logoImg from '@/assets/images/agape-logo.png'; -import { motion } from 'framer-motion'; - -import ProfileCard from './views/components/ProfileCard'; - -import { - BuildingIcon, - CalendarIcon, - CertificateIcon, - ClockIcon, -} from '@phosphor-icons/react'; -import { useState } from 'react'; -import { summary } from './views/components/mock/summary'; -import { Navigation } from './views/components/navigation'; -import { StorySlide } from './views/components/story-slide'; -import { Button } from './views/components/ui/button'; -import { Card } from './views/components/ui/card'; - -import { Badge } from './views/components/ui/badge'; +import { Navigate, Route, Routes } from 'react-router-dom'; +import { LoginSlide } from './views/components/login-slide'; +import { RetrospectiveSlides } from './views/components/retrospective-slides'; export function App() { return ( -
- -
+ + } /> + } /> + } /> + ); } -export default function Retrospectiva() { - const total = 6; - const [current, setCurrent] = useState(0); - return ( -
- - -
- -
- - Ágape Logo - - - - Sua Retrospectiva -
- Ágape {summary.year} -
- - - Um ano de conquistas, eficiência e transparência na gestão pública - - - - - - - - Transparência - - Eficiência - - Parceria - -
-
- - -
-
- - - -

- Você utilizou -

- - {summary.numApplicationsUsed} - -

- aplicações Ágape neste ano -

-
- -
- {summary.topApplications.map((app, index) => ( - -

- {app.name} -

-

- {app.uses} usos -

-
- ))} -
-
-
- - -
-
- - - -

- Principais Ações -

-

- As atividades que mais impactaram sua gestão -

-
- -
- {summary.topActions.map((action, index) => ( - - -
-
-
- {index + 1} -
-

- {action.action} -

-
-
- {action.count.toLocaleString('pt-BR')} -
-
-
-
- ))} -
-
-
- - -
- - - - - -
-

- Seu horário preferido -

- - - {summary.favoriteHourRange} - -

- É quando você mais acessa as plataformas Ágape -

-
-
- - - - -

- {summary.badge.title} -

-
- - {summary.badge.subtitle} - -
-

- {summary.badge.description} -

-
-
-
-
- - -
-
- - - - -

- A Ágape deseja a você um {summary.year + 1} repleto de sucesso! -

- -

- {summary.messageFinal} -

- - -

- © {summary.year} {summary.orgName} -

-

O Futuro da Gestão Pública começa aqui!

-
-
- -
-
- console.log('Contact clicked')} - /> -
-
-
-
-
-
- ); -} +export default App; diff --git a/src/app/hooks/useMetrics.ts b/src/app/hooks/useMetrics.ts new file mode 100644 index 0000000..fa8c72a --- /dev/null +++ b/src/app/hooks/useMetrics.ts @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query'; +import { metricsService } from '../services/metrics'; + +export function useMetricsByPortal(cpf: string, ano: number) { + return useQuery({ + queryKey: ['metricsByPortal', cpf, ano], + queryFn: () => metricsService.getMetricsByPortal({ cpf, ano }), + enabled: !!cpf && !!ano, + }); +} + +export function useMetricsBySystems(cpf: string, ano: number) { + return useQuery({ + queryKey: ['metricsBySystems', cpf, ano], + queryFn: () => metricsService.getMetricsBySystems({ cpf, ano }), + enabled: !!cpf && !!ano, + }); +} diff --git a/src/app/services/index.ts b/src/app/services/index.ts index fce3918..806180a 100644 --- a/src/app/services/index.ts +++ b/src/app/services/index.ts @@ -6,6 +6,12 @@ const api = axios.create({ baseURL: import.meta.env.VITE_API_URL, }); +// Adicionar token temporariamente aos headers +const token = import.meta.env.VITE_API_TOKEN || ''; +if (token) { + api.defaults.headers.common['Authorization'] = `Bearer ${token}`; +} + export const queryClient = new QueryClient({ defaultOptions: { queries: { diff --git a/src/views/components/login-slide.tsx b/src/views/components/login-slide.tsx new file mode 100644 index 0000000..f44f9d7 --- /dev/null +++ b/src/views/components/login-slide.tsx @@ -0,0 +1,148 @@ +import logoImg from '@/assets/images/agape-logo.png'; +import { motion } from 'framer-motion'; +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Button } from './ui/button'; +import { Input } from './ui/input'; + +export function LoginSlide() { + const [cpf, setCpf] = useState(''); + const [ano, setAno] = useState(new Date().getFullYear().toString()); + const [errors, setErrors] = useState<{ cpf?: string; ano?: string }>({}); + const navigate = useNavigate(); + + const validateCPF = (cpfValue: string) => { + const cleanCPF = cpfValue.replace(/\D/g, ''); + return cleanCPF.length === 11; + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const newErrors: { cpf?: string; ano?: string } = {}; + + if (!cpf || !validateCPF(cpf)) { + newErrors.cpf = 'CPF inválido (deve ter 11 dígitos)'; + } + + if ( + !ano || + parseInt(ano) < 2020 || + parseInt(ano) > new Date().getFullYear() + ) { + newErrors.ano = `Ano deve estar entre 2020 e ${new Date().getFullYear()}`; + } + + if (Object.keys(newErrors).length > 0) { + setErrors(newErrors); + return; + } + + // Navega para a retrospectiva com os parâmetros + navigate(`/retrospectiva?cpf=${cpf.replace(/\D/g, '')}&ano=${ano}`); + }; + + const formatCPF = (value: string) => { + const cleanValue = value.replace(/\D/g, ''); + if (cleanValue.length <= 11) { + return cleanValue + .replace(/(\d{3})(\d)/, '$1.$2') + .replace(/(\d{3})(\d)/, '$1.$2') + .replace(/(\d{3})(\d{1,2})/, '$1-$2'); + } + return value; + }; + + return ( +
+ +
+ + Ágape Logo + + + +

Sua Retrospectiva

+

Ágape {ano}

+
+
+ + +
+ + { + setCpf(formatCPF(e.target.value)); + if (errors.cpf) { + setErrors({ ...errors, cpf: undefined }); + } + }} + className="bg-white/20 border-white/30 text-white placeholder:text-white/50" + /> + {errors.cpf &&

{errors.cpf}

} +
+ +
+ + { + setAno(e.target.value); + if (errors.ano) { + setErrors({ ...errors, ano: undefined }); + } + }} + className="bg-white/20 border-white/30 text-white" + /> + {errors.ano &&

{errors.ano}

} +
+ + + + +
+ + + Seus dados são seguros e processados com transparência + +
+
+ ); +} diff --git a/src/views/components/retrospective-slides.tsx b/src/views/components/retrospective-slides.tsx new file mode 100644 index 0000000..f63e780 --- /dev/null +++ b/src/views/components/retrospective-slides.tsx @@ -0,0 +1,402 @@ +import { + useMetricsByPortal, + useMetricsBySystems, +} from '@/app/hooks/useMetrics'; +import logoAImg from '@/assets/images/a-agape.png'; +import logoImg from '@/assets/images/agape-logo.png'; +import { + BuildingIcon, + CalendarIcon, + ClockIcon, + SpinnerIcon, +} from '@phosphor-icons/react'; +import { motion } from 'framer-motion'; +import * as React from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { Navigation } from './navigation'; +import ProfileCard from './ProfileCard'; +import { StorySlide } from './story-slide'; + + +export function RetrospectiveSlides() { + const [searchParams] = useSearchParams(); + const cpf = searchParams.get('cpf') || ''; + const ano = searchParams.get('ano') + ? parseInt(searchParams.get('ano')!) + : new Date().getFullYear(); + + const { + data: metricsPortal, + isLoading: isLoadingPortal, + error: errorPortal, + } = useMetricsByPortal(cpf, ano); + + const { + data: metricsSystems, + isLoading: isLoadingSystems, + error: errorSystems, + } = useMetricsBySystems(cpf, ano); + + const [current, setCurrent] = React.useState(0); + + // Calcula slides baseado nos dados disponíveis + const slides: number[] = []; + slides.push(0); // Slide inicial + if (metricsPortal && metricsPortal.length > 0) slides.push(1); // Slide portais + if (metricsSystems && metricsSystems.length > 0) slides.push(2); // Slide sistemas + if ( + (metricsPortal && metricsPortal.length > 0) || + (metricsSystems && metricsSystems.length > 0) + ) { + slides.push(3); // Slide com informações gerais + } + slides.push(4); // Slide final + + const total = slides.length; + + const isLoading = isLoadingPortal || isLoadingSystems; + const hasError = errorPortal || errorSystems; + + // Extrai total de acessos + const totalAcessos = [ + ...(metricsPortal || []), + ...(metricsSystems || []), + ].reduce((acc, item) => acc + item.totalAcessos, 0); + + + // Top sistemas/portais por acessos + const topItems = [...(metricsPortal || []), ...(metricsSystems || [])] + .sort((a, b) => b.totalAcessos - a.totalAcessos) + .slice(0, 5); + + return ( +
+ {isLoading ? ( +
+
+ +

+ Carregando sua retrospectiva... +

+
+
+ ) : hasError ? ( +
+
+

+ Erro ao carregar dados +

+

+ Verifique seu CPF e ano, e tente novamente. +

+
+
+ ) : ( + <> + + +
+ +
+ + Ágape Logo + + + + Sua Retrospectiva +
+ Ágape {ano} +
+ + + Um ano de conquistas, eficiência e transparência na gestão + pública + + + + Seus dados analisados: CPF ••••••••••• + + + + Transparência + + Eficiência + + Parceria + +
+
+ + {metricsPortal && metricsPortal.length > 0 && ( + +
+
+ + + +

+ Seus Portais +

+ + {metricsPortal.length} + +

+ portais acessados neste ano +

+
+ +
+ {metricsPortal.map((portal, index) => ( + +

+ {portal.nomeSistema} +

+

+ Acessos +

+

+ {portal.totalAcessos.toLocaleString('pt-BR')} +

+
+ ))} +
+
+
+ )} + + {metricsSystems && metricsSystems.length > 0 && ( + +
+
+ + + +

+ Seus Sistemas +

+ + {metricsSystems.length} + +

+ sistemas utilizados neste ano +

+
+ +
+ {metricsSystems.map((sistema, index) => ( + +

+ {sistema.nomeSistema} +

+

+ Acessos +

+

+ {sistema.totalAcessos.toLocaleString('pt-BR')} +

+
+ ))} +
+
+
+ )} + + {topItems.length > 0 && ( + +
+ + + + + +
+

+ Seu Engajamento +

+ + + {totalAcessos.toLocaleString('pt-BR')} + +

+ Total de acessos ao longo do ano +

+
+
+ + +

+ Principais Recursos +

+ {topItems.slice(0, 3).map((item, index) => ( + +
+

{item.nomeSistema}

+

+ {item.totalAcessos.toLocaleString('pt-BR')} acessos +

+
+
+ + #{index + 1} + +
+
+ ))} +
+
+
+ )} + +
+
+ + + + +

+ A Ágape deseja a você um {ano + 1} repleto de sucesso! +

+ +

+ Obrigado por fazer parte desta jornada de transformação + digital na gestão pública. Sua dedicação é essencial para + construirmos um serviço público mais eficiente e + transparente. +

+ + +

© {ano} Ágape Sistemas e Tecnologia

+

+ O Futuro da Gestão Pública começa aqui! +

+
+
+ +
+
+ console.log('Contact clicked')} + /> +
+
+
+
+
+ + )} +
+ ); +} diff --git a/src/views/components/ui/input.tsx b/src/views/components/ui/input.tsx new file mode 100644 index 0000000..1cf376f --- /dev/null +++ b/src/views/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; + +import { cn } from '@/app/utils'; + +const Input = React.forwardRef< + HTMLInputElement, + React.InputHTMLAttributes +>(({ className, type, ...props }, ref) => ( + +)); +Input.displayName = 'Input'; + +export { Input };