[MVPTN-88] feat(DataTable): Cria componentes da DataTable para sere reutilizados por outras partes do sistema

This commit is contained in:
Keven Willian Pereira de Souza 2025-09-26 11:33:17 -03:00
parent a045b3ca72
commit 3fe6ed8c08
11 changed files with 2767 additions and 3469 deletions

View file

@ -0,0 +1,23 @@
/**
* Formata um número de CPF no padrão 999.999.999-99
*
* @param value - CPF em string ou number
* @returns CPF formatado ou string vazia se inválido
*/
export function FormatCPF(value: string | number): string {
if (!value) return "";
// Converte para string e remove tudo que não seja número
const digits = String(value).replace(/\D/g, "");
// Garante que tenha no máximo 11 dígitos
const cleanValue = digits.slice(0, 11);
// Retorna formatado ou vazio se não tiver tamanho suficiente
if (cleanValue.length !== 11) return cleanValue;
return cleanValue.replace(
/(\d{3})(\d{3})(\d{3})(\d{2})/,
"$1.$2.$3-$4"
);
}

View file

@ -0,0 +1,53 @@
/**
* Formata uma data e hora brasileira (DD/MM/YYYY HH:mm)
*
* Suporta:
* - Entrada como string, Date ou number (timestamp)
* - Dados incompletos (apenas dia/mês, sem hora, etc.)
* - Retorna "-" se vazio ou inválido
*
* @param value - Data ou hora em string, Date ou timestamp
* @returns Data formatada no padrão DD/MM/YYYY HH:mm ou parcial
*/
export function FormatDateTime(value: string | Date | number | null | undefined): string {
if (!value) return "-";
let date: Date;
// Converte entrada para Date
if (value instanceof Date) {
date = value;
} else if (typeof value === "number") {
date = new Date(value);
} else if (typeof value === "string") {
// Remove caracteres extras e tenta criar Date
const cleanValue = value.trim().replace(/[^0-9]/g, "");
if (cleanValue.length === 8) {
// DDMMYYYY
const day = parseInt(cleanValue.slice(0, 2), 10);
const month = parseInt(cleanValue.slice(2, 4), 10) - 1;
const year = parseInt(cleanValue.slice(4, 8), 10);
date = new Date(year, month, day);
} else {
// Tenta parse padrão
const parsed = new Date(value);
if (isNaN(parsed.getTime())) return "-";
date = parsed;
}
} else {
return "-";
}
// Extrai partes da data
const day = date.getDate().toString().padStart(2, "0");
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const year = date.getFullYear();
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
// Monta string parcialmente, dependendo da hora estar disponível
const hasTime = !(hours === "00" && minutes === "00");
return `${day}/${month}/${year}${hasTime ? ` ${hours}:${minutes}` : ""}`;
}

View file

@ -0,0 +1,47 @@
/**
* Formata um número de telefone brasileiro.
*
* Suporta:
* - Com ou sem DDD
* - Números incompletos
* - Telefones com 8 ou 9 dígitos
*
* @param value - Número de telefone em string ou number
* @returns Telefone formatado ou "-" se vazio
*/
export function FormatPhone(value: string | number): string {
if (!value) return "-";
// Converte para string e remove tudo que não for número
const digits = String(value).replace(/\D/g, "");
// Se não tiver nada após limpar, retorna "-"
if (digits.length === 0) return "-";
// Garante no máximo 11 dígitos
const cleanValue = digits.slice(0, 11);
// -------------------------------
// SEM DDD
// -------------------------------
if (cleanValue.length <= 8) {
// Até 8 dígitos → formato parcial
return cleanValue.replace(/(\d{4})(\d{0,4})/, "$1-$2").replace(/-$/, "");
}
// -------------------------------
// COM DDD
// -------------------------------
if (cleanValue.length === 9 || cleanValue.length === 10) {
// DDD + telefone de 8 dígitos
return cleanValue.replace(/^(\d{2})(\d{4})(\d{0,4})$/, "($1) $2-$3").replace(/-$/, "");
}
if (cleanValue.length === 11) {
// DDD + telefone de 9 dígitos
return cleanValue.replace(/^(\d{2})(\d{5})(\d{0,4})$/, "($1) $2-$3").replace(/-$/, "");
}
// Caso genérico, se não cair em nenhuma regra
return cleanValue;
}

View file

@ -1,9 +1,9 @@
export default function empty(data: any) {
if (!data || !data === null || !data === undefined) {
return true;
}
return false;
}
/**
* Verifica se um valor é vazio, null ou undefined
*
* @param data - Qualquer valor
* @returns true se estiver vazio, null ou undefined
*/
export default function empty(data: unknown): boolean {
return data == null || data === "" || data === false;
}

View file

@ -1,249 +0,0 @@
"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<User>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all rows"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label={`Select row ${row.index}`}
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "name",
header: "Name",
cell: ({ row }) => <span>{row.getValue("name")}</span>,
},
{
accessorKey: "email",
header: ({ column }) => (
<Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
Email <ArrowUpDown className="ml-1 h-4 w-4" />
</Button>
),
cell: ({ row }) => <span className="lowercase">{row.getValue("email")}</span>,
},
{
accessorKey: "role",
header: "Role",
cell: ({ row }) => <span>{row.getValue("role")}</span>,
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => (
<span className={row.getValue("status") === "active" ? "text-green-600" : "text-red-600"}>
{row.getValue("status")}
</span>
),
},
{
id: "actions",
header: "Actions",
cell: ({ row }) => {
const user = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open actions</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem onClick={() => alert(`Editar usuário ${user.id}`)}>Edit</DropdownMenuItem>
<DropdownMenuItem onClick={() => alert(`Deletar usuário ${user.id}`)}>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
enableHiding: false,
},
];
export default function UsersTable() {
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
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 (
<div className="p-4">
{/* Filtro */}
<div className="flex items-center mb-4 space-x-2">
<Input
placeholder="Buscar por email..."
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
onChange={(e) => table.getColumn("email")?.setFilterValue(e.target.value)}
className="max-w-sm"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto">
Colunas visíveis
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((col) => col.getCanHide())
.map((col) => (
<DropdownMenuCheckboxItem
key={col.id}
checked={col.getIsVisible()}
onCheckedChange={(v) => col.toggleVisibility(!!v)}
>
{col.id}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* Tabela */}
<div className="overflow-hidden rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
Nenhum resultado encontrado.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* Paginação */}
<div className="flex items-center justify-end space-x-2 mt-4">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Anterior
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Próxima
</Button>
</div>
{/* Quantidade de linhas selecionadas */}
<div className="mt-2 text-sm text-gray-600">
{Object.keys(rowSelection).length} de {table.getFilteredRowModel().rows.length} selecionadas
</div>
</div>
);
}

View file

@ -1,463 +0,0 @@
'use client';
import MaskedText from 'react-masked-text';
import React, { 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";
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();
const columns: ColumnDef<TPessoaInterface>[] = [
// ID
{
accessorKey: "pessoa_id",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
# <ArrowUpDownIcon className="ml-1 h-4 w-4" />
</Button>
),
cell: ({ row }) => (
Number(row.getValue("pessoa_id"))
),
},
// Nome / Email / Foto
{
id: "nome_completo",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Nome / Email <ArrowUpDownIcon className="ml-1 h-4 w-4 cursor-pointer" />
</Button>
),
accessorFn: (row) => row,
cell: ({ row }) => {
const pessoa = row.original;
return (
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center overflow-hidden">
{pessoa.foto ? (
<img
src={pessoa.foto}
alt={pessoa.nome || "Avatar"}
className="w-full h-full object-cover"
/>
) : (
<span className="text-sm font-medium text-gray-700">
{GetNameInitials(pessoa.nome)}
</span>
)}
</div>
<div>
<div className="font-semibold text-gray-900">
{pessoa.nome || "-"}
</div>
<div className="text-sm text-gray-500">{pessoa.email || "-"}</div>
</div>
</div>
);
},
sortingFn: (a, b) => {
const nameA = a.original.nome?.toLowerCase() || "";
const nameB = b.original.nome?.toLowerCase() || "";
return nameA.localeCompare(nameB);
},
},
// CPF
{
accessorKey: "cpf_cnpj",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
CPF <ArrowUpDownIcon className="ml-1 h-4 w-4 cursor-pointer" />
</Button>
),
cell: ({ row }) => (
row.getValue("cpf_cnpj")
),
},
// Telefone
{
accessorKey: "telefone",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Telefone <ArrowUpDownIcon className="ml-1 h-4 w-4 cursor-pointer" />
</Button>
),
cell: ({ row }) => (
<span>{row.getValue("telefone") || "-"}</span>
),
},
// Cidade / UF
{
id: "cidade_uf",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Cidade/UF
<ArrowUpDownIcon className="ml-1 h-4 w-4 cursor-pointer" />
</Button>
),
accessorFn: (row) => `${row.cidade}/${row.uf}`,
cell: ({ row }) => <span>{row.getValue("cidade_uf") || "-"}</span>,
sortingFn: (a, b) => {
const cityA = `${a.original.cidade}/${a.original.uf}`.toLowerCase();
const cityB = `${b.original.cidade}/${b.original.uf}`.toLowerCase();
return cityA.localeCompare(cityB);
},
},
// Data de cadastro
{
accessorKey: "data_cadastro",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Cadastro <ArrowUpDownIcon className="ml-1 h-4 w-4 cursor-pointer" />
</Button>
),
cell: ({ row }) => <span>{row.getValue("data_cadastro") || "-"}</span>,
sortingFn: "datetime", // você pode usar função própria se precisar
},
// Ações (não ordenável)
{
id: "actions",
header: "Actions",
cell: ({ row }) => {
const pessoa = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="cursor-pointer">
<EllipsisIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="left" align="start">
<DropdownMenuGroup>
<DropdownMenuItem className="cursor-pointer" onSelect={() => { }}>
<PencilIcon className="mr-2 h-4 w-4" />
Editar
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="cursor-pointer text-red-600"
onSelect={() => { }}
>
<Trash2Icon className="mr-2 h-4 w-4" />
Remover
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
},
enableSorting: false,
enableHiding: false,
},
];
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<TPessoaInterface | null>(null);
const [isFormOpen, setIsFormOpen] = useState(false);
// Estado para saber qual item será deletado
const [itemToDelete, setItemToDelete] = useState<TPessoaInterface | null>(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();
}, []);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
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
*/
if (tPessoa.length == 0) {
return <Loading type={2} />;
}
return (
<div>
{/* Cabeçalho */}
<Header
title={"Pessoas Físicas"}
description={"Gerenciamento de pessoas físicas"}
buttonText={"Nova Pessoa"}
buttonAction={() => { handleOpenForm(null) }}
/>
{/* Filtro */}
<div className="flex items-center mb-4 space-x-2">
<Input
placeholder="Buscar por email..."
value={(table.getColumn("nome_completo")?.getFilterValue() as string) ?? ""}
onChange={(e) => table.getColumn("nome_completo")?.setFilterValue(e.target.value)}
className="w-full"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto">
Colunas visíveis
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((col) => col.getCanHide())
.map((col) => (
<DropdownMenuCheckboxItem
key={col.id}
checked={col.getIsVisible()}
onCheckedChange={(v) => col.toggleVisibility(!!v)}
>
{col.id}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* Tabela */}
<div className="overflow-hidden rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
Nenhum resultado encontrado.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* Paginação */}
<div className="flex items-center justify-end space-x-2 mt-4">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Anterior
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Próxima
</Button>
</div>
{/* Modal de confirmação */}
<ConfirmDialog
isOpen={isConfirmOpen}
title="Confirmar exclusão"
description="Atenção"
message={`Deseja realmente excluir o andamento "${itemToDelete?.nome}"?`}
confirmText="Sim, excluir"
cancelText="Cancelar"
onConfirm={handleDelete}
onCancel={handleCancel}
/>
{/* Formulário de criação/edição */}
<TPessoaForm
isOpen={isFormOpen}
data={selectedAndamento}
onClose={handleCloseForm}
onSave={handleSave}
buttonIsLoading={buttonIsLoading}
/>
</div>
);
}

View file

@ -1,174 +0,0 @@
'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<TPessoaInterface | null>(null);
const [isFormOpen, setIsFormOpen] = useState(false);
// Estado para saber qual item será deletado
const [itemToDelete, setItemToDelete] = useState<TPessoaInterface | null>(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 <Loading type={2} />;
}
return (
<div>
{/* Cabeçalho */}
<Header
title={"Pessoas Físicas"}
description={"Gerenciamento de pessoas físicas"}
buttonText={"Nova Pessoa"}
buttonAction={() => { handleOpenForm(null) }}
/>
{/* Tabela de andamentos */}
<Card>
<CardContent>
<TPessoaTable
data={tPessoa}
onEdit={handleOpenForm}
onDelete={handleConfirmDelete}
/>
</CardContent>
</Card>
{/* Modal de confirmação */}
<ConfirmDialog
isOpen={isConfirmOpen}
title="Confirmar exclusão"
description="Atenção"
message={`Deseja realmente excluir o andamento "${itemToDelete?.nome}"?`}
confirmText="Sim, excluir"
cancelText="Cancelar"
onConfirm={handleDelete}
onCancel={handleCancel}
/>
{/* Formulário de criação/edição */}
<TPessoaForm
isOpen={isFormOpen}
data={selectedAndamento}
onClose={handleCloseForm}
onSave={handleSave}
buttonIsLoading={buttonIsLoading}
/>
</div>
); 4
}

View file

@ -140,8 +140,8 @@ export default function TPessoaFisica() {
{/* Tabela de Registros */}
<TPessoaTable
data={tPessoa}
onDelete={() => { }}
onEdit={() => { }}
onDelete={handleConfirmDelete}
onEdit={handleOpenForm}
/>
{/* Modal de confirmação */}

View file

@ -9,209 +9,225 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from "@/components/ui/table";
import { ArrowUpDownIcon, EllipsisIcon, PencilIcon, Trash2Icon } from "lucide-react";
import TPessoaInterface from "../../_interfaces/TPessoaInterface";
import { TPessoaIndexData } from "../../_data/TPessoa/TPessoaIndexData";
import { ColumnDef } from "@tanstack/react-table";
import GetNameInitials from "@/actions/text/GetNameInitials";
import GetNameInitials from "@/actions/text/GetNameInitials";
import { DataTable } from "@/app/_components/dataTable/DataTable";
import TPessoaInterface from "../../_interfaces/TPessoaInterface";
import { FormatCPF } from "@/actions/CPF/FormatCPF";
import { FormatPhone } from "@/actions/phone/FormatPhone";
import { FormatDateTime } from "@/actions/dateTime/FormatDateTime";
import empty from "@/actions/validations/empty";
// Tipagem das props
interface TPessoaTableProps {
data: TPessoaInterface[];
onEdit: (item: TPessoaInterface, isEditingFormStatus: boolean) => void;
onDelete: (item: TPessoaInterface, isEditingFormStatus: boolean) => void;
}
const pessoas = await TPessoaIndexData();
/**
* Função para criar a definição das colunas da tabela
*/
function createPessoaColumns(
onEdit: (item: TPessoaInterface, isEditingFormStatus: boolean) => void,
onDelete: (item: TPessoaInterface, isEditingFormStatus: boolean) => void
): ColumnDef<TPessoaInterface>[] {
return [
// ID
{
accessorKey: "pessoa_id",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
# <ArrowUpDownIcon className="ml-1 h-4 w-4" />
</Button>
),
cell: ({ row }) => Number(row.getValue("pessoa_id")),
enableSorting: false,
},
const columns: ColumnDef<TPessoaInterface>[] = [
// Nome / Email / Foto
{
id: "nome_completo",
accessorFn: (row) => row,
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Nome / Email <ArrowUpDownIcon className="ml-1 h-4 w-4 cursor-pointer" />
</Button>
),
cell: ({ row }) => {
const pessoa = row.original;
// ID
{
accessorKey: "pessoa_id",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
# <ArrowUpDownIcon className="ml-1 h-4 w-4" />
</Button>
),
cell: ({ row }) => (
Number(row.getValue("pessoa_id"))
),
},
// Nome / Email / Foto
{
id: "nome_completo",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Nome / Email <ArrowUpDownIcon className="ml-1 h-4 w-4 cursor-pointer" />
</Button>
),
accessorFn: (row) => row,
cell: ({ row }) => {
const pessoa = row.original;
return (
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center overflow-hidden">
{pessoa.foto ? (
<img
src={pessoa.foto}
alt={pessoa.nome || "Avatar"}
className="w-full h-full object-cover"
/>
) : (
<span className="text-sm font-medium text-gray-700">
{GetNameInitials(pessoa.nome)}
</span>
)}
</div>
<div>
<div className="font-semibold text-gray-900">
{pessoa.nome || "-"}
return (
<div className="flex items-center gap-3">
{/* Foto ou Iniciais */}
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center overflow-hidden">
{pessoa.foto ? (
<img
src={pessoa.foto}
alt={pessoa.nome || "Avatar"}
className="w-full h-full object-cover"
/>
) : (
<span className="text-sm font-medium text-gray-700">
{GetNameInitials(pessoa.nome)}
</span>
)}
</div>
{/* Nome e Email */}
<div>
<div className="font-semibold text-gray-900 capitalize">
{pessoa.nome || "-"}
</div>
<div className="text-sm text-gray-500">
{empty(pessoa.email) ? "Email não informado" : pessoa.email}
</div>
</div>
<div className="text-sm text-gray-500">{pessoa.email || "-"}</div>
</div>
</div>
);
);
},
sortingFn: (a, b) =>
(a.original.nome?.toLowerCase() || "").localeCompare(
b.original.nome?.toLowerCase() || ""
),
},
sortingFn: (a, b) => {
const nameA = a.original.nome?.toLowerCase() || "";
const nameB = b.original.nome?.toLowerCase() || "";
return nameA.localeCompare(nameB);
// CPF
{
accessorKey: "cpf_cnpj",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
CPF <ArrowUpDownIcon className="ml-1 h-4 w-4 cursor-pointer" />
</Button>
),
cell: ({ row }) => (
FormatCPF(row.getValue("cpf_cnpj"))
),
},
},
// CPF
{
accessorKey: "cpf_cnpj",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
CPF <ArrowUpDownIcon className="ml-1 h-4 w-4 cursor-pointer" />
</Button>
),
cell: ({ row }) => (
row.getValue("cpf_cnpj")
),
},
// Telefone
{
accessorKey: "telefone",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Telefone <ArrowUpDownIcon className="ml-1 h-4 w-4 cursor-pointer" />
</Button>
),
cell: ({ row }) => (
<span>{row.getValue("telefone") || "-"}</span>
),
},
// Cidade / UF
{
id: "cidade_uf",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Cidade/UF
<ArrowUpDownIcon className="ml-1 h-4 w-4 cursor-pointer" />
</Button>
),
accessorFn: (row) => `${row.cidade}/${row.uf}`,
cell: ({ row }) => <span>{row.getValue("cidade_uf") || "-"}</span>,
sortingFn: (a, b) => {
const cityA = `${a.original.cidade}/${a.original.uf}`.toLowerCase();
const cityB = `${b.original.cidade}/${b.original.uf}`.toLowerCase();
return cityA.localeCompare(cityB);
// Telefone
{
accessorKey: "telefone",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Telefone <ArrowUpDownIcon className="ml-1 h-4 w-4 cursor-pointer" />
</Button>
),
cell: ({ row }) => (
FormatPhone(row.getValue("telefone"))
),
},
},
// Data de cadastro
{
accessorKey: "data_cadastro",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Cadastro <ArrowUpDownIcon className="ml-1 h-4 w-4 cursor-pointer" />
</Button>
),
cell: ({ row }) => <span>{row.getValue("data_cadastro") || "-"}</span>,
sortingFn: "datetime", // você pode usar função própria se precisar
},
// Ações (não ordenável)
{
id: "actions",
header: "Actions",
cell: ({ row }) => {
const pessoa = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="cursor-pointer">
<EllipsisIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="left" align="start">
<DropdownMenuGroup>
<DropdownMenuItem className="cursor-pointer" onSelect={() => { }}>
<PencilIcon className="mr-2 h-4 w-4" />
Editar
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="cursor-pointer text-red-600"
onSelect={() => { }}
>
<Trash2Icon className="mr-2 h-4 w-4" />
Remover
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
// Cidade / UF
{
id: "cidade_uf",
accessorFn: (row) => `${row.cidade}/${row.uf}`,
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Cidade/UF <ArrowUpDownIcon className="ml-1 h-4 w-4 cursor-pointer" />
</Button>
),
cell: ({ row }) => <span>{row.getValue("cidade_uf") || "-"}</span>,
sortingFn: (a, b) =>
`${a.original.cidade}/${a.original.uf}`.toLowerCase()
.localeCompare(`${b.original.cidade}/${b.original.uf}`.toLowerCase()),
},
enableSorting: false,
enableHiding: false,
},
];
// Data de cadastro
{
accessorKey: "data_cadastro",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Cadastro <ArrowUpDownIcon className="ml-1 h-4 w-4 cursor-pointer" />
</Button>
),
cell: ({ row }) => (
FormatDateTime(row.getValue("data_cadastro"))
),
sortingFn: "datetime",
},
// Ações
{
id: "actions",
header: "Ações",
cell: ({ row }) => {
const pessoa = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="cursor-pointer">
<EllipsisIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="left" align="start">
<DropdownMenuGroup>
<DropdownMenuItem
className="cursor-pointer"
onSelect={() => onEdit(pessoa, true)}
>
<PencilIcon className="mr-2 h-4 w-4" />
Editar
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="cursor-pointer text-red-600"
onSelect={() => onDelete(pessoa, true)}
>
<Trash2Icon className="mr-2 h-4 w-4" />
Remover
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
},
enableSorting: false,
enableHiding: false,
},
];
}
/**
* Componente principal da tabela
*/
export default function TPessoaTable({
data,
onEdit,
onDelete
onDelete,
}: TPessoaTableProps) {
const columns = createPessoaColumns(onEdit, onDelete);
return (
<div>
aqui
<DataTable
data={data}
columns={columns}
filterColumn="nome_completo"
filterPlaceholder="Buscar por nome ou email..."
/>
</div>
);
}
}

View file

@ -1,24 +1,65 @@
import { Button } from "@/components/ui/button";
import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHeader, TableRow } from "@/components/ui/table";
import { ColumnFiltersState, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, RowSelectionState, SortingState, useReactTable, VisibilityState } from "@tanstack/react-table";
import React from "react";
"use client";
interface dataTableProps {
data: any,
columns: any
import React from "react";
import {
ColumnDef,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
SortingState,
ColumnFiltersState,
VisibilityState,
RowSelectionState,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { ChevronLeftIcon, ChevronRightIcon, EyeIcon } from "lucide-react";
// Tipagem genérica
export interface DataTableProps<TData> {
data: TData[];
columns: ColumnDef<TData, any>[];
filterColumn?: string; // Define qual coluna será usada para filtro
filterPlaceholder?: string;
onEdit?: (item: TData) => void;
onDelete?: (item: TData) => void;
}
export default function dataTable({ data, columns }, dataTableProps) {
export function DataTable<TData>({
data,
columns,
filterColumn,
filterPlaceholder = "Buscar...",
onEdit,
onDelete,
}: DataTableProps<TData>) {
// Estados internos da tabela
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
// Configuração da tabela
const table = useReactTable({
data: data,
data,
columns,
state: {
sorting,
@ -36,22 +77,24 @@ export default function dataTable({ data, columns }, dataTableProps) {
getPaginationRowModel: getPaginationRowModel(),
});
return (
<div>
<div className="space-y-4">
{/* Filtros e colunas */}
<div className="flex items-center gap-2">
{filterColumn && (
<Input
placeholder={filterPlaceholder}
value={(table.getColumn(filterColumn as string)?.getFilterValue() as string) ?? ""}
onChange={(e) => table.getColumn(filterColumn as string)?.setFilterValue(e.target.value)}
className="w-full"
/>
)}
{/* Filtro */}
<div className="flex items-center mb-4 space-x-2">
<Input
placeholder="Buscar por email..."
value={(table.getColumn("nome_completo")?.getFilterValue() as string) ?? ""}
onChange={(e) => table.getColumn("nome_completo")?.setFilterValue(e.target.value)}
className="w-full"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto">
Colunas visíveis
<Button variant="outline" className="ml-auto cursor-pointer">
<EyeIcon /> Colunas visíveis
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
@ -63,6 +106,7 @@ export default function dataTable({ data, columns }, dataTableProps) {
key={col.id}
checked={col.getIsVisible()}
onCheckedChange={(v) => col.toggleVisibility(!!v)}
className="cursor-pointer"
>
{col.id}
</DropdownMenuCheckboxItem>
@ -72,16 +116,14 @@ export default function dataTable({ data, columns }, dataTableProps) {
</div>
{/* Tabela */}
<div className="overflow-hidden rounded-md border" >
<div className="overflow-hidden rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
))}
</TableRow>
@ -90,7 +132,7 @@ export default function dataTable({ data, columns }, dataTableProps) {
<TableBody>
{table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
<TableRow key={row.id} className="cursor-pointer">
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
@ -107,28 +149,31 @@ export default function dataTable({ data, columns }, dataTableProps) {
)}
</TableBody>
</Table>
</div >
</div>
{/* Paginação */}
<div className="flex items-center justify-end space-x-2 mt-4">
<div className="flex items-center justify-end gap-2">
<Button
className="cursor-pointer"
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<ChevronLeftIcon />
Anterior
</Button>
<Button
className="cursor-pointer"
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Próxima
<ChevronRightIcon />
</Button>
</div>
</div>
);
}
}