[MVPTN-88] feat(DataTable): Cria componentes da DataTable para sere reutilizados por outras partes do sistema
This commit is contained in:
parent
a045b3ca72
commit
3fe6ed8c08
11 changed files with 2767 additions and 3469 deletions
23
src/actions/CPF/FormatCPF.ts
Normal file
23
src/actions/CPF/FormatCPF.ts
Normal 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"
|
||||
);
|
||||
}
|
||||
53
src/actions/dateTime/FormatDateTime.ts
Normal file
53
src/actions/dateTime/FormatDateTime.ts
Normal 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}` : ""}`;
|
||||
}
|
||||
47
src/actions/phone/FormatPhone.ts
Normal file
47
src/actions/phone/FormatPhone.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -140,8 +140,8 @@ export default function TPessoaFisica() {
|
|||
{/* Tabela de Registros */}
|
||||
<TPessoaTable
|
||||
data={tPessoa}
|
||||
onDelete={() => { }}
|
||||
onEdit={() => { }}
|
||||
onDelete={handleConfirmDelete}
|
||||
onEdit={handleOpenForm}
|
||||
/>
|
||||
|
||||
{/* Modal de confirmação */}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue