[MVPTN-37] feat(Item): Finaliza o vinculo de pessoa ao item de pedido

This commit is contained in:
Keven 2025-11-11 13:58:27 -03:00
parent b85cd6aeb9
commit 6fe6c86b5d
13 changed files with 506 additions and 131 deletions

51
package-lock.json generated
View file

@ -22,7 +22,7 @@
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
@ -2132,12 +2132,12 @@
}
},
"node_modules/@radix-ui/react-separator": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
"integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==",
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz",
"integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3"
"@radix-ui/react-primitive": "2.1.4"
},
"peerDependencies": {
"@types/react": "*",
@ -2154,6 +2154,47 @@
}
}
},
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
"integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.4"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
"integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",

View file

@ -24,7 +24,7 @@
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",

View file

@ -5,6 +5,7 @@ import {
Frame,
GalleryVerticalEnd,
HouseIcon,
SquareMousePointer,
SquareTerminal,
UsersIcon
} from 'lucide-react';
@ -46,7 +47,7 @@ const data = {
{
title: 'Servicos',
url: '#',
icon: UsersIcon,
icon: SquareMousePointer,
isActive: false,
items: [
{

193
src/components/ui/item.tsx Normal file
View file

@ -0,0 +1,193 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Separator } from "@/components/ui/separator"
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
role="list"
data-slot="item-group"
className={cn("group/item-group flex flex-col", className)}
{...props}
/>
)
}
function ItemSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="item-separator"
orientation="horizontal"
className={cn("my-0", className)}
{...props}
/>
)
}
const itemVariants = cva(
"group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
{
variants: {
variant: {
default: "bg-transparent",
outline: "border-border",
muted: "bg-muted/50",
},
size: {
default: "p-4 gap-4 ",
sm: "py-3 px-4 gap-2.5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Item({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"div"> &
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div"
return (
<Comp
data-slot="item"
data-variant={variant}
data-size={size}
className={cn(itemVariants({ variant, size, className }))}
{...props}
/>
)
}
const itemMediaVariants = cva(
"flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5",
{
variants: {
variant: {
default: "bg-transparent",
icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
image:
"size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover",
},
},
defaultVariants: {
variant: "default",
},
}
)
function ItemMedia({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
return (
<div
data-slot="item-media"
data-variant={variant}
className={cn(itemMediaVariants({ variant, className }))}
{...props}
/>
)
}
function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-content"
className={cn(
"flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
className
)}
{...props}
/>
)
}
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-title"
className={cn(
"flex w-fit items-center gap-2 text-sm leading-snug font-medium",
className
)}
{...props}
/>
)
}
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="item-description"
className={cn(
"text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance",
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
className
)}
{...props}
/>
)
}
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-actions"
className={cn("flex items-center gap-2", className)}
{...props}
/>
)
}
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-header"
className={cn(
"flex basis-full items-center justify-between gap-2",
className
)}
{...props}
/>
)
}
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-footer"
className={cn(
"flex basis-full items-center justify-between gap-2",
className
)}
{...props}
/>
)
}
export {
Item,
ItemMedia,
ItemContent,
ItemActions,
ItemGroup,
ItemSeparator,
ItemTitle,
ItemDescription,
ItemHeader,
ItemFooter,
}

View file

@ -1,13 +1,13 @@
'use client';
"use client"
import * as React from 'react';
import * as SeparatorPrimitive from '@radix-ui/react-separator';
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from '@/lib/utils';
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = 'horizontal',
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
@ -17,12 +17,12 @@ function Separator({
decorative={decorative}
orientation={orientation}
className={cn(
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
className,
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>
);
)
}
export { Separator };
export { Separator }

View file

@ -2,6 +2,7 @@ import { ColumnDef } from '@tanstack/react-table';
import { ArrowUpDownIcon } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { FormatCPF } from '@/shared/actions/CPF/FormatCPF';
import { FormatDateTime } from '@/shared/actions/dateTime/FormatDateTime';
import { FormatPhone } from '@/shared/actions/phone/FormatPhone';
@ -13,8 +14,26 @@ import TPessoaInterface from '../../interfaces/TPessoa/TPessoaInterface';
/**
* Função para criar a definição das colunas da tabela
*/
export function TPessoaTableFormColumnsDialog(): ColumnDef<TPessoaInterface>[] {
export function TPessoaTableFormColumnsDialog(setSelectedTPessoa: React.Dispatch<React.SetStateAction<any | null>>): ColumnDef<TPessoaInterface>[] {
return [
{
id: 'select',
header: '',
cell: ({ row, table }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => {
// Limpa todas as seleções antes de selecionar uma nova
table.resetRowSelection();
row.toggleSelected(!!value);
setSelectedTPessoa(value ? row.original : null);
}}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
// ID
{
accessorKey: 'pessoa_id',

View file

@ -10,19 +10,17 @@ import {
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTitle
} from '@/components/ui/dialog';
import { Form } from '@/components/ui/form';
import { useTPessoaFisicaIndexHook } from '@/packages/administrativo/hooks/TPessoa/TPessoaFisica/useTPessoaFisicaIndexHook';
import { useTPessoaRepresentanteFormHook } from '@/packages/administrativo/hooks/TPessoaRepresentante/useTPessoaRepresentanteFormHook';
import { useTPessoaJuridicaIndexHook } from '@/packages/administrativo/hooks/TPessoa/TPessoaJuridica/useTPessoaJuridicaIndexHook';
import TPessoaInterface from '@/packages/administrativo/interfaces/TPessoa/TPessoaInterface';
import TPessoaTableFormInterface from '@/packages/administrativo/interfaces/TPessoa/TPessoaTableFormInterface';
import { DataTable } from '@/shared/components/dataTable/DataTable';
import LoadingButton from '@/shared/components/loadingButton/LoadingButton';
import { useTPessoaJuridicaIndexHook } from '../../hooks/TPessoa/TPessoaJuridica/useTPessoaJuridicaIndexHook';
import { TPessoaTableFormColumnsDialog } from './TPessoaTableFormColumnsDialog';
export default function TPessoaTableFormDialog({
isOpen,
tipoPessoa,
@ -34,9 +32,9 @@ export default function TPessoaTableFormDialog({
const { tPessoaFisica, fetchTPessoaFisica } = useTPessoaFisicaIndexHook();
const { tPessoaJuridica, fetchTPessoaJuridica } = useTPessoaJuridicaIndexHook();
const [pessoas, setPessoas] = useState<any>()
const [selectedTPessoa, setSelectedTPessoa] = useState<TPessoaInterface | null>(null);
const form = useTPessoaRepresentanteFormHook();
// Executa o Hook de Acordo com o tipo de pessoa informado
const loadData = async (tipoPessoa: string) => {
switch (tipoPessoa) {
@ -55,29 +53,34 @@ export default function TPessoaTableFormDialog({
};
// Atualiza a variavel de pessoa quando tiver alteração na variavel de pessoas fisicas
useEffect(() => {
setPessoas(tPessoaFisica)
}, [tPessoaFisica])
// Atualiza a variavel de pessoa quando tiver alteração na variavel de pessoas juridicas
useEffect(() => {
setPessoas(tPessoaJuridica)
}, [tPessoaJuridica])
// Executa o hook correspondente ao tipo de pessoa, sempre que o tipo pessoa mudar
useEffect(() => {
// Verifica se o tipo pessoa esta preenchido
if (tipoPessoa) {
// Dispara o carregamento de informações
loadData(tipoPessoa);
}
}, [tipoPessoa]);
const columns = TPessoaTableFormColumnsDialog();
const columns = TPessoaTableFormColumnsDialog(setSelectedTPessoa);
return (
<Dialog
@ -91,37 +94,34 @@ export default function TPessoaTableFormDialog({
<DialogTitle>Pessoa</DialogTitle>
<DialogDescription>Busque a pessoa desejada</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSave)} className="space-y-6">
<div className="max-h-[50vh] overflow-y-auto">
<DataTable
data={pessoas}
columns={columns}
filterColumn="nome_completo"
filterPlaceholder="Buscar por nome ou email..."
/>
</div>
{/* Rodapé do Dialog */}
<DialogFooter className="mt-4">
<DialogClose asChild>
<Button
variant="outline"
type="button"
onClick={() => onClose(null, false)}
className="cursor-pointer"
>
Cancelar
</Button>
</DialogClose>
<LoadingButton
text="Salvar"
textLoading="Aguarde..."
loading={buttonIsLoading}
type="submit"
/>
</DialogFooter>
</form>
</Form>
<div className="max-h-[50vh] overflow-y-auto">
<DataTable
data={pessoas}
columns={columns}
filterColumn="nome_completo"
filterPlaceholder="Buscar por nome ou email..."
/>
</div>
{/* Rodapé do Dialog */}
<DialogFooter className="mt-4">
<DialogClose asChild>
<Button
variant="outline"
type="button"
onClick={() => onClose(null, false)}
className="cursor-pointer"
>
Cancelar
</Button>
</DialogClose>
<LoadingButton
text="Selecionar"
textLoading="Aguarde..."
loading={buttonIsLoading}
type="button"
onClick={() => { onSave(selectedTPessoa); onClose(null, false); }}
/>
</DialogFooter>
</DialogContent>
</Dialog>
);

View file

@ -1,9 +1,8 @@
import { TPessoaRepresentanteFormValues } from '../../schemas/TPessoaRepresentante/TPessoaRepresentanteSchema';
export default interface TPessoaTableFormInterface {
isOpen: boolean;
tipoPessoa: string;
onClose: (item: null, isFormStatus: boolean) => void;
onSave: (data: TPessoaRepresentanteFormValues) => void;
onSave: (data: any) => void;
buttonIsLoading: boolean;
}

View file

@ -83,13 +83,10 @@ export default function TServicoItemPedidoFormColumns(): ColumnDef<TServicoItemP
const value = Number(row.getValue('quantidade') ?? 1);
const min = 0;
const max = 999;
const updateData = (table.options.meta as TableMeta | undefined)?.updateData;
const setNext = (next: number) => {
if (updateData) updateData(row.index, 'quantidade', next);
};
const dec = () => setNext(Math.max(min, value - 1));
const inc = () => setNext(Math.min(max, value + 1));

View file

@ -1,6 +1,6 @@
'use client';
import { CreditCard, Package, UserSquare2 } from 'lucide-react';
import { CreditCard, Package, TrashIcon, UserSquare2 } from 'lucide-react';
import { useRouter } from 'next/navigation';
import * as React from 'react';
import { useCallback, useState } from 'react';
@ -16,6 +16,7 @@ import {
FormMessage
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Item, ItemActions, ItemContent, ItemDescription, ItemTitle } from '@/components/ui/item';
import GEmolumentoServicoSelect from '@/packages/administrativo/components/GEmolumento/GEmolumentoServicoSelect';
import GUsuarioSelect from '@/packages/administrativo/components/GUsuario/GUsuarioSelect';
import TPessoaTableFormDialog from '@/packages/administrativo/components/TPessoa/TPessoaTableFormDialog';
@ -62,7 +63,6 @@ export default function TServicoPedidoForm({ servico_pedido_id }: TServicoPedido
* Fecha o formulário e limpa o andamento selecionado
*/
const handleCloseForm = useCallback(() => {
console.log('handleCloseForm');
setIsFormOpen(false);
}, []);
@ -123,28 +123,101 @@ export default function TServicoPedidoForm({ servico_pedido_id }: TServicoPedido
const handleAddItem = async () => {
const servicoEEmolumento = {
servico_tipo: servicoAtual,
emolumento: emolumentoAtual,
};
// Adiciona o item remotamente (se for necessário)
const newItem = await addTServicoItemPedido(servicoEEmolumento);
// Garante que o item seja adicionado no estado local (para a tabela renderizar)
if (newItem) localAddTServicoItemPedido(newItem);
};
const handleAddItemAndTPessoa = async (selectedTPessoa: any) => {
// Monta o payload principal com serviço e emolumento
const servicoEEmolumento = {
servico_tipo: servicoAtual,
emolumento: emolumentoAtual,
};
// Adiciona o item remotamente (se for necessário)
const newItem = await addTServicoItemPedido(servicoEEmolumento);
// Atribui dados principais da pessoa
newItem.pessoa_id = selectedTPessoa.pessoa_id;
// Cria a subview (conteúdo dinâmico a ser renderizado abaixo da linha)
newItem.subview = (
<div className="p-5">
<Item variant="outline">
<ItemContent>
<ItemTitle>
{selectedTPessoa.cpf_cnpj} - {selectedTPessoa.nome}
</ItemTitle>
<ItemDescription>
{selectedTPessoa.email}
</ItemDescription>
</ItemContent>
<ItemActions>
<Button variant="outline" size="sm">
<TrashIcon />
</Button>
</ItemActions>
</Item>
</div>
);
// Adiciona o item no estado local (para renderizar na tabela)
if (newItem) {
localAddTServicoItemPedido(newItem);
}
};
const handleSelectTServicoTipo = async () => {
if (servicoAtual && emolumentoAtual) {
// const servicoEEmolumento = {
// Verifica se deve selecionar a pessoa
if (servicoAtual.tipo_pessoa) {
// servico_tipo: servicoAtual,
// emolumento: emolumentoAtual,
setServicoTipoPessoa(servicoAtual.tipo_pessoa)
// };
setIsFormOpen(true);
// // Adiciona o item remotamente (se for necessário)
// const newItem = await addTServicoItemPedido(servicoEEmolumento);
}
else {
// // Garante que o item seja adicionado no estado local (para a tabela renderizar)
// if (newItem) localAddTServicoItemPedido(newItem);
handleAddItem()
setServicoTipoPessoa(servicoAtual.tipo_pessoa)
setIsFormOpen(true);
}
}
};
}
const sections: StepSection[] = [
{ key: 'pedido', id: 'selectPedido', icon: <Package className="h-4 w-4" />, title: 'Pedido', description: 'Dados gerais do pedido.' },
@ -368,7 +441,7 @@ export default function TServicoPedidoForm({ servico_pedido_id }: TServicoPedido
/>
</div>
<div className="col-span-12 sm:col-span-12 md:col-span-12 text-end">
<Button type="button" onClick={handleAddItem}>
<Button type="button" onClick={handleSelectTServicoTipo}>
+ Adicionar
</Button>
</div>
@ -496,17 +569,17 @@ export default function TServicoPedidoForm({ servico_pedido_id }: TServicoPedido
</main>
</div>
</form>
{/* Formulário de criação/edição */}
{isFormOpen && (
<TPessoaTableFormDialog
isOpen={isFormOpen}
tipoPessoa={servicoTipoPessoa}
onClose={handleCloseForm}
onSave={handleAddItemAndTPessoa}
buttonIsLoading={buttonIsLoading}
/>
)}
</Form>
{/* Formulário de criação/edição */}
{isFormOpen && (
<TPessoaTableFormDialog
isOpen={isFormOpen}
tipoPessoa={servicoTipoPessoa}
onClose={handleCloseForm}
onSave={handleSave}
buttonIsLoading={buttonIsLoading}
/>
)}
</div>
);
}

View file

@ -8,19 +8,19 @@ export const TServicoPedidoSchema = z.object({
valor_pedido: z.number().optional(),
valor_pago: z.number().optional(),
usuario_id: z.number().optional(),
data_pedido: z.string().optional(),
data_pedido: z.union([z.string(), z.null()]),
mensalista_livrocaixa_id: z.union([z.number(), z.null()]),
observacao: z.union([z.string(), z.null()]),
escrevente_id: z.number(),
situacao: z.string().optional(),
estornado: z.string().optional(),
estornado: z.union([z.string(), z.null()]),
nfse_id: z.union([z.number(), z.null()]),
apresentante: z.string().optional(),
cpfcnpj_apresentante: z.string().optional(),
selo_pessoa_nome: z.string().optional(),
selo_pessoa_cpfcnpj: z.string().optional(),
login: z.string().optional(),
funcao: z.string().optional(),
funcao: z.union([z.string(), z.null()]),
itens: z.array(TServicoItemPedidoSchema).default([]),
servico_tipo_id: z.object().optional(),
emolumento_id: z.object().optional(),

View file

@ -35,7 +35,19 @@ import {
import DataTableInterface from './interfaces/DataTableInterface';
export function DataTable<TData>({
/**
* DataTable genérico com suporte a subvisões dinâmicas (subtabelas ou detalhes).
* O conteúdo extra pode ser definido dinamicamente por linha, em `row.original.subview`.
*
* Exemplo de item:
* {
* id: 1,
* descricao: 'Item principal',
* valor: 100,
* subview: <CustomComponente detalhes={...} />,
* }
*/
export function DataTable<TData extends { subview?: React.ReactNode | (() => React.ReactNode) }>({
data,
columns,
filterColumn,
@ -44,7 +56,6 @@ export function DataTable<TData>({
onDelete,
onRowClick,
}: DataTableInterface<TData>) {
// Garante que data sempre seja array
const safeData = Array.isArray(data) ? data : [];
// Estados internos da tabela
@ -53,7 +64,7 @@ export function DataTable<TData>({
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
// Configuração da tabela
// Cria a tabela
const table = useReactTable({
data: safeData,
columns: [
@ -66,12 +77,20 @@ export function DataTable<TData>({
cell: ({ row }: any) => (
<div className="flex gap-2">
{onEdit && (
<Button variant="ghost" size="sm" onClick={() => onEdit(row.original)}>
<Button
variant="ghost"
size="sm"
onClick={() => onEdit(row.original)}
>
Editar
</Button>
)}
{onDelete && (
<Button variant="destructive" size="sm" onClick={() => onDelete(row.original)}>
<Button
variant="destructive"
size="sm"
onClick={() => onDelete(row.original)}
>
Excluir
</Button>
)}
@ -81,12 +100,7 @@ export function DataTable<TData>({
]
: []),
],
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
state: { sorting, columnFilters, columnVisibility, rowSelection },
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
@ -99,16 +113,22 @@ export function DataTable<TData>({
return (
<div className="space-y-4">
{/* Filtros e colunas */}
{/* 🔍 Filtros e colunas */}
<div className="flex items-center gap-2">
{filterColumn && (
<Input
placeholder={filterPlaceholder}
value={(table.getColumn(filterColumn)?.getFilterValue() as string) ?? ''}
onChange={(e) => table.getColumn(filterColumn)?.setFilterValue(e.target.value)}
value={
(table.getColumn(filterColumn)?.getFilterValue() as string) ?? ''
}
onChange={(e) =>
table.getColumn(filterColumn)?.setFilterValue(e.target.value)
}
className="w-full"
/>
)}
{/* Menu de colunas visíveis */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto cursor-pointer">
@ -133,9 +153,11 @@ export function DataTable<TData>({
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* Tabela */}
{/* 🧱 Tabela principal */}
<div className="overflow-hidden rounded-md border">
<Table>
{/* Cabeçalho */}
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
@ -143,30 +165,64 @@ export function DataTable<TData>({
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
{/* Corpo */}
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
className={onRowClick ? 'hover:bg-muted/50 cursor-pointer' : ''}
onClick={() => onRowClick?.(row.original)}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
table.getRowModel().rows.map((row) => {
const subview =
typeof row.original.subview === 'function'
? row.original.subview()
: row.original.subview;
return (
<React.Fragment key={row.id}>
{/* Linha principal */}
<TableRow
className={
onRowClick ? 'hover:bg-muted/50 cursor-pointer' : ''
}
onClick={() => onRowClick?.(row.original)}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
{/* Subview dinâmica (qualquer conteúdo) */}
{subview && (
<TableRow className="bg-muted/10">
<TableCell
colSpan={row.getVisibleCells().length}
className="p-0"
>
{subview}
</TableCell>
</TableRow>
)}
</React.Fragment>
);
})
) : (
<TableRow>
<TableCell colSpan={table.getAllColumns().length} className="h-24 text-center">
<TableCell
colSpan={table.getAllColumns().length}
className="h-24 text-center"
>
Nenhum resultado encontrado.
</TableCell>
</TableRow>
@ -174,10 +230,12 @@ export function DataTable<TData>({
</TableBody>
</Table>
</div>
{/* Paginação */}
{/* 📄 Paginação */}
<div className="flex items-center justify-between gap-4">
<span className="text-muted-foreground text-sm">
Página {table.getState().pagination.pageIndex + 1} de {table.getPageCount()}
Página {table.getState().pagination.pageIndex + 1} de{' '}
{table.getPageCount()}
</span>
<div className="flex items-center gap-2">
<Button
@ -185,8 +243,6 @@ export function DataTable<TData>({
size="sm"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
aria-label="Primeira página"
className="cursor-pointer"
type="button"
>
Primeira
@ -196,8 +252,6 @@ export function DataTable<TData>({
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
aria-label="Página anterior"
className="cursor-pointer"
type="button"
>
<ChevronLeftIcon className="h-4 w-4" />
@ -208,8 +262,6 @@ export function DataTable<TData>({
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
aria-label="Próxima página"
className="cursor-pointer"
type="button"
>
Próxima
@ -218,10 +270,10 @@ export function DataTable<TData>({
<Button
variant="outline"
size="sm"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
onClick={() =>
table.setPageIndex(table.getPageCount() - 1)
}
disabled={!table.getCanNextPage()}
aria-label="Última página"
className="cursor-pointer"
type="button"
>
Última

View file

@ -41,4 +41,4 @@ const LoadingButton = forwardRef<HTMLButtonElement, LoadingButtonProps>(
LoadingButton.displayName = 'LoadingButton';
export default LoadingButton;
export default LoadingButton;