diff --git a/package-lock.json b/package-lock.json index c13ef56..918aa7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", + "@tanstack/react-table": "^8.21.3", "@tinymce/tinymce-react": "^6.3.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -1985,6 +1986,39 @@ "tailwindcss": "4.1.12" } }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tinymce/tinymce-react": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/@tinymce/tinymce-react/-/tinymce-react-6.3.0.tgz", @@ -2286,12 +2320,6 @@ "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, "node_modules/js-cookie": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", @@ -2301,6 +2329,12 @@ "node": ">=14" } }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", diff --git a/package.json b/package.json index 7e642c5..1ffb165 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", + "@tanstack/react-table": "^8.21.3", "@tinymce/tinymce-react": "^6.3.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/src/actions/text/GetNameInitials.ts b/src/actions/text/GetNameInitials.ts new file mode 100644 index 0000000..621409e --- /dev/null +++ b/src/actions/text/GetNameInitials.ts @@ -0,0 +1,16 @@ +export default function GetNameInitials(data: string): string { + if (!data) return ""; + + // Remove espaços extras no início e no fim e divide em palavras + const palavras = data.trim().split(/\s+/); + + if (palavras.length === 0) return ""; + + if (palavras.length === 1) { + // Apenas uma palavra → retorna as duas primeiras letras + return palavras[0].substring(0, 2).toUpperCase(); + } + + // Duas ou mais palavras → retorna a primeira letra das duas primeiras palavras + return (palavras[0].charAt(0) + palavras[1].charAt(0)).toUpperCase(); +} \ No newline at end of file diff --git a/src/app/(protected)/(cadastros)/cadastros/(t_pessoa)/pessoa/fisica/page copy 2.tsx b/src/app/(protected)/(cadastros)/cadastros/(t_pessoa)/pessoa/fisica/page copy 2.tsx new file mode 100644 index 0000000..9697d08 --- /dev/null +++ b/src/app/(protected)/(cadastros)/cadastros/(t_pessoa)/pessoa/fisica/page copy 2.tsx @@ -0,0 +1,249 @@ +"use client"; + +import * as React from "react"; +import { + ColumnDef, + useReactTable, + getCoreRowModel, + getPaginationRowModel, + getSortedRowModel, + getFilteredRowModel, + SortingState, + ColumnFiltersState, + VisibilityState, + RowSelectionState, + flexRender, +} from "@tanstack/react-table"; + +import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "@/components/ui/table"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuLabel, + DropdownMenuSeparator, +} from "@/components/ui/dropdown-menu"; +import { Checkbox } from "@/components/ui/checkbox"; +import { MoreHorizontal, ArrowUpDown } from "lucide-react"; + +// Define o tipo dos dados +type User = { + id: string; + name: string; + email: string; + role: "admin" | "user" | "guest"; + status: "active" | "inactive"; +}; + +// Exemplo de dados +const users: User[] = [ + { id: "1", name: "Alice", email: "alice@example.com", role: "admin", status: "active" }, + { id: "2", name: "Bob", email: "bob@example.com", role: "user", status: "inactive" }, + { id: "3", name: "Carol", email: "carol@example.com", role: "user", status: "active" }, + { id: "4", name: "Dave", email: "dave@example.com", role: "guest", status: "active" }, + // ... mais +]; + +// Definição de colunas +const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all rows" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label={`Select row ${row.index}`} + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: "name", + header: "Name", + cell: ({ row }) => {row.getValue("name")}, + }, + { + accessorKey: "email", + header: ({ column }) => ( + + ), + cell: ({ row }) => {row.getValue("email")}, + }, + { + accessorKey: "role", + header: "Role", + cell: ({ row }) => {row.getValue("role")}, + }, + { + accessorKey: "status", + header: "Status", + cell: ({ row }) => ( + + {row.getValue("status")} + + ), + }, + { + id: "actions", + header: "Actions", + cell: ({ row }) => { + const user = row.original; + return ( + + + + + + Actions + alert(`Editar usuário ${user.id}`)}>Edit + alert(`Deletar usuário ${user.id}`)}>Delete + + + ); + }, + enableHiding: false, + }, +]; + +export default function UsersTable() { + + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState([]); + const [columnVisibility, setColumnVisibility] = React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + + const table = useReactTable({ + data: users, + columns, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + }); + + return ( +
+ {/* Filtro */} +
+ table.getColumn("email")?.setFilterValue(e.target.value)} + className="max-w-sm" + /> + + + + + + {table + .getAllColumns() + .filter((col) => col.getCanHide()) + .map((col) => ( + col.toggleVisibility(!!v)} + > + {col.id} + + ))} + + +
+ + {/* Tabela */} +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + ))} + + ))} + + + {table.getRowModel().rows.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + Nenhum resultado encontrado. + + + )} + +
+ +
+ + {/* Paginação */} +
+ + +
+ + {/* Quantidade de linhas selecionadas */} +
+ {Object.keys(rowSelection).length} de {table.getFilteredRowModel().rows.length} selecionadas +
+
+ ); +} diff --git a/src/app/(protected)/(cadastros)/cadastros/(t_pessoa)/pessoa/fisica/page copy.tsx b/src/app/(protected)/(cadastros)/cadastros/(t_pessoa)/pessoa/fisica/page copy.tsx new file mode 100644 index 0000000..baaffc1 --- /dev/null +++ b/src/app/(protected)/(cadastros)/cadastros/(t_pessoa)/pessoa/fisica/page copy.tsx @@ -0,0 +1,174 @@ +'use client'; + +import { useEffect, useState, useCallback } from "react"; +import { Card, CardContent } from "@/components/ui/card"; + +import Loading from "@/app/_components/loading/loading"; +import TPessoaTable from "../../../_components/t_pessoa/TPessoaTable"; +import TPessoaForm from "../../../_components/t_pessoa/TPessoaForm"; + +import { useTPessoaIndexHook } from "../../../_hooks/t_pessoa/useTPessoaIndexHook"; +import { useTPessoaSaveHook } from "../../../_hooks/t_pessoa/useTPessoaSaveHook"; +import { useTPessoaDeleteHook } from "../../../_hooks/t_pessoa/useTPessoaDeleteHook"; + +import ConfirmDialog from "@/app/_components/confirm_dialog/ConfirmDialog"; +import { useConfirmDialog } from "@/app/_components/confirm_dialog/useConfirmDialog"; + +import TPessoaInterface from "../../../_interfaces/TPessoaInterface"; +import Header from "@/app/_components/structure/Header"; + +export default function TPessoaFisica() { + + // Controle de estado do botão + const [buttonIsLoading, setButtonIsLoading] = useState(false); + + // Hooks para leitura e salvamento + const { tPessoa, fetchTPessoa } = useTPessoaIndexHook(); + const { saveTCensec } = useTPessoaSaveHook(); + const { deleteTCensec } = useTPessoaDeleteHook(); + + // Estados + const [selectedAndamento, setSelectedAndamento] = useState(null); + const [isFormOpen, setIsFormOpen] = useState(false); + + // Estado para saber qual item será deletado + const [itemToDelete, setItemToDelete] = useState(null); + + /** + * Hook do modal de confirmação + */ + const { + isOpen: isConfirmOpen, + openDialog: openConfirmDialog, + handleConfirm, + handleCancel, + } = useConfirmDialog(); + + /** + * Abre o formulário no modo de edição ou criação + */ + const handleOpenForm = useCallback((data: TPessoaInterface | null) => { + setSelectedAndamento(data); + setIsFormOpen(true); + }, []); + + /** + * Fecha o formulário e limpa o andamento selecionado + */ + const handleCloseForm = useCallback(() => { + setSelectedAndamento(null); + setIsFormOpen(false); + }, []); + + /** + * Salva os dados do formulário + */ + const handleSave = useCallback(async (formData: TPessoaInterface) => { + + // Coloca o botão em estado de loading + setButtonIsLoading(true); + + // Aguarda salvar o registro + await saveTCensec(formData); + + // Remove o botão em estado de loading + setButtonIsLoading(false); + + // Atualiza a lista de dados + fetchTPessoa(); + + }, [saveTCensec, fetchTPessoa, handleCloseForm]); + + /** + * Quando o usuário clica em "remover" na tabela + */ + const handleConfirmDelete = useCallback((item: TPessoaInterface) => { + + // Define o item atual para remoção + setItemToDelete(item); + + // Abre o modal de confirmação + openConfirmDialog(); + + }, [openConfirmDialog]); + + /** + * Executa a exclusão de fato quando o usuário confirma + */ + const handleDelete = useCallback(async () => { + + // Protege contra null + if (!itemToDelete) return; + + // Executa o Hook de remoção + await deleteTCensec(itemToDelete); + + // Atualiza a lista + await fetchTPessoa(); + + // Limpa o item selecionado + setItemToDelete(null); + + // Fecha o modal + handleCancel(); + + }, [itemToDelete, fetchTPessoa, handleCancel]); + + /** + * Busca inicial dos dados + */ + useEffect(() => { + fetchTPessoa(); + }, []); + + /** + * Tela de loading enquanto carrega os dados + */ + if (tPessoa.length == 0) { + return ; + } + + return ( +
+ {/* Cabeçalho */} +
{ handleOpenForm(null) }} + /> + + {/* Tabela de andamentos */} + + + + + + + {/* Modal de confirmação */} + + + {/* Formulário de criação/edição */} + +
+ ); 4 +} \ No newline at end of file diff --git a/src/app/(protected)/(cadastros)/cadastros/(t_pessoa)/pessoa/fisica/page.tsx b/src/app/(protected)/(cadastros)/cadastros/(t_pessoa)/pessoa/fisica/page.tsx index dc579cd..ad8259d 100644 --- a/src/app/(protected)/(cadastros)/cadastros/(t_pessoa)/pessoa/fisica/page.tsx +++ b/src/app/(protected)/(cadastros)/cadastros/(t_pessoa)/pessoa/fisica/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState, useCallback } from "react"; +import React, { useEffect, useState, useCallback } from "react"; import { Card, CardContent } from "@/components/ui/card"; import Loading from "@/app/_components/loading/loading"; @@ -16,6 +16,159 @@ import { useConfirmDialog } from "@/app/_components/confirm_dialog/useConfirmDia import TPessoaInterface from "../../../_interfaces/TPessoaInterface"; import Header from "@/app/_components/structure/Header"; +import { ColumnDef, ColumnFiltersState, flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, RowSelectionState, SortingState, useReactTable, VisibilityState } from "@tanstack/react-table"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Button } from "@/components/ui/button"; +import { ArrowUpDownIcon, EllipsisIcon, MoreHorizontalIcon, PencilIcon, Trash2Icon } from "lucide-react"; +import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { TPessoaIndexData } from "../../../_data/TPessoa/TPessoaIndexData"; +import { Input } from "@/components/ui/input"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import GetNameInitials from "@/actions/text/GetNameInitials"; + +const pessoas = await TPessoaIndexData(); + +// Definição de colunas +const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all rows" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label={`Select row ${row.index}`} + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: "#", + header: ({ column }) => ( + + ), + cell: ({ row }) => {row.getValue("pessoa_id") || ''}, + }, + { + accessorKey: "foto_nome_email", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ {/* Avatar */} +
+ {row.getValue("foto") ? ( + {row.getValue("nome") + ) : ( + + {GetNameInitials(row.getValue("nome"))} + + )} +
+ {/* Nome e Email */} +
+
+ {row.getValue("nome") || "-"} +
+
+ {row.getValue("email") || "-"} +
+
+
+ ), + }, + { + accessorKey: "cpf", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + row.getValue("cpf_cnpj") + ), + }, + { + accessorKey: "telefone", + header: "Telefone", + cell: ({ row }) => ( + {row.getValue("telefone")} + ), + }, + { + accessorKey: "cidade_uf", + header: "Cidade/UF", + cell: ({ row }) => ( + row.getValue("cidade") + "/" + row.getValue("uf") + ), + }, + { + accessorKey: "data_cadastro", + header: "Cadastro", + cell: ({ row }) => ( + row.getValue("data_cadastro") + ), + }, + { + id: "actions", + header: "Actions", + cell: ({ row }) => { + const pessoa = row.original; + return ( + + + + + + + + { }} + > + + Editar + + + + + { }} + > + + Remover + + + + + ); + }, + enableHiding: false, + }, +]; export default function TPessoaFisica() { @@ -121,6 +274,31 @@ export default function TPessoaFisica() { fetchTPessoa(); }, []); + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState([]); + const [columnVisibility, setColumnVisibility] = React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + + const table = useReactTable({ + data: pessoas.data, + columns, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + }); + + /** * Tela de loading enquanto carrega os dados */ @@ -132,9 +310,9 @@ export default function TPessoaFisica() {
{/* Cabeçalho */}
{ handleOpenForm(null) }} /> @@ -149,6 +327,103 @@ export default function TPessoaFisica() { +
+ {/* Filtro */} +
+ table.getColumn("foto_nome_email")?.setFilterValue(e.target.value)} + className="max-w-sm" + /> + + + + + + {table + .getAllColumns() + .filter((col) => col.getCanHide()) + .map((col) => ( + col.toggleVisibility(!!v)} + > + {col.id} + + ))} + + +
+ + {/* Tabela */} +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + ))} + + ))} + + + {table.getRowModel().rows.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + Nenhum resultado encontrado. + + + )} + +
+ +
+ + {/* Paginação */} +
+ + +
+ + {/* Quantidade de linhas selecionadas */} +
+ {Object.keys(rowSelection).length} de {table.getFilteredRowModel().rows.length} selecionadas +
+
+ {/* Modal de confirmação */}
- ); 4 + ); } \ No newline at end of file