Compare commits
No commits in common. "cd45b9fa4157f15cda3095e1199342848ea8bd66" and "5a8a97e9b4cc5138baddb4ec886ee463b38fe30a" have entirely different histories.
cd45b9fa41
...
5a8a97e9b4
|
|
@ -1,5 +1,5 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="pt-br">
|
<html lang="en">
|
||||||
<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" />
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@
|
||||||
"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",
|
||||||
|
|
@ -3488,15 +3487,6 @@
|
||||||
"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",
|
||||||
|
|
@ -3738,15 +3728,6 @@
|
||||||
"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",
|
||||||
|
|
@ -4711,19 +4692,6 @@
|
||||||
"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",
|
||||||
|
|
@ -5976,15 +5944,6 @@
|
||||||
"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",
|
||||||
|
|
@ -6222,15 +6181,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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",
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@
|
||||||
"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",
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
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="/:token" element={<RetrospectiveSlides />} />
|
<Route path="/" element={<LoginSlide />} />
|
||||||
|
<Route path="/retrospectiva" element={<RetrospectiveSlides />} />
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export const localStorageKeys = {
|
|
||||||
ACCESS_TOKEN: '@review:token',
|
|
||||||
};
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
/* 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;
|
|
||||||
}
|
|
||||||
|
|
@ -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(ano: number) {
|
export function useMetricsByPortal(cpf: string, ano: number) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['metricsByPortal', ano],
|
queryKey: ['metricsByPortal', cpf, ano],
|
||||||
queryFn: () => metricsService.getMetricsByPortal({ ano }),
|
queryFn: () => metricsService.getMetricsByPortal({ cpf, ano }),
|
||||||
enabled: !!ano,
|
enabled: !!cpf && !!ano,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMetricsBySystems(ano: number) {
|
export function useMetricsBySystems(cpf: string, ano: number) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['metricsBySystems', ano],
|
queryKey: ['metricsBySystems', cpf, ano],
|
||||||
queryFn: () => metricsService.getMetricsBySystems({ ano }),
|
queryFn: () => metricsService.getMetricsBySystems({ cpf, ano }),
|
||||||
enabled: !!ano,
|
enabled: !!cpf && !!ano,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,10 @@ const api = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_URL,
|
baseURL: import.meta.env.VITE_API_URL,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function setAuthToken(newToken: string | null) {
|
// Adicionar token temporariamente aos headers
|
||||||
if (newToken) {
|
const token = import.meta.env.VITE_API_TOKEN || '';
|
||||||
api.defaults.headers.common['Authorization'] = `Bearer ${newToken}`;
|
if (token) {
|
||||||
} else {
|
api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||||
delete api.defaults.headers.common['Authorization'];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const queryClient = new QueryClient({
|
export const queryClient = new QueryClient({
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,19 @@ 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}`,
|
`/userAccessMetricsByPortal?ano=${ano}&cpf=${cpf}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,16 @@ 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(`/userAccessMetricsBySystem?ano=${ano}`);
|
const { data } = await api.get(
|
||||||
|
`/userAccessMetricsBySystem?ano=${ano}&cpf=${cpf}`,
|
||||||
|
);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,16 +14,13 @@ 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}>
|
||||||
<AuthProvider>
|
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
</AuthProvider>
|
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</BrowserRouter>,
|
</BrowserRouter>,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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: #febb01;
|
--secondary: oklch(0.97 0 0);
|
||||||
--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);
|
||||||
|
|
|
||||||
|
|
@ -379,6 +379,7 @@
|
||||||
.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;
|
||||||
|
|
@ -600,12 +601,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-content {
|
.metric-content {
|
||||||
display: flex;
|
flex: 1;
|
||||||
justify-content: center;
|
min-width: 0;
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 0px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-value {
|
.metric-value {
|
||||||
|
|
@ -615,6 +612,8 @@
|
||||||
-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 {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,4 @@
|
||||||
import React, {
|
import React, { useCallback, useEffect, useMemo, useRef } from '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';
|
||||||
|
|
@ -45,15 +39,7 @@ 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);
|
||||||
|
|
||||||
|
|
@ -360,31 +346,30 @@ const ProfileCardComponent = ({
|
||||||
gap: '16px',
|
gap: '16px',
|
||||||
padding: '24px',
|
padding: '24px',
|
||||||
}}>
|
}}>
|
||||||
{/* Card de Acessos (dinâmico) */}
|
{/* Card de Acessos */}
|
||||||
<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">
|
<h4 className="metric-value">23.343</h4>
|
||||||
{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">
|
<h4 className="metric-value">8</h4>
|
||||||
{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 bg-gradient-to-br from-blue-900/30 to-cyan-900/30">
|
<div className="metric-card-wide bg-gradient-to-br from-indigo-900/30 to-purple-900/30">
|
||||||
<div className="metric-content">
|
<div className="metric-content-wide">
|
||||||
<h4 className="metric-value">{topAppName || '—'}</h4>
|
<div className="flex items-center gap-2">
|
||||||
<p className="metric-label">Aplicação mais utilizada</p>
|
<span className="top-badge">TOP 1</span>
|
||||||
|
<h4 className="metric-app-name">agGestor</h4>
|
||||||
|
</div>
|
||||||
|
<p className="metric-app-desc">
|
||||||
|
Aplicação mais utilizada
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
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%',
|
||||||
|
};
|
||||||
|
|
@ -1,89 +1,74 @@
|
||||||
/* 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 { ChartLineUpIcon, SpinnerIcon } from '@phosphor-icons/react';
|
import {
|
||||||
|
BuildingIcon,
|
||||||
|
CalendarIcon,
|
||||||
|
ClockIcon,
|
||||||
|
SpinnerIcon,
|
||||||
|
} from '@phosphor-icons/react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useEffect } from 'react';
|
import * as React from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useSearchParams } 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 { token } = useParams();
|
const [searchParams] = useSearchParams();
|
||||||
const { authenticate } = useAuth();
|
const cpf = searchParams.get('cpf') || '';
|
||||||
|
const ano = searchParams.get('ano')
|
||||||
useEffect(() => {
|
? parseInt(searchParams.get('ano')!)
|
||||||
if (token) {
|
: new Date().getFullYear();
|
||||||
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&cpf=00000000000</code>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: metricsPortal,
|
data: metricsPortal,
|
||||||
isLoading: isLoadingPortal,
|
isLoading: isLoadingPortal,
|
||||||
error: errorPortal,
|
error: errorPortal,
|
||||||
} = useMetricsByPortal(year);
|
} = useMetricsByPortal(cpf, ano);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: metricsSystems,
|
data: metricsSystems,
|
||||||
isLoading: isLoadingSystems,
|
isLoading: isLoadingSystems,
|
||||||
error: errorSystems,
|
error: errorSystems,
|
||||||
} = useMetricsBySystems(year);
|
} = useMetricsBySystems(cpf, ano);
|
||||||
|
|
||||||
|
const [current, setCurrent] = React.useState(0);
|
||||||
|
|
||||||
|
// Calcula slides baseado nos dados disponíveis
|
||||||
const slides: number[] = [];
|
const slides: number[] = [];
|
||||||
slides.push(0);
|
slides.push(0); // Slide inicial
|
||||||
if (metricsPortal && metricsPortal.length > 0) slides.push(1);
|
if (metricsPortal && metricsPortal.length > 0) slides.push(1); // Slide portais
|
||||||
|
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(2);
|
slides.push(3); // Slide com informações gerais
|
||||||
}
|
}
|
||||||
slides.push(3);
|
slides.push(4); // Slide final
|
||||||
|
|
||||||
|
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 ? (
|
||||||
|
|
@ -108,9 +93,10 @@ 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="max-w-7xl mx-auto w-full px-6">
|
|
||||||
<div className="text-center space-y-8">
|
<div className="text-center space-y-8">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ scale: 0.8, opacity: 0 }}
|
initial={{ scale: 0.8, opacity: 0 }}
|
||||||
|
|
@ -125,26 +111,14 @@ export function RetrospectiveSlides() {
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.h1
|
<motion.h1
|
||||||
className="text-5xl md:text-6xl font-medium mb-4"
|
className="text-6xl md:text-7xl 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 }}>
|
||||||
{metricsPortal && metricsPortal[0]?.nomeUsuario ? (
|
Sua Retrospectiva
|
||||||
<>
|
|
||||||
Olá,{' '}
|
|
||||||
<span className="text-secondary">
|
|
||||||
{metricsPortal[0].nomeUsuario.split(' ')[0]}
|
|
||||||
</span>
|
|
||||||
!
|
|
||||||
<br />
|
<br />
|
||||||
Bem-vindo à sua Retrospectiva Ágape {year}
|
Ágape {ano}
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
Retrospectiva Ágape {year}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
|
|
||||||
<motion.p
|
<motion.p
|
||||||
|
|
@ -157,6 +131,15 @@ export function RetrospectiveSlides() {
|
||||||
pública
|
pública
|
||||||
</motion.p>
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
className="text-lg text-white/70"
|
||||||
|
initial={{ y: 20, opacity: 0 }}
|
||||||
|
whileInView={{ y: 0, opacity: 1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.5 }}>
|
||||||
|
Seus dados analisados: CPF •••••••••••
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
className="flex items-center justify-center gap-4 text-sm text-white/70 pt-8"
|
className="flex items-center justify-center gap-4 text-sm text-white/70 pt-8"
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
|
|
@ -170,18 +153,127 @@ export function RetrospectiveSlides() {
|
||||||
<span>Parceria</span>
|
<span>Parceria</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</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">
|
<StorySlide className="bg-linear-to-br from-slate-50 to-blue-50">
|
||||||
<div className="max-w-7xl mx-auto w-full h-full px-6 py-8 md:py-16 relative z-10">
|
<div className="space-y-8">
|
||||||
<div className="flex flex-col lg:flex-row items-center justify-between h-full">
|
<div className="text-center space-y-4">
|
||||||
<motion.div
|
<motion.div
|
||||||
className="lg:w-1/2 flex flex-col items-start mb-10 lg:mb-0 space-y-8 lg:pr-12"
|
initial={{ scale: 0 }}
|
||||||
initial={{ opacity: 0, x: -30 }}
|
whileInView={{ scale: 1 }}
|
||||||
whileInView={{ opacity: 1, x: 0 }}
|
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
|
||||||
|
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">
|
||||||
|
{portal.nomeSistema}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Acessos
|
||||||
|
</p>
|
||||||
|
<p className="text-3xl font-bold text-[#145190] mt-2">
|
||||||
|
{portal.totalAcessos.toLocaleString('pt-BR')}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</StorySlide>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{metricsSystems && metricsSystems.length > 0 && (
|
||||||
|
<StorySlide className="bg-linear-to-br from-slate-50 to-blue-50">
|
||||||
|
<div className="space-y-8">
|
||||||
|
<div className="text-center space-y-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
whileInView={{ scale: 1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ type: 'spring', duration: 0.6 }}>
|
||||||
|
<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 }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.6 }}>
|
transition={{ duration: 0.6 }}>
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|
@ -189,189 +281,69 @@ export function RetrospectiveSlides() {
|
||||||
whileInView={{ scale: 1 }}
|
whileInView={{ scale: 1 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ type: 'spring', duration: 0.6 }}>
|
transition={{ type: 'spring', duration: 0.6 }}>
|
||||||
<img src={logoAImg} className="w-10" />
|
<ClockIcon className="h-24 w-24 mb-8 text-yellow-300 lg:mx-0" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.h2
|
<div className="space-y-6">
|
||||||
className="text-3xl md:text-4xl lg:text-5xl font-bold text-gray-900 mb-4"
|
<h2 className="text-4xl md:text-5xl font-bold">
|
||||||
initial={{ opacity: 0, y: -20 }}
|
Seu Engajamento
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.6, delay: 0.1 }}>
|
|
||||||
<span className="text-[#0e233d mt-2">
|
|
||||||
AgPortal <br />
|
|
||||||
Sua Porta de Entrada Digital
|
|
||||||
</span>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<div className="lg:w-1/2 h-full flex flex-col items-center justify-center">
|
|
||||||
<motion.div
|
|
||||||
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>
|
</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
|
<motion.div
|
||||||
key={index}
|
className="text-6xl md:text-7xl font-bold text-yellow-300"
|
||||||
className={`flex items-center p-4 md:p-5 rounded-xl backdrop-blur-sm border border-white/20 ${
|
initial={{ scale: 0.5, opacity: 0 }}
|
||||||
isMedal
|
whileInView={{ scale: 1, opacity: 1 }}
|
||||||
? '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 }}
|
viewport={{ once: true }}
|
||||||
transition={{
|
transition={{
|
||||||
type: 'spring',
|
type: 'spring',
|
||||||
delay: index * 0.1 + 0.2,
|
duration: 0.8,
|
||||||
|
delay: 0.2,
|
||||||
}}>
|
}}>
|
||||||
<p className="text-xl md:text-2xl font-bold text-yellow-300 mb-1">
|
{totalAcessos.toLocaleString('pt-BR')}
|
||||||
{(
|
</motion.div>
|
||||||
(item.totalAcessos /
|
<p className="text-xl text-white/90 max-w-lg">
|
||||||
(totalAcessos || 1)) *
|
Total de acessos ao longo do ano
|
||||||
100
|
|
||||||
).toFixed(0)}
|
|
||||||
%
|
|
||||||
</p>
|
</p>
|
||||||
<div className="w-24 md:w-32 h-1.5 bg-white/20 rounded-full overflow-hidden">
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
className="h-full bg-yellow-400 rounded-full"
|
className="space-y-4"
|
||||||
initial={{ width: 0 }}
|
initial={{ x: 50, opacity: 0 }}
|
||||||
whileInView={{
|
whileInView={{ x: 0, opacity: 1 }}
|
||||||
width: `${
|
|
||||||
(item.totalAcessos /
|
|
||||||
(totalAcessos || 1)) *
|
|
||||||
100
|
|
||||||
}%`,
|
|
||||||
}}
|
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{
|
transition={{ duration: 0.6, delay: 0.2 }}>
|
||||||
duration: 1,
|
<h3 className="text-2xl font-bold text-white mb-6">
|
||||||
delay: index * 0.1 + 0.3,
|
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>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
))}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</StorySlide>
|
</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="max-w-7xl mx-auto w-full h-full px-6 py-12">
|
<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="h-full flex flex-col md:flex-row items-center justify-center gap-12">
|
<div className="space-y-12 w-full md:w-1/2 text-center md:text-left">
|
||||||
<div className="space-y-8 w-full md:w-1/2 text-center md:text-left">
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ scale: 0 }}
|
initial={{ scale: 0 }}
|
||||||
whileInView={{ scale: 1 }}
|
whileInView={{ scale: 1 }}
|
||||||
|
|
@ -381,7 +353,7 @@ export function RetrospectiveSlides() {
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<h2 className="text-4xl md:text-5xl font-bold text-[#0e233d]">
|
<h2 className="text-4xl md:text-5xl font-bold text-[#0e233d]">
|
||||||
{'<'} O futuro é programado aqui! Feliz {year + 1}! {'/>'}
|
A Ágape deseja a você um {ano + 1} repleto de sucesso!
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p className="text-xl text-muted-foreground max-w-xl">
|
<p className="text-xl text-muted-foreground max-w-xl">
|
||||||
|
|
@ -397,7 +369,7 @@ export function RetrospectiveSlides() {
|
||||||
whileInView={{ opacity: 1 }}
|
whileInView={{ opacity: 1 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.6, delay: 0.4 }}>
|
transition={{ duration: 0.6, delay: 0.4 }}>
|
||||||
<p>© {year} Ágape Sistemas e Tecnologia</p>
|
<p>© {ano} Ágape Sistemas e Tecnologia</p>
|
||||||
<p className="mt-2">
|
<p className="mt-2">
|
||||||
O Futuro da Gestão Pública começa aqui!
|
O Futuro da Gestão Pública começa aqui!
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -413,10 +385,6 @@ export function RetrospectiveSlides() {
|
||||||
status="Online"
|
status="Online"
|
||||||
contactText="Contato"
|
contactText="Contato"
|
||||||
avatarUrl="https://github.com/GuilhermeSantosUI.png"
|
avatarUrl="https://github.com/GuilhermeSantosUI.png"
|
||||||
totalAcessos={totalAcessos}
|
|
||||||
aplicacoesCount={aplicacoesCount}
|
|
||||||
topAppName={topAppName}
|
|
||||||
topAppAcessos={topAppAcessos}
|
|
||||||
showUserInfo={true}
|
showUserInfo={true}
|
||||||
enableTilt={true}
|
enableTilt={true}
|
||||||
enableMobileTilt={false}
|
enableMobileTilt={false}
|
||||||
|
|
@ -425,7 +393,6 @@ export function RetrospectiveSlides() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</StorySlide>
|
</StorySlide>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue