diff --git a/src/app/(protected)/(cadastros)/cadastros/(t_pessoa)/pessoa/grafico/page.tsx b/src/app/(protected)/(cadastros)/cadastros/(t_pessoa)/pessoa/grafico/page.tsx new file mode 100644 index 0000000..34f2897 --- /dev/null +++ b/src/app/(protected)/(cadastros)/cadastros/(t_pessoa)/pessoa/grafico/page.tsx @@ -0,0 +1,278 @@ +'use client' + +import React, { useMemo, useState } from "react"; +import { + Card, + CardTitle, + CardContent, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { + PieChart, + Pie, + Cell, + ResponsiveContainer, + BarChart, + Bar, + XAxis, + YAxis, + Tooltip, + Legend, +} from "recharts"; + +// Função de agregação +function sampleAggregations(data) { + const bySexo: Record = {}; + const byUF: Record = {}; + const byProf: Record = {}; + const byEstCivil: Record = {}; + const byInstr: Record = {}; + const docsExpiry: any[] = []; + + const now = new Date(); + for (const p of data) { + const sexo = p.SEXO || "Não informado"; + bySexo[sexo] = (bySexo[sexo] || 0) + 1; + + const uf = p.UF_RESIDENCIA || p.UF || "Não informado"; + byUF[uf] = (byUF[uf] || 0) + 1; + + const prof = p.TB_PROFISSAO_ID || "Outros"; + byProf[prof] = (byProf[prof] || 0) + 1; + + const ec = p.TB_ESTADOCIVIL_ID || "Não informado"; + byEstCivil[ec] = (byEstCivil[ec] || 0) + 1; + + const gi = p.GRAU_INSTRUCAO || "Não informado"; + byInstr[gi] = (byInstr[gi] || 0) + 1; + + if (p.DOCUMENTO_VALIDADE) { + const dv = new Date(p.DOCUMENTO_VALIDADE); + const days = Math.ceil((dv.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); + docsExpiry.push({ + id: p.PESSOA_ID || p.DOCUMENTO_NUMERO || Math.random(), + days, + date: p.DOCUMENTO_VALIDADE + }); + } + } + + return { + sexo: Object.entries(bySexo).map(([key, value]) => ({ name: key, value })), + uf: Object.entries(byUF).map(([key, value]) => ({ name: key, value })), + prof: Object.entries(byProf) + .map(([key, value]) => ({ name: String(key), value })) + .sort((a, b) => b.value - a.value) + .slice(0, 10), + estCivil: Object.entries(byEstCivil).map(([key, value]) => ({ name: key, value })), + instr: Object.entries(byInstr).map(([key, value]) => ({ name: key, value })), + docsExpiry, + }; +} + +// Paleta +const COLORS = [ + "#4F46E5", + "#06B6D4", + "#F59E0B", + "#EF4444", + "#10B981", + "#8B5CF6", + "#F97316", + "#6366F1", + "#EC4899", + "#334155", +]; + +export default function PessoasDashboard({ dataset = null }) { + const mock = useMemo(() => { + if (dataset) return dataset; + // Mock realista + const nomes = ["Ana Clara", "Bruno Silva", "Carlos Souza", "Daniela Oliveira", "Eduardo Lima", "Fernanda Rocha", "Gustavo Alves", "Helena Martins"]; + const sexos = ["Masculino", "Feminino", "Outro"]; + const ufs = ["São Paulo", "Rio de Janeiro", "Minas Gerais", "Goiás", "Distrito Federal"]; + const profs = ["Advogado", "Professor", "Agricultor", "Engenheiro Civil", "Estudante Universitário", "Médico", "Enfermeiro", "Empresário"]; + const estc = ["Solteiro(a)", "Casado(a)", "Divorciado(a)", "Viúvo(a)", "União Estável"]; + const instrucao = ["Ensino Fundamental", "Ensino Médio", "Ensino Superior", "Pós-Graduação", "Mestrado", "Doutorado"]; + const arr: any[] = []; + for (let i = 0; i < 200; i++) { + const birth = new Date(1960 + Math.floor(Math.random() * 50), Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1); + const docVal = new Date(); + docVal.setFullYear(docVal.getFullYear() + Math.floor(Math.random() * 5)); + arr.push({ + PESSOA_ID: i + 1, + NOME: nomes[Math.floor(Math.random() * nomes.length)], + SEXO: sexos[Math.floor(Math.random() * sexos.length)], + UF_RESIDENCIA: ufs[Math.floor(Math.random() * ufs.length)], + TB_PROFISSAO_ID: profs[Math.floor(Math.random() * profs.length)], + TB_ESTADOCIVIL_ID: estc[Math.floor(Math.random() * estc.length)], + GRAU_INSTRUCAO: instrucao[Math.floor(Math.random() * instrucao.length)], + DOCUMENTO_VALIDADE: docVal.toISOString().slice(0, 10), + DATA_NASCIMENTO: birth.toISOString().slice(0, 10), + }); + } + return arr; + }, [dataset]); + + const aggregations = useMemo(() => sampleAggregations(mock), [mock]); + + return ( +
+
+

Dashboard de Pessoas

+
+ + +
+
+ + {/* Linha 1 */} +
+ + Distribuição por Sexo + + + + + {aggregations.sexo.map((entry, index) => ( + + ))} + + + + + + + + + Idade (faixas etárias) + + + { + const now = new Date(); + const ages = mock.map((p) => { + const d = p.DATA_NASCIMENTO ? new Date(p.DATA_NASCIMENTO) : null; + if (!d) return null; + return Math.floor((now.getTime() - d.getTime()) / (1000 * 60 * 60 * 24 * 365.25)); + }).filter(Boolean) as number[]; + const buckets = ["0-17","18-25","26-35","36-45","46-60","61+"]; + const counts: Record = {"0-17":0,"18-25":0,"26-35":0,"36-45":0,"46-60":0,"61+":0}; + for (const a of ages) { + if (a<=17) counts['0-17']++; + else if (a<=25) counts['18-25']++; + else if (a<=35) counts['26-35']++; + else if (a<=45) counts['36-45']++; + else if (a<=60) counts['46-60']++; + else counts['61+']++; + } + return buckets.map(b => ({ bucket: b, value: counts[b]})); + })()}> + + + + + + + + + + + Top Profissões + + + + + + + + + + + +
+ + {/* Linha 2 */} +
+ + Distribuição por Unidade Federativa + + + + + + + + + + + + + + Documentos próximos ao vencimento + +
    + {aggregations.docsExpiry + .sort((a,b) => a.days - b.days) + .slice(0, 15) + .map((d) => ( +
  • +
    Documento #{d.id}
    +
    {d.date} ({d.days} dias)
    +
  • + ))} +
+
+
+
+ + {/* Linha 3 */} +
+ + Estado Civil + + + + + + + + + + + + Grau de Instrução + + + + + + + + + + + + + + Documentos por Tipo (placeholder) + + + + + + + + + + + +
+ +
+ + Dica: ao conectar seu dataset real, ajuste o mapeamento em sampleAggregations (por exemplo, TB_PROFISSAO_ID → nome da profissão, TB_ESTADOCIVIL_ID → descrição). + +
+
+ ); +} diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 8dd598f..d05bbc6 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -1,75 +1,92 @@ -import * as React from 'react'; +import * as React from "react" -import { cn } from '@/lib/utils'; +import { cn } from "@/lib/utils" -function Card({ className, ...props }: React.ComponentProps<'div'>) { +function Card({ className, ...props }: React.ComponentProps<"div">) { return (
- ); + ) } -function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { return (
- ); + ) } -function CardTitle({ className, ...props }: React.ComponentProps<'div'>) { +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { return (
- ); + ) } -function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { return (
- ); + ) } -function CardAction({ className, ...props }: React.ComponentProps<'div'>) { +function CardAction({ className, ...props }: React.ComponentProps<"div">) { return (
- ); + ) } -function CardContent({ className, ...props }: React.ComponentProps<'div'>) { - return
; +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) } -function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { return (
- ); + ) } -export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }; +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx new file mode 100644 index 0000000..8b42f21 --- /dev/null +++ b/src/components/ui/chart.tsx @@ -0,0 +1,357 @@ +"use client" + +import * as React from "react" +import * as RechartsPrimitive from "recharts" + +import { cn } from "@/lib/utils" + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const + +export type ChartConfig = { + [k in string]: { + label?: React.ReactNode + icon?: React.ComponentType + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ) +} + +type ChartContextProps = { + config: ChartConfig +} + +const ChartContext = React.createContext(null) + +function useChart() { + const context = React.useContext(ChartContext) + + if (!context) { + throw new Error("useChart must be used within a ") + } + + return context +} + +function ChartContainer({ + id, + className, + children, + config, + ...props +}: React.ComponentProps<"div"> & { + config: ChartConfig + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"] +}) { + const uniqueId = React.useId() + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` + + return ( + +
+ + + {children} + +
+
+ ) +} + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([, config]) => config.theme || config.color + ) + + if (!colorConfig.length) { + return null + } + + return ( +