[MVPTN-88] commit de backup
This commit is contained in:
parent
920b9d8aa6
commit
a045b3ca72
9 changed files with 1007 additions and 464 deletions
7
package-lock.json
generated
7
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,4 +59,5 @@ export default interface TPessoaInterface {
|
|||
tb_tipologradouro_id?: number;
|
||||
unidade?: string;
|
||||
numero_end?: string;
|
||||
foto?: string;
|
||||
}
|
||||
134
src/app/_components/dataTable/dataTable.tsx
Normal file
134
src/app/_components/dataTable/dataTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue