feat: adicionar suporte a métricas dinâmicas no componente ProfileCard e integrar ao slide de retrospectiva
parent
8f4a42eea8
commit
01554466e4
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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,17 +360,23 @@ 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>
|
||||||
|
|
@ -365,13 +385,106 @@ const ProfileCardComponent = ({
|
||||||
<div className="metric-content-wide">
|
<div className="metric-content-wide">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="top-badge">TOP 1</span>
|
<span className="top-badge">TOP 1</span>
|
||||||
<h4 className="metric-app-name">agGestor</h4>
|
<h4 className="metric-app-name">{topAppName || '—'}</h4>
|
||||||
</div>
|
</div>
|
||||||
<p className="metric-app-desc">
|
<p className="metric-app-desc">
|
||||||
Aplicação mais utilizada
|
{topAppAcessos != null
|
||||||
|
? `${topAppAcessos.toLocaleString('pt-BR')} acessos`
|
||||||
|
: 'Aplicação mais utilizada'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Botões de compartilhamento */}
|
||||||
|
{showShareButtons && (
|
||||||
|
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
||||||
|
<button
|
||||||
|
className="btn-primary"
|
||||||
|
onClick={async () => {
|
||||||
|
if (navigator.share) {
|
||||||
|
try {
|
||||||
|
setSharing(true);
|
||||||
|
await navigator.share({
|
||||||
|
title: `Retrospectiva Ágape - ${name}`,
|
||||||
|
text: `${name} — ${title}\nAcessos: ${
|
||||||
|
totalAcessos != null
|
||||||
|
? totalAcessos.toLocaleString('pt-BR')
|
||||||
|
: '—'
|
||||||
|
}`,
|
||||||
|
url: window.location.href,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
setSharing(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const text = encodeURIComponent(
|
||||||
|
`${name} — ${title} | Acessos: ${
|
||||||
|
totalAcessos != null ? totalAcessos : ''
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
const url = encodeURIComponent(
|
||||||
|
window.location.href,
|
||||||
|
);
|
||||||
|
window.open(
|
||||||
|
`https://twitter.com/intent/tweet?text=${text}&url=${url}`,
|
||||||
|
'_blank',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{sharing ? 'Compartilhando...' : 'Compartilhar'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn-secondary"
|
||||||
|
onClick={() => {
|
||||||
|
const text = encodeURIComponent(
|
||||||
|
`${name} — ${title} | Acessos: ${
|
||||||
|
totalAcessos != null ? totalAcessos : ''
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
const url = encodeURIComponent(window.location.href);
|
||||||
|
window.open(
|
||||||
|
`https://www.facebook.com/sharer/sharer.php?u=${url}"e=${text}`,
|
||||||
|
'_blank',
|
||||||
|
);
|
||||||
|
}}>
|
||||||
|
Facebook
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn-secondary"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
const wrap = wrapRef.current;
|
||||||
|
if (!wrap) return;
|
||||||
|
const html2canvas = (await import('html2canvas'))
|
||||||
|
.default;
|
||||||
|
const canvas = await html2canvas(wrap, {
|
||||||
|
backgroundColor: null,
|
||||||
|
scale: 2,
|
||||||
|
});
|
||||||
|
const dataUrl = canvas.toDataURL('image/png');
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = dataUrl;
|
||||||
|
a.download = `retrospectiva-${
|
||||||
|
handle || 'usuario'
|
||||||
|
}.png`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erro ao gerar imagem', err);
|
||||||
|
alert(
|
||||||
|
'Não foi possível gerar a imagem. Por favor atualize a página e tente novamente.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
Download PNG
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,11 @@ import {
|
||||||
} 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 { BuildingIcon, ChartLineUpIcon, ClockIcon, SpinnerIcon } from '@phosphor-icons/react';
|
import {
|
||||||
|
BuildingIcon,
|
||||||
|
ChartLineUpIcon,
|
||||||
|
SpinnerIcon,
|
||||||
|
} from '@phosphor-icons/react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
@ -62,6 +66,13 @@ export function RetrospectiveSlides() {
|
||||||
.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 ? (
|
||||||
|
|
@ -178,7 +189,7 @@ export function RetrospectiveSlides() {
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.6, delay: 0.1 }}>
|
transition={{ duration: 0.6, delay: 0.1 }}>
|
||||||
Portal Corporativo
|
Acessos no AgPortal
|
||||||
</motion.h2>
|
</motion.h2>
|
||||||
|
|
||||||
<motion.p
|
<motion.p
|
||||||
|
|
@ -242,20 +253,6 @@ export function RetrospectiveSlides() {
|
||||||
Por mês
|
Por mês
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white/10 backdrop-blur-sm rounded-lg p-3 border border-white/20 text-center">
|
|
||||||
<div className="text-lg font-bold text-white mb-1">
|
|
||||||
{Math.round(
|
|
||||||
metricsPortal.reduce(
|
|
||||||
(acc, p) => acc + p.totalAcessos,
|
|
||||||
0,
|
|
||||||
) / 2920,
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-blue-100 opacity-90">
|
|
||||||
Por hora*
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
@ -270,14 +267,11 @@ export function RetrospectiveSlides() {
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.6 }}>
|
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-2">
|
<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-2">
|
||||||
<ChartLineUpIcon className="h-4 w-4 text-yellow-300" />
|
<ChartLineUpIcon className="h-4 w-4 text-yellow-300" />
|
||||||
<span className="text-sm font-semibold text-white">
|
<span className="text-sm font-semibold text-white">
|
||||||
Aplicativos Mais Acessados
|
Aplicativos Mais Acessados
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-base text-white/70">
|
|
||||||
Os sistemas que mais acessaram o portal
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Lista de sistemas */}
|
{/* Lista de sistemas */}
|
||||||
|
|
@ -368,44 +362,12 @@ export function RetrospectiveSlides() {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Indicador de progresso */}
|
|
||||||
<motion.div
|
|
||||||
className="pt-4"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
whileInView={{ opacity: 1 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.6, delay: 0.8 }}>
|
|
||||||
<div className="flex flex-col items-center gap-2">
|
|
||||||
<div className="text-xs text-blue-200 font-medium">
|
|
||||||
Próxima análise
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{[...Array(3)].map((_, i) => (
|
|
||||||
<motion.div
|
|
||||||
key={i}
|
|
||||||
className="h-1.5 w-1.5 rounded-full bg-white/60"
|
|
||||||
animate={{
|
|
||||||
opacity: [0.3, 1, 0.3],
|
|
||||||
scale: [1, 1.2, 1],
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
duration: 1.5,
|
|
||||||
repeat: Infinity,
|
|
||||||
delay: i * 0.2,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</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="h-full flex flex-col md:flex-row items-center justify-center px-6 gap-12 py-12">
|
<div className="h-full flex flex-col md:flex-row items-center justify-center px-6 gap-12 py-12">
|
||||||
<div className="space-y-8 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">
|
||||||
|
|
@ -450,6 +412,11 @@ export function RetrospectiveSlides() {
|
||||||
status="Online"
|
status="Online"
|
||||||
contactText="Contato"
|
contactText="Contato"
|
||||||
avatarUrl="https://github.com/GuilhermeSantosUI.png"
|
avatarUrl="https://github.com/GuilhermeSantosUI.png"
|
||||||
|
// métricas para preencher o cartão final
|
||||||
|
totalAcessos={totalAcessos}
|
||||||
|
aplicacoesCount={aplicacoesCount}
|
||||||
|
topAppName={topAppName}
|
||||||
|
topAppAcessos={topAppAcessos}
|
||||||
showUserInfo={true}
|
showUserInfo={true}
|
||||||
enableTilt={true}
|
enableTilt={true}
|
||||||
enableMobileTilt={false}
|
enableMobileTilt={false}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue