Compare commits

...

10 Commits

16 changed files with 532 additions and 450 deletions

View File

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="pt-br">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />

50
package-lock.json generated
View File

@ -24,6 +24,7 @@
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"framer-motion": "^12.23.26", "framer-motion": "^12.23.26",
"html2canvas": "^1.4.1",
"lucide-react": "^0.554.0", "lucide-react": "^0.554.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-day-picker": "^9.11.1", "react-day-picker": "^9.11.1",
@ -3487,6 +3488,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.8.29", "version": "2.8.29",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.29.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.29.tgz",
@ -3728,6 +3738,15 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@ -4692,6 +4711,19 @@
"hermes-estree": "0.25.1" "hermes-estree": "0.25.1"
} }
}, },
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -5944,6 +5976,15 @@
"url": "https://opencollective.com/webpack" "url": "https://opencollective.com/webpack"
} }
}, },
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/tiny-invariant": { "node_modules/tiny-invariant": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@ -6181,6 +6222,15 @@
} }
} }
}, },
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/vaul": { "node_modules/vaul": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",

View File

@ -26,6 +26,7 @@
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"framer-motion": "^12.23.26", "framer-motion": "^12.23.26",
"html2canvas": "^1.4.1",
"lucide-react": "^0.554.0", "lucide-react": "^0.554.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-day-picker": "^9.11.1", "react-day-picker": "^9.11.1",

View File

@ -1,12 +1,10 @@
import { Navigate, Route, Routes } from 'react-router-dom'; import { Navigate, Route, Routes } from 'react-router-dom';
import { LoginSlide } from './views/components/login-slide';
import { RetrospectiveSlides } from './views/components/retrospective-slides'; import { RetrospectiveSlides } from './views/components/retrospective-slides';
export function App() { export function App() {
return ( return (
<Routes> <Routes>
<Route path="/" element={<LoginSlide />} /> <Route path="/:token" element={<RetrospectiveSlides />} />
<Route path="/retrospectiva" element={<RetrospectiveSlides />} />
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Routes> </Routes>
); );

View File

@ -0,0 +1,3 @@
export const localStorageKeys = {
ACCESS_TOKEN: '@review:token',
};

View File

@ -0,0 +1,78 @@
/* eslint-disable react-refresh/only-export-components */
import { api } from '@/app/services';
import { createContext, useCallback, useContext, useState } from 'react';
import { localStorageKeys } from '../config/local-storage-keys';
type AuthState = {
token: string;
};
type AuthContextValue = {
signedIn: boolean;
authenticate(token: string): void;
signOut(): void;
};
export const AuthContext = createContext({} as AuthContextValue);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [isSignedIn, setIsSignedIn] = useState<boolean>(false);
const [, setAuthState] = useState<AuthState>(() => {
const token = localStorage.getItem(localStorageKeys.ACCESS_TOKEN);
if (token) {
api.defaults.headers.Authorization = `Bearer ${token}`;
setIsSignedIn(true);
return { token };
}
return {} as AuthState;
});
const authenticate = useCallback((token: string) => {
try {
localStorage.setItem(localStorageKeys.ACCESS_TOKEN, token);
api.defaults.headers.Authorization = `Bearer ${token}`;
setIsSignedIn(true);
setAuthState({ token });
} catch (error) {
console.error(error);
}
}, []);
const signOut = useCallback(() => {
localStorage.removeItem(localStorageKeys.ACCESS_TOKEN);
setIsSignedIn(false);
setAuthState({} as AuthState);
api.defaults.headers.Authorization = '';
api.defaults.headers['Cache-Control'] = 'no-cache';
}, []);
return (
<AuthContext.Provider
value={{
signedIn: isSignedIn,
authenticate,
signOut,
}}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}

View File

@ -1,18 +1,18 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { metricsService } from '../services/metrics'; import { metricsService } from '../services/metrics';
export function useMetricsByPortal(cpf: string, ano: number) { export function useMetricsByPortal(ano: number) {
return useQuery({ return useQuery({
queryKey: ['metricsByPortal', cpf, ano], queryKey: ['metricsByPortal', ano],
queryFn: () => metricsService.getMetricsByPortal({ cpf, ano }), queryFn: () => metricsService.getMetricsByPortal({ ano }),
enabled: !!cpf && !!ano, enabled: !!ano,
}); });
} }
export function useMetricsBySystems(cpf: string, ano: number) { export function useMetricsBySystems(ano: number) {
return useQuery({ return useQuery({
queryKey: ['metricsBySystems', cpf, ano], queryKey: ['metricsBySystems', ano],
queryFn: () => metricsService.getMetricsBySystems({ cpf, ano }), queryFn: () => metricsService.getMetricsBySystems({ ano }),
enabled: !!cpf && !!ano, enabled: !!ano,
}); });
} }

View File

@ -6,10 +6,12 @@ const api = axios.create({
baseURL: import.meta.env.VITE_API_URL, baseURL: import.meta.env.VITE_API_URL,
}); });
// Adicionar token temporariamente aos headers export function setAuthToken(newToken: string | null) {
const token = import.meta.env.VITE_API_TOKEN || ''; if (newToken) {
if (token) { api.defaults.headers.common['Authorization'] = `Bearer ${newToken}`;
api.defaults.headers.common['Authorization'] = `Bearer ${token}`; } else {
delete api.defaults.headers.common['Authorization'];
}
} }
export const queryClient = new QueryClient({ export const queryClient = new QueryClient({

View File

@ -4,19 +4,18 @@ type MetricsByPortalProps = {
nomeSistema: string; nomeSistema: string;
totalAcessos: number; totalAcessos: number;
horasLogadas: number; horasLogadas: number;
nomeUsuario: string;
}; };
type GetMetricsByPortalParams = { type GetMetricsByPortalParams = {
ano: number; ano: number;
cpf: string;
}; };
export async function getMetricsByPortal({ export async function getMetricsByPortal({
ano, ano,
cpf,
}: GetMetricsByPortalParams): Promise<MetricsByPortalProps[]> { }: GetMetricsByPortalParams): Promise<MetricsByPortalProps[]> {
const { data } = await api.get( const { data } = await api.get(
`/userAccessMetricsByPortal?ano=${ano}&cpf=${cpf}`, `/userAccessMetricsByPortal?ano=${ano}`,
); );
return data; return data;

View File

@ -8,16 +8,12 @@ type MetricsBySystemsProps = {
type GetMetricsBySystemsParams = { type GetMetricsBySystemsParams = {
ano: number; ano: number;
cpf: string;
}; };
export async function getMetricsBySystems({ export async function getMetricsBySystems({
ano, ano,
cpf,
}: GetMetricsBySystemsParams): Promise<MetricsBySystemsProps[]> { }: GetMetricsBySystemsParams): Promise<MetricsBySystemsProps[]> {
const { data } = await api.get( const { data } = await api.get(`/userAccessMetricsBySystem?ano=${ano}`);
`/userAccessMetricsBySystem?ano=${ano}&cpf=${cpf}`,
);
return data; return data;
} }

View File

@ -14,13 +14,16 @@ import { ptBR } from 'date-fns/locale';
setDefaultOptions({ locale: ptBR }); setDefaultOptions({ locale: ptBR });
import '@/styles/index.css'; import '@/styles/index.css';
import { AuthProvider } from './app/hooks/use-auth';
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<BrowserRouter> <BrowserRouter>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<StrictMode> <AuthProvider>
<App /> <StrictMode>
</StrictMode> <App />
</StrictMode>
</AuthProvider>
</QueryClientProvider> </QueryClientProvider>
</BrowserRouter>, </BrowserRouter>,
); );

View File

@ -117,7 +117,7 @@
--popover-foreground: oklch(0.145 0 0); --popover-foreground: oklch(0.145 0 0);
--primary: #0e233d; --primary: #0e233d;
--primary-foreground: oklch(0.985 0 0); --primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0); --secondary: #febb01;
--secondary-foreground: oklch(0.205 0 0); --secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0); --muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0); --muted-foreground: oklch(0.556 0 0);

View File

@ -379,7 +379,6 @@
.pc-details p { .pc-details p {
font-weight: 600; font-weight: 600;
position: relative; position: relative;
top: -12px;
white-space: nowrap; white-space: nowrap;
font-size: 16px; font-size: 16px;
margin: 0 auto; margin: 0 auto;
@ -601,8 +600,12 @@
} }
.metric-content { .metric-content {
flex: 1; display: flex;
min-width: 0; justify-content: center;
align-items: center;
flex-direction: column;
width: 100%;
border-radius: 0px;
} }
.metric-value { .metric-value {
@ -612,8 +615,6 @@
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
line-height: 1;
margin-bottom: 4px;
} }
.metric-label { .metric-label {

View File

@ -1,4 +1,10 @@
import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import './ProfileCard.css'; import './ProfileCard.css';
import logoImg from '@/assets/images/agape-logo.png'; import logoImg from '@/assets/images/agape-logo.png';
@ -39,7 +45,15 @@ const ProfileCardComponent = ({
contactText = 'Contact', contactText = 'Contact',
showUserInfo = true, showUserInfo = true,
onContactClick, onContactClick,
// métricas para popular o cartão
totalAcessos = null,
aplicacoesCount = null,
topAppName = null,
topAppAcessos = null,
// permite mostrar/ocultar botões de compartilhamento
showShareButtons = true,
}) => { }) => {
const [sharing, setSharing] = useState(false);
const wrapRef = useRef(null); const wrapRef = useRef(null);
const shellRef = useRef(null); const shellRef = useRef(null);
@ -346,30 +360,31 @@ const ProfileCardComponent = ({
gap: '16px', gap: '16px',
padding: '24px', padding: '24px',
}}> }}>
{/* Card de Acessos */} {/* Card de Acessos (dinâmico) */}
<div className="metric-card bg-gradient-to-br from-purple-900/30 to-blue-900/30"> <div className="metric-card bg-gradient-to-br from-purple-900/30 to-blue-900/30">
<div className="metric-content"> <div className="metric-content">
<h4 className="metric-value">23.343</h4> <h4 className="metric-value">
{totalAcessos != null
? totalAcessos.toLocaleString('pt-BR')
: '—'}
</h4>
<p className="metric-label">Acessos</p> <p className="metric-label">Acessos</p>
</div> </div>
</div> </div>
<div className="metric-card bg-gradient-to-br from-blue-900/30 to-cyan-900/30"> <div className="metric-card bg-gradient-to-br from-blue-900/30 to-cyan-900/30">
<div className="metric-content"> <div className="metric-content">
<h4 className="metric-value">8</h4> <h4 className="metric-value">
{aplicacoesCount != null ? aplicacoesCount : '—'}
</h4>
<p className="metric-label">Aplicações</p> <p className="metric-label">Aplicações</p>
</div> </div>
</div> </div>
<div className="metric-card-wide bg-gradient-to-br from-indigo-900/30 to-purple-900/30"> <div className="metric-card bg-gradient-to-br from-blue-900/30 to-cyan-900/30">
<div className="metric-content-wide"> <div className="metric-content">
<div className="flex items-center gap-2"> <h4 className="metric-value">{topAppName || '—'}</h4>
<span className="top-badge">TOP 1</span> <p className="metric-label">Aplicação mais utilizada</p>
<h4 className="metric-app-name">agGestor</h4>
</div>
<p className="metric-app-desc">
Aplicação mais utilizada
</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,97 +0,0 @@
export const summary = {
year: 2025,
orgName: 'Ágape Sistemas e Tecnologia',
numApplicationsUsed: 42,
messageFinal:
'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.',
topApplications: [
{
name: 'Portal do Servidor',
uses: 156,
},
{
name: 'Sistema de Protocolo',
uses: 89,
},
{
name: 'Gestão de Processos',
uses: 67,
},
{
name: 'Controle de Ponto',
uses: 54,
},
{
name: 'Licitações Online',
uses: 42,
},
],
distributionByDepartment: [
{
name: 'Saúde',
value: 28,
},
{
name: 'Educação',
value: 22,
},
{
name: 'Administração',
value: 18,
},
{
name: 'Finanças',
value: 16,
},
{
name: 'Infraestrutura',
value: 12,
},
{
name: 'Outros',
value: 4,
},
],
topActions: [
{
action: 'Protocolo de Documentos',
count: 156,
},
{
action: 'Análise de Processos',
count: 134,
},
{
action: 'Solicitação de Aquisições',
count: 98,
},
{
action: 'Controle de Ponto',
count: 87,
},
{
action: 'Gestão de Licitações',
count: 76,
},
{
action: 'Relatórios Gerenciais',
count: 65,
},
],
favoriteHourRange: '09:00 - 11:00',
badge: {
title: 'Agente da Eficiência',
subtitle: 'Nível Ouro',
description:
'Você se destacou pela consistência e otimização no uso das ferramentas Ágape, contribuindo significativamente para a melhoria dos processos da gestão pública.',
},
// Opcional: Estatísticas adicionais
totalDocumentsProcessed: 2456,
averageProcessingTime: '2.3 dias',
efficiencyGain: '37%',
};

View File

@ -1,74 +1,89 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { useAuth } from '@/app/hooks/use-auth';
import { import {
useMetricsByPortal, useMetricsByPortal,
useMetricsBySystems, useMetricsBySystems,
} from '@/app/hooks/useMetrics'; } from '@/app/hooks/useMetrics';
import logoAImg from '@/assets/images/a-agape.png'; import logoAImg from '@/assets/images/a-agape.png';
import logoImg from '@/assets/images/agape-logo.png'; import logoImg from '@/assets/images/agape-logo.png';
import { import { ChartLineUpIcon, SpinnerIcon } from '@phosphor-icons/react';
BuildingIcon,
CalendarIcon,
ClockIcon,
SpinnerIcon,
} from '@phosphor-icons/react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import * as React from 'react'; import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { Navigation } from './navigation';
import ProfileCard from './ProfileCard'; import ProfileCard from './ProfileCard';
import { StorySlide } from './story-slide'; import { StorySlide } from './story-slide';
export function RetrospectiveSlides() { export function RetrospectiveSlides() {
const [searchParams] = useSearchParams(); const { token } = useParams();
const cpf = searchParams.get('cpf') || ''; const { authenticate } = useAuth();
const ano = searchParams.get('ano')
? parseInt(searchParams.get('ano')!) useEffect(() => {
: new Date().getFullYear(); if (token) {
authenticate(token);
}
}, [token, authenticate]);
const year = new Date().getFullYear();
if (!token) {
return (
<div className="h-screen w-full bg-black flex items-center justify-center">
<div className="text-center space-y-4 max-w-md">
<p className="text-white text-lg font-semibold">
Token inválido ou ausente
</p>
<p className="text-white/70">
Esta aplicação deve ser acessada com o parâmetro <code>token</code>{' '}
na URL. Exemplo: <code>?token=SEU_TOKEN&amp;cpf=00000000000</code>
</p>
</div>
</div>
);
}
const { const {
data: metricsPortal, data: metricsPortal,
isLoading: isLoadingPortal, isLoading: isLoadingPortal,
error: errorPortal, error: errorPortal,
} = useMetricsByPortal(cpf, ano); } = useMetricsByPortal(year);
const { const {
data: metricsSystems, data: metricsSystems,
isLoading: isLoadingSystems, isLoading: isLoadingSystems,
error: errorSystems, error: errorSystems,
} = useMetricsBySystems(cpf, ano); } = useMetricsBySystems(year);
const [current, setCurrent] = React.useState(0);
// Calcula slides baseado nos dados disponíveis
const slides: number[] = []; const slides: number[] = [];
slides.push(0); // Slide inicial slides.push(0);
if (metricsPortal && metricsPortal.length > 0) slides.push(1); // Slide portais if (metricsPortal && metricsPortal.length > 0) slides.push(1);
if (metricsSystems && metricsSystems.length > 0) slides.push(2); // Slide sistemas
if ( if (
(metricsPortal && metricsPortal.length > 0) || (metricsPortal && metricsPortal.length > 0) ||
(metricsSystems && metricsSystems.length > 0) (metricsSystems && metricsSystems.length > 0)
) { ) {
slides.push(3); // Slide com informações gerais slides.push(2);
} }
slides.push(4); // Slide final slides.push(3);
const total = slides.length;
const isLoading = isLoadingPortal || isLoadingSystems; const isLoading = isLoadingPortal || isLoadingSystems;
const hasError = errorPortal || errorSystems; const hasError = errorPortal || errorSystems;
// Extrai total de acessos
const totalAcessos = [ const totalAcessos = [
...(metricsPortal || []), ...(metricsPortal || []),
...(metricsSystems || []), ...(metricsSystems || []),
].reduce((acc, item) => acc + item.totalAcessos, 0); ].reduce((acc, item) => acc + item.totalAcessos, 0);
// Top sistemas/portais por acessos
const topItems = [...(metricsPortal || []), ...(metricsSystems || [])] const topItems = [...(metricsPortal || []), ...(metricsSystems || [])]
.filter((item) => (item.nomeSistema || '').toLowerCase() !== 'agportal')
.sort((a, b) => b.totalAcessos - a.totalAcessos) .sort((a, b) => b.totalAcessos - a.totalAcessos)
.slice(0, 5); .slice(0, 5);
const allItems = [...(metricsPortal || []), ...(metricsSystems || [])];
const aplicacoesCount = new Set(
allItems.map((i) => (i.nomeSistema || '').trim()).filter((n) => !!n),
).size;
const topAppName = topItems[0]?.nomeSistema ?? null;
const topAppAcessos = topItems[0]?.totalAcessos ?? null;
return ( return (
<div className="w-full h-screen overflow-hidden"> <div className="w-full h-screen overflow-hidden">
{isLoading ? ( {isLoading ? (
@ -93,303 +108,321 @@ export function RetrospectiveSlides() {
</div> </div>
) : ( ) : (
<> <>
<Navigation total={total} current={current} setCurrent={setCurrent} />
<div className="h-full snap-y snap-mandatory overflow-y-scroll scroll-smooth"> <div className="h-full snap-y snap-mandatory overflow-y-scroll scroll-smooth">
<StorySlide className="bg-linear-to-br from-[#0e233d] via-[#173b63] to-[#145190] text-white"> <StorySlide className="bg-linear-to-br from-[#0e233d] via-[#173b63] to-[#145190] text-white">
<div className="text-center space-y-8"> <div className="max-w-7xl mx-auto w-full px-6">
<motion.div <div className="text-center space-y-8">
initial={{ scale: 0.8, opacity: 0 }} <motion.div
whileInView={{ scale: 1, opacity: 1 }} initial={{ scale: 0.8, opacity: 0 }}
viewport={{ once: true }} whileInView={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.6 }}> viewport={{ once: true }}
<img transition={{ duration: 0.6 }}>
src={logoImg} <img
alt="Ágape Logo" src={logoImg}
className="mx-auto w-52 mb-6" alt="Ágape Logo"
/> className="mx-auto w-52 mb-6"
</motion.div> />
</motion.div>
<motion.h1 <motion.h1
className="text-6xl md:text-7xl font-medium mb-4" className="text-5xl md:text-6xl font-medium mb-4"
initial={{ y: 20, opacity: 0 }} initial={{ y: 20, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }} whileInView={{ y: 0, opacity: 1 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }}> transition={{ duration: 0.6, delay: 0.2 }}>
Sua Retrospectiva {metricsPortal && metricsPortal[0]?.nomeUsuario ? (
<br /> <>
Ágape {ano} Olá,{' '}
</motion.h1> <span className="text-secondary">
{metricsPortal[0].nomeUsuario.split(' ')[0]}
</span>
!
<br />
Bem-vindo à sua Retrospectiva Ágape {year}
</>
) : (
<>
Retrospectiva Ágape {year}
</>
)}
</motion.h1>
<motion.p <motion.p
className="text-xl md:text-2xl text-white/90 max-w-2xl mx-auto font-light" className="text-xl md:text-2xl text-white/90 max-w-2xl mx-auto font-light"
initial={{ y: 20, opacity: 0 }} initial={{ y: 20, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }} whileInView={{ y: 0, opacity: 1 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.4 }}> transition={{ duration: 0.6, delay: 0.4 }}>
Um ano de conquistas, eficiência e transparência na gestão Um ano de conquistas, eficiência e transparência na gestão
pública pública
</motion.p> </motion.p>
<motion.p <motion.div
className="text-lg text-white/70" className="flex items-center justify-center gap-4 text-sm text-white/70 pt-8"
initial={{ y: 20, opacity: 0 }} initial={{ opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }} whileInView={{ opacity: 1 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.5 }}> transition={{ duration: 0.6, delay: 0.8 }}>
Seus dados analisados: CPF <span>Transparência</span>
</motion.p> <span></span>
<span>Eficiência</span>
<motion.div <span></span>
className="flex items-center justify-center gap-4 text-sm text-white/70 pt-8" <span>Parceria</span>
initial={{ opacity: 0 }} </motion.div>
whileInView={{ opacity: 1 }} </div>
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.8 }}>
<span>Transparência</span>
<span></span>
<span>Eficiência</span>
<span></span>
<span>Parceria</span>
</motion.div>
</div> </div>
</StorySlide> </StorySlide>
{metricsPortal && metricsPortal.length > 0 && ( {metricsPortal && metricsPortal.length > 0 && (
<StorySlide className="bg-linear-to-br from-slate-50 to-blue-50"> <>
<div className="space-y-8"> <StorySlide className="bg-linear-to-br from-slate-50 to-blue-50">
<div className="text-center space-y-4"> <div className="max-w-7xl mx-auto w-full h-full px-6 py-8 md:py-16 relative z-10">
<motion.div <div className="flex flex-col lg:flex-row items-center justify-between h-full">
initial={{ scale: 0 }}
whileInView={{ scale: 1 }}
viewport={{ once: true }}
transition={{ type: 'spring', duration: 0.6 }}>
<BuildingIcon className="h-16 w-16 mx-auto text-[#0e233d]" />
</motion.div>
<h2 className="text-4xl md:text-5xl font-bold text-[#0e233d]">
Seus Portais
</h2>
<motion.div
className="text-7xl md:text-8xl font-bold text-[#145190]"
initial={{ scale: 0.5, opacity: 0 }}
whileInView={{ scale: 1, opacity: 1 }}
viewport={{ once: true }}
transition={{
type: 'spring',
duration: 0.8,
delay: 0.2,
}}>
{metricsPortal.length}
</motion.div>
<p className="text-2xl text-muted-foreground">
portais acessados neste ano
</p>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 mt-12">
{metricsPortal.map((portal, index) => (
<motion.div <motion.div
key={index} className="lg:w-1/2 flex flex-col items-start mb-10 lg:mb-0 space-y-8 lg:pr-12"
className="p-6 bg-white rounded-lg shadow hover:shadow-md transition-shadow" initial={{ opacity: 0, x: -30 }}
initial={{ y: 20, opacity: 0 }} whileInView={{ opacity: 1, x: 0 }}
whileInView={{ y: 0, opacity: 1 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}> transition={{ duration: 0.6 }}>
<h3 className="text-xl font-semibold text-[#0e233d] truncate"> <motion.div
{portal.nomeSistema} initial={{ scale: 0 }}
</h3> whileInView={{ scale: 1 }}
<p className="text-sm text-muted-foreground mt-1"> viewport={{ once: true }}
Acessos transition={{ type: 'spring', duration: 0.6 }}>
</p> <img src={logoAImg} className="w-10" />
<p className="text-3xl font-bold text-[#145190] mt-2"> </motion.div>
{portal.totalAcessos.toLocaleString('pt-BR')}
</p>
</motion.div>
))}
</div>
</div>
</StorySlide>
)}
{metricsSystems && metricsSystems.length > 0 && ( <motion.h2
<StorySlide className="bg-linear-to-br from-slate-50 to-blue-50"> className="text-3xl md:text-4xl lg:text-5xl font-bold text-gray-900 mb-4"
<div className="space-y-8"> initial={{ opacity: 0, y: -20 }}
<div className="text-center space-y-4"> whileInView={{ opacity: 1, y: 0 }}
<motion.div viewport={{ once: true }}
initial={{ scale: 0 }} transition={{ duration: 0.6, delay: 0.1 }}>
whileInView={{ scale: 1 }} <span className="text-[#0e233d mt-2">
viewport={{ once: true }} AgPortal <br />
transition={{ type: 'spring', duration: 0.6 }}> Sua Porta de Entrada Digital
<CalendarIcon className="h-16 w-16 mx-auto text-[#0e233d]" />
</motion.div>
<h2 className="text-4xl md:text-5xl font-bold text-[#0e233d]">
Seus Sistemas
</h2>
<motion.div
className="text-7xl md:text-8xl font-bold text-[#145190]"
initial={{ scale: 0.5, opacity: 0 }}
whileInView={{ scale: 1, opacity: 1 }}
viewport={{ once: true }}
transition={{
type: 'spring',
duration: 0.8,
delay: 0.2,
}}>
{metricsSystems.length}
</motion.div>
<p className="text-2xl text-muted-foreground">
sistemas utilizados neste ano
</p>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 mt-12">
{metricsSystems.map((sistema, index) => (
<motion.div
key={index}
className="p-6 bg-white rounded-lg shadow hover:shadow-md transition-shadow"
initial={{ y: 20, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}>
<h3 className="text-xl font-semibold text-[#0e233d] truncate">
{sistema.nomeSistema}
</h3>
<p className="text-sm text-muted-foreground mt-1">
Acessos
</p>
<p className="text-3xl font-bold text-[#145190] mt-2">
{sistema.totalAcessos.toLocaleString('pt-BR')}
</p>
</motion.div>
))}
</div>
</div>
</StorySlide>
)}
{topItems.length > 0 && (
<StorySlide className="bg-linear-to-br from-[#0e233d] via-[#173b63] to-[#145190] text-white">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center min-h-screen p-8">
<motion.div
className="space-y-8 text-center lg:text-left"
initial={{ x: -50, opacity: 0 }}
whileInView={{ x: 0, opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}>
<motion.div
initial={{ scale: 0 }}
whileInView={{ scale: 1 }}
viewport={{ once: true }}
transition={{ type: 'spring', duration: 0.6 }}>
<ClockIcon className="h-24 w-24 mb-8 text-yellow-300 lg:mx-0" />
</motion.div>
<div className="space-y-6">
<h2 className="text-4xl md:text-5xl font-bold">
Seu Engajamento
</h2>
<motion.div
className="text-6xl md:text-7xl font-bold text-yellow-300"
initial={{ scale: 0.5, opacity: 0 }}
whileInView={{ scale: 1, opacity: 1 }}
viewport={{ once: true }}
transition={{
type: 'spring',
duration: 0.8,
delay: 0.2,
}}>
{totalAcessos.toLocaleString('pt-BR')}
</motion.div>
<p className="text-xl text-white/90 max-w-lg">
Total de acessos ao longo do ano
</p>
</div>
</motion.div>
<motion.div
className="space-y-4"
initial={{ x: 50, opacity: 0 }}
whileInView={{ x: 0, opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }}>
<h3 className="text-2xl font-bold text-white mb-6">
Principais Recursos
</h3>
{topItems.slice(0, 3).map((item, index) => (
<motion.div
key={index}
className="bg-white/10 backdrop-blur border-white/20 text-white p-4 rounded-lg flex items-center justify-between"
initial={{ x: 50, opacity: 0 }}
whileInView={{ x: 0, opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}>
<div>
<p className="font-semibold">{item.nomeSistema}</p>
<p className="text-sm text-white/70">
{item.totalAcessos.toLocaleString('pt-BR')} acessos
</p>
</div>
<div className="text-right">
<span className="text-2xl font-bold text-yellow-300">
#{index + 1}
</span> </span>
</div> </motion.h2>
<motion.p
className="text-lg md:text-xl text-gray-600 mb-8 max-w-2xl"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }}>
O AgPortal é o hub central que conecta todos os
colaboradores às ferramentas, sistemas e informações
necessárias para o dia a dia de trabalho.
</motion.p>
</motion.div> </motion.div>
))}
</motion.div> <div className="lg:w-1/2 h-full flex flex-col items-center justify-center">
</div> <motion.div
</StorySlide> className="space-y-4 mb-8"
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.8, delay: 0.3 }}>
<div className="flex flex-col items-center justify-center gap-3">
<div className="text-9xl font-black text-gray-900 tracking-tight">
{metricsPortal
.reduce((acc, p) => acc + p.totalAcessos, 0)
.toLocaleString('pt-BR')}
</div>
<div className="flex flex-col">
<span className="text-lg md:text-xl text-muted-foreground">
Acessos em {year}
</span>
</div>
</div>
</motion.div>
</div>
</div>
</div>
</StorySlide>
<StorySlide className="bg-linear-to-br from-[#0e233d] via-[#173b63] to-[#145190] text-white overflow-hidden">
<div className="max-w-7xl mx-auto w-full h-full px-6 py-8 md:py-12">
<div className="flex flex-col md:flex-row items-start md:items-center justify-between h-full">
<div className="space-y-8 w-full md:w-1/2 text-center md:text-left">
<motion.div
className="space-y-8"
initial={{ scale: 0.9, opacity: 0 }}
whileInView={{ scale: 1, opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}>
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/10 backdrop-blur-sm border border-white/20 mb-4">
<ChartLineUpIcon className="h-4 w-4 text-yellow-300" />
<span className="text-sm font-semibold text-white">
Top 5 Aplicativos
</span>
</div>
<h2 className="text-4xl md:text-5xl font-bold text-white/80">
Aplicativos mais acessados
</h2>
<p className="text-xl text-white/80 max-w-xl">
Aqui estão as 5 aplicações com mais acessos e o
total de horas registradas nelas durante o ano.
</p>
</motion.div>
</div>
<div className="md:w-3/5 w-full mt-8 md:mt-0">
<div className="w-full grid grid-cols-1 gap-3 md:gap-4">
{topItems.slice(0, 5).map((item, index) => {
const medalColors = [
'bg-yellow-400 text-yellow-900',
'bg-gray-300 text-gray-900',
'bg-orange-400 text-orange-900',
'bg-blue-400/20 text-blue-300',
'bg-purple-400/20 text-purple-300',
];
const isMedal = index < 3;
const medalColor =
medalColors[index] || 'bg-white/20';
return (
<motion.div
key={index}
className={`flex items-center p-4 md:p-5 rounded-xl backdrop-blur-sm border border-white/20 ${
isMedal
? 'bg-white/15'
: 'bg-white/10 hover:bg-white/15'
} transition-all`}
initial={{ x: 50, opacity: 0 }}
whileInView={{ x: 0, opacity: 1 }}
viewport={{ once: true }}
transition={{
duration: 0.5,
delay: index * 0.1,
}}>
<div
className={`w-10 h-10 md:w-12 md:h-12 rounded-full flex items-center justify-center font-bold text-base md:text-lg mr-4 ${medalColor}`}>
{index === 0 && '🥇'}
{index === 1 && '🥈'}
{index === 2 && '🥉'}
{index >= 3 && `#${index + 1}`}
</div>
<div className="flex-1 min-w-0">
<p className="text-base md:text-lg font-semibold truncate">
{item.nomeSistema}
</p>
<p className="text-sm text-white/70">
{item.totalAcessos.toLocaleString('pt-BR')}{' '}
acessos {' '}
{Math.round(
item.horasLogadas || 0,
).toLocaleString('pt-BR')}{' '}
h
</p>
</div>
<motion.div
className="ml-4 flex flex-col items-end"
initial={{ scale: 0 }}
whileInView={{ scale: 1 }}
viewport={{ once: true }}
transition={{
type: 'spring',
delay: index * 0.1 + 0.2,
}}>
<p className="text-xl md:text-2xl font-bold text-yellow-300 mb-1">
{(
(item.totalAcessos /
(totalAcessos || 1)) *
100
).toFixed(0)}
%
</p>
<div className="w-24 md:w-32 h-1.5 bg-white/20 rounded-full overflow-hidden">
<motion.div
className="h-full bg-yellow-400 rounded-full"
initial={{ width: 0 }}
whileInView={{
width: `${
(item.totalAcessos /
(totalAcessos || 1)) *
100
}%`,
}}
viewport={{ once: true }}
transition={{
duration: 1,
delay: index * 0.1 + 0.3,
}}
/>
</div>
</motion.div>
</motion.div>
);
})}
</div>
</div>
</div>
</div>
</StorySlide>
</>
)} )}
<StorySlide className="bg-linear-to-br from-slate-50 to-blue-50"> <StorySlide className="bg-linear-to-br from-slate-50 to-blue-50">
<div className="h-screen max-h-screen overflow-y-auto flex flex-col md:flex-row items-center justify-center px-6 gap-12"> <div className="max-w-7xl mx-auto w-full h-full px-6 py-12">
<div className="space-y-12 w-full md:w-1/2 text-center md:text-left"> <div className="h-full flex flex-col md:flex-row items-center justify-center gap-12">
<motion.div <div className="space-y-8 w-full md:w-1/2 text-center md:text-left">
initial={{ scale: 0 }} <motion.div
whileInView={{ scale: 1 }} initial={{ scale: 0 }}
viewport={{ once: true }} whileInView={{ scale: 1 }}
transition={{ type: 'spring', duration: 0.6 }}> viewport={{ once: true }}
<img src={logoAImg} className="w-10" /> transition={{ type: 'spring', duration: 0.6 }}>
</motion.div> <img src={logoAImg} className="w-10" />
</motion.div>
<h2 className="text-4xl md:text-5xl font-bold text-[#0e233d]"> <h2 className="text-4xl md:text-5xl font-bold text-[#0e233d]">
A Ágape deseja a você um {ano + 1} repleto de sucesso! {'<'} O futuro é programado aqui! Feliz {year + 1}! {'/>'}
</h2> </h2>
<p className="text-xl text-muted-foreground max-w-xl"> <p className="text-xl text-muted-foreground max-w-xl">
Obrigado por fazer parte desta jornada de transformação Obrigado por fazer parte desta jornada de transformação
digital na gestão pública. Sua dedicação é essencial para digital na gestão pública. Sua dedicação é essencial para
construirmos um serviço público mais eficiente e construirmos um serviço público mais eficiente e
transparente. transparente.
</p>
<motion.div
className="text-sm text-muted-foreground"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.4 }}>
<p>&copy; {ano} Ágape Sistemas e Tecnologia</p>
<p className="mt-2">
O Futuro da Gestão Pública começa aqui!
</p> </p>
</motion.div>
</div>
<div className="w-full md:w-1/2 flex justify-center"> <motion.div
<div className="max-w-md w-full"> className="text-sm text-muted-foreground"
<ProfileCard initial={{ opacity: 0 }}
name="Usuário Ágape" whileInView={{ opacity: 1 }}
title="Gestor Público" viewport={{ once: true }}
handle="agape" transition={{ duration: 0.6, delay: 0.4 }}>
status="Online" <p>&copy; {year} Ágape Sistemas e Tecnologia</p>
contactText="Contato" <p className="mt-2">
avatarUrl="https://github.com/GuilhermeSantosUI.png" O Futuro da Gestão Pública começa aqui!
showUserInfo={true} </p>
enableTilt={true} </motion.div>
enableMobileTilt={false} </div>
onContactClick={() => console.log('Contact clicked')}
/> <div className="w-full md:w-1/2 flex justify-center">
<div className="max-w-md w-full">
<ProfileCard
name="Usuário Ágape"
title="Gestor Público"
handle="agape"
status="Online"
contactText="Contato"
avatarUrl="https://github.com/GuilhermeSantosUI.png"
totalAcessos={totalAcessos}
aplicacoesCount={aplicacoesCount}
topAppName={topAppName}
topAppAcessos={topAppAcessos}
showUserInfo={true}
enableTilt={true}
enableMobileTilt={false}
onContactClick={() => console.log('Contact clicked')}
/>
</div>
</div> </div>
</div> </div>
</div> </div>