diff --git a/package-lock.json b/package-lock.json index 24d39c6..276c819 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4273,7 +4273,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true, "license": "MIT" }, "node_modules/d3-array": { @@ -4397,6 +4396,127 @@ "node": ">=12" } }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -4492,6 +4612,12 @@ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -4564,6 +4690,16 @@ "node": ">=0.10.0" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -5479,6 +5615,15 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/fast-equals": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.2.tgz", + "integrity": "sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -6042,6 +6187,15 @@ "node": ">=12" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -6928,6 +7082,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -7849,6 +8009,21 @@ } } }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -8825,6 +9000,9 @@ } }, "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", 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 681ad98..9d5630d 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -1,92 +1,132 @@ 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">) { - return ( -
) { + return ( +
+ className + )} + {...props} + /> + ) ) -} + } -function CardHeader({ 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">) { - return ( -
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) ) -} + } -function CardDescription({ 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">) { - 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 CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) + } + + function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) ) -} + } -function CardFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -export { - Card, - CardHeader, - CardFooter, - CardTitle, - CardAction, - CardDescription, - CardContent, -} + 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 ( +