[MVPTN-88] commit de backup

This commit is contained in:
Keven Willian Pereira de Souza 2025-09-26 09:04:44 -03:00
parent 920b9d8aa6
commit a045b3ca72
9 changed files with 1007 additions and 464 deletions

7
package-lock.json generated
View file

@ -38,6 +38,7 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.62.0",
"react-masked-text": "^1.0.5",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"tinymce": "^8.1.2",
@ -2950,6 +2951,12 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/react-masked-text": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/react-masked-text/-/react-masked-text-1.0.5.tgz",
"integrity": "sha512-WichrlCXehL0apIfIgOdi2mjBE03tdMi8wXF+DhHe2ySWYxXCkP88aqDBaJZWUMa3Jp8p2h71u7TpC7EzEjXYw==",
"license": "ISC"
},
"node_modules/react-remove-scroll": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",

View file

@ -39,6 +39,7 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.62.0",
"react-masked-text": "^1.0.5",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"tinymce": "^8.1.2",

View file

@ -1,4 +1,4 @@
export default function GetNameInitials(data: string): string {
export default function GetNameInitials(data?: string): string {
if (!data) return "";
// Remove espaços extras no início e no fim e divide em palavras

View file

@ -0,0 +1,463 @@
'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,7 +1,6 @@
'use client';
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";
@ -16,159 +15,6 @@ 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<TPessoaInterface>[] = [
{
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: "#",
header: ({ column }) => (
<Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
# <ArrowUpDownIcon className="ml-1 h-4 w-4" />
</Button>
),
cell: ({ row }) => <span>{row.getValue("pessoa_id") || ''}</span>,
},
{
accessorKey: "foto_nome_email",
header: ({ column }) => (
<Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
Nome <ArrowUpDownIcon className="ml-1 h-4 w-4 cursor-pointer" />
</Button>
),
cell: ({ row }) => (
<div className="flex items-center gap-3">
{/* Avatar */}
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center overflow-hidden">
{row.getValue("foto") ? (
<img
src={row.getValue("foto")}
alt={row.getValue("nome") || "Avatar"}
className="w-full h-full object-cover"
/>
) : (
<span className="text-sm font-medium text-gray-700">
{GetNameInitials(row.getValue("nome"))}
</span>
)}
</div>
{/* Nome e Email */}
<div>
<div className="font-semibold text-gray-900">
{row.getValue("nome") || "-"}
</div>
<div className="text-sm text-gray-500">
{row.getValue("email") || "-"}
</div>
</div>
</div>
),
},
{
accessorKey: "cpf",
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")
),
},
{
accessorKey: "telefone",
header: "Telefone",
cell: ({ row }) => (
<span>{row.getValue("telefone")}</span>
),
},
{
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 (
<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>
);
},
enableHiding: false,
},
];
export default function TPessoaFisica() {
@ -274,31 +120,6 @@ export default function TPessoaFisica() {
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
*/
@ -316,113 +137,12 @@ export default function TPessoaFisica() {
buttonAction={() => { handleOpenForm(null) }}
/>
{/* Tabela de andamentos */}
<Card>
<CardContent>
{/* Tabela de Registros */}
<TPessoaTable
data={tPessoa}
onEdit={handleOpenForm}
onDelete={handleConfirmDelete}
onDelete={() => { }}
onEdit={() => { }}
/>
</CardContent>
</Card>
<div className="p-4">
{/* Filtro */}
<div className="flex items-center mb-4 space-x-2">
<Input
placeholder="Buscar por email..."
value={(table.getColumn("foto_nome_email")?.getFilterValue() as string) ?? ""}
onChange={(e) => table.getColumn("foto_nome_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>
{/* Modal de confirmação */}
<ConfirmDialog

View file

@ -0,0 +1,217 @@
'use client';
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from "@/components/ui/table";
import { EllipsisIcon, PencilIcon, Trash2Icon } from "lucide-react";
import TPessoaInterface from "../../_interfaces/TPessoaInterface";
interface TPessoaTableProps {
data: TPessoaInterface[];
onEdit: (item: TPessoaInterface, isEditingFormStatus: boolean) => void;
onDelete: (item: TPessoaInterface, isEditingFormStatus: boolean) => void;
}
/**
* Renderiza o badge de situação
*/
function StatusBadge({ situacao }: { situacao: string }) {
const isActive = situacao === "A";
const baseClasses =
"text-xs font-medium px-2.5 py-0.5 rounded-sm me-2";
const activeClasses =
"bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300";
const inactiveClasses =
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300";
return (
<span className={`${baseClasses} ${isActive ? activeClasses : inactiveClasses}`}>
{isActive ? "Ativo" : "Inativo"}
</span>
);
}
export default function TCensecTable({
data,
onEdit,
onDelete
}: TPessoaTableProps) {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-16 text-center">#</TableHead>
<TableHead className="w-32 text-center">Tipo</TableHead>
<TableHead>
Nome / Email
</TableHead>
<TableHead className="w-44">CPF / CNPJ</TableHead>
<TableHead className="w-44">Telefone</TableHead>
<TableHead className="w-52">Cidade / UF</TableHead>
<TableHead className="w-36">Data Cadastro</TableHead>
<TableHead className="w-28 text-center">Status</TableHead>
<TableHead className="text-right w-20">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((item) => (
<TableRow
key={item.pessoa_id}
className="hover:bg-muted/50 transition-colors cursor-pointer"
>
{/* ID */}
<TableCell className="text-center font-medium">
{Number(item.pessoa_id)}
</TableCell>
{/* Tipo de pessoa */}
<TableCell className="text-center">
<span
className={`px-2 py-1 rounded-full text-xs font-medium ${item.pessoa_tipo === "F"
? "bg-blue-100 text-blue-700"
: "bg-green-100 text-green-700"
}`}
>
{item.pessoa_tipo === "F" ? "Física" : "Jurídica"}
</span>
</TableCell>
{/* Nome e Email*/}
<TableCell>
<div className="flex items-center gap-3">
{/* Avatar */}
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center overflow-hidden">
{item?.foto ? (
<img
src={item?.foto}
alt={item.nome || "Avatar"}
className="w-full h-full object-cover"
/>
) : (
<span className="text-sm font-medium text-gray-700">
{item.nome
? item.nome
.split(" ")
.slice(0, 2) // Pega no máximo 2 palavras
.map((n) => n[0])
.join("")
.toUpperCase()
: "?"}
</span>
)}
</div>
{/* Nome e Email */}
<div>
<div className="font-semibold text-gray-900">
{item.nome || "-"}
</div>
<div className="text-sm text-gray-500">
{item.email || "-"}
</div>
</div>
</div>
</TableCell>
{/* CPF/CNPJ */}
<TableCell>
{item.cpf_cnpj
? item.pessoa_tipo === "F"
? item.cpf_cnpj.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4")
: item.cpf_cnpj.replace(/(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})/, "$1.$2.$3/$4-$5")
: "-"}
</TableCell>
{/* Telefone */}
<TableCell>
{item.telefone
? `(${item.ddd || ""}) ${item.telefone.replace(/(\d{5})(\d{4})/, "$1-$2")}`
: "-"}
</TableCell>
{/* Cidade / UF */}
<TableCell>
{item.cidade && item.uf ? `${item.cidade} - ${item.uf}` : "-"}
</TableCell>
{/* Data Cadastro */}
<TableCell>
{item.data_cadastro
? new Date(item.data_cadastro).toLocaleDateString("pt-BR")
: "-"}
</TableCell>
{/* Status (Exemplo: enviado_cnncnb) */}
<TableCell className="text-center">
<span
className={`px-2 py-1 rounded-full text-xs font-medium ${item.enviado_cnncnb
? "bg-green-100 text-green-700"
: "bg-gray-200 text-gray-700"
}`}
>
{item.enviado_cnncnb ? "Enviado" : "Pendente"}
</span>
</TableCell>
{/* Ações */}
<TableCell className="text-right">
<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(item, true)}
>
<PencilIcon className="mr-2 h-4 w-4" />
Editar
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="cursor-pointer text-red-600"
onSelect={() => onDelete(item, true)}
>
<Trash2Icon className="mr-2 h-4 w-4" />
Remover
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}

View file

@ -18,8 +18,11 @@ import {
TableRow
} from "@/components/ui/table";
import { EllipsisIcon, PencilIcon, Trash2Icon } from "lucide-react";
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";
interface TPessoaTableProps {
data: TPessoaInterface[];
@ -27,170 +30,157 @@ interface TPessoaTableProps {
onDelete: (item: TPessoaInterface, isEditingFormStatus: boolean) => void;
}
/**
* Renderiza o badge de situação
*/
function StatusBadge({ situacao }: { situacao: string }) {
const isActive = situacao === "A";
const pessoas = await TPessoaIndexData();
const baseClasses =
"text-xs font-medium px-2.5 py-0.5 rounded-sm me-2";
const columns: ColumnDef<TPessoaInterface>[] = [
const activeClasses =
"bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300";
const inactiveClasses =
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300";
return (
<span className={`${baseClasses} ${isActive ? activeClasses : inactiveClasses}`}>
{isActive ? "Ativo" : "Inativo"}
</span>
);
}
export default function TCensecTable({
data,
onEdit,
onDelete
}: TPessoaTableProps) {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-16 text-center">#</TableHead>
<TableHead className="w-32 text-center">Tipo</TableHead>
<TableHead>
Nome / Email
</TableHead>
<TableHead className="w-44">CPF / CNPJ</TableHead>
<TableHead className="w-44">Telefone</TableHead>
<TableHead className="w-52">Cidade / UF</TableHead>
<TableHead className="w-36">Data Cadastro</TableHead>
<TableHead className="w-28 text-center">Status</TableHead>
<TableHead className="text-right w-20">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((item) => (
<TableRow
key={item.pessoa_id}
className="hover:bg-muted/50 transition-colors cursor-pointer"
// ID
{
accessorKey: "pessoa_id",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
{/* ID */}
<TableCell className="text-center font-medium">
{Number(item.pessoa_id)}
</TableCell>
# <ArrowUpDownIcon className="ml-1 h-4 w-4" />
</Button>
),
cell: ({ row }) => (
Number(row.getValue("pessoa_id"))
),
},
{/* Tipo de pessoa */}
<TableCell className="text-center">
<span
className={`px-2 py-1 rounded-full text-xs font-medium ${item.pessoa_tipo === "F"
? "bg-blue-100 text-blue-700"
: "bg-green-100 text-green-700"
}`}
// Nome / Email / Foto
{
id: "nome_completo",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
{item.pessoa_tipo === "F" ? "Física" : "Jurídica"}
</span>
</TableCell>
{/* Nome e Email*/}
<TableCell>
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">
{/* Avatar */}
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center overflow-hidden">
{item?.foto ? (
{pessoa.foto ? (
<img
src={item?.foto}
alt={item.nome || "Avatar"}
src={pessoa.foto}
alt={pessoa.nome || "Avatar"}
className="w-full h-full object-cover"
/>
) : (
<span className="text-sm font-medium text-gray-700">
{item.nome
? item.nome
.split(" ")
.slice(0, 2) // Pega no máximo 2 palavras
.map((n) => n[0])
.join("")
.toUpperCase()
: "?"}
{GetNameInitials(pessoa.nome)}
</span>
)}
</div>
{/* Nome e Email */}
<div>
<div className="font-semibold text-gray-900">
{item.nome || "-"}
{pessoa.nome || "-"}
</div>
<div className="text-sm text-gray-500">
{item.email || "-"}
<div className="text-sm text-gray-500">{pessoa.email || "-"}</div>
</div>
</div>
</div>
</TableCell>
);
},
sortingFn: (a, b) => {
const nameA = a.original.nome?.toLowerCase() || "";
const nameB = b.original.nome?.toLowerCase() || "";
return nameA.localeCompare(nameB);
},
},
{/* CPF/CNPJ */}
<TableCell>
{item.cpf_cnpj
? item.pessoa_tipo === "F"
? item.cpf_cnpj.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4")
: item.cpf_cnpj.replace(/(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})/, "$1.$2.$3/$4-$5")
: "-"}
</TableCell>
{/* Telefone */}
<TableCell>
{item.telefone
? `(${item.ddd || ""}) ${item.telefone.replace(/(\d{5})(\d{4})/, "$1-$2")}`
: "-"}
</TableCell>
{/* Cidade / UF */}
<TableCell>
{item.cidade && item.uf ? `${item.cidade} - ${item.uf}` : "-"}
</TableCell>
{/* Data Cadastro */}
<TableCell>
{item.data_cadastro
? new Date(item.data_cadastro).toLocaleDateString("pt-BR")
: "-"}
</TableCell>
{/* Status (Exemplo: enviado_cnncnb) */}
<TableCell className="text-center">
<span
className={`px-2 py-1 rounded-full text-xs font-medium ${item.enviado_cnncnb
? "bg-green-100 text-green-700"
: "bg-gray-200 text-gray-700"
}`}
>
{item.enviado_cnncnb ? "Enviado" : "Pendente"}
</span>
</TableCell>
{/* Ações */}
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
// CPF
{
accessorKey: "cpf_cnpj",
header: ({ column }) => (
<Button
variant="ghost"
size="icon"
className="cursor-pointer"
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={() => onEdit(item, true)}
>
<DropdownMenuItem className="cursor-pointer" onSelect={() => { }}>
<PencilIcon className="mr-2 h-4 w-4" />
Editar
</DropdownMenuItem>
@ -199,7 +189,7 @@ export default function TCensecTable({
<DropdownMenuItem
className="cursor-pointer text-red-600"
onSelect={() => onDelete(item, true)}
onSelect={() => { }}
>
<Trash2Icon className="mr-2 h-4 w-4" />
Remover
@ -207,11 +197,21 @@ export default function TCensecTable({
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
},
enableSorting: false,
enableHiding: false,
},
];
export default function TPessoaTable({
data,
onEdit,
onDelete
}: TPessoaTableProps) {
return (
<div>
aqui
</div>
);
}

View file

@ -59,4 +59,5 @@ export default interface TPessoaInterface {
tb_tipologradouro_id?: number;
unidade?: string;
numero_end?: string;
foto?: string;
}

View file

@ -0,0 +1,134 @@
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";
interface dataTableProps {
data: any,
columns: any
}
export default function dataTable({ data, columns }, dataTableProps) {
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: data,
columns,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
return (
<div>
{/* 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>
</div>
);
}