[MVPTN-37] feat(ItensPedido): Implementa parcialmente os pessoas vinculadas aos itens dos pedidos

This commit is contained in:
Keven 2025-11-10 18:21:14 -03:00
parent 88cb6f67c8
commit b85cd6aeb9
11 changed files with 440 additions and 101 deletions

View file

@ -1,7 +1,7 @@
'use client';
import { HouseIcon, IdCardIcon, UserIcon } from 'lucide-react';
import React, { useEffect } from 'react';
import { useEffect } from 'react';
import { Button } from '@/components/ui/button';
import {

View file

@ -0,0 +1,142 @@
import { ColumnDef } from '@tanstack/react-table';
import { ArrowUpDownIcon } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { FormatCPF } from '@/shared/actions/CPF/FormatCPF';
import { FormatDateTime } from '@/shared/actions/dateTime/FormatDateTime';
import { FormatPhone } from '@/shared/actions/phone/FormatPhone';
import GetNameInitials from '@/shared/actions/text/GetNameInitials';
import empty from '@/shared/actions/validations/empty';
import TPessoaInterface from '../../interfaces/TPessoa/TPessoaInterface';
/**
* Função para criar a definição das colunas da tabela
*/
export function TPessoaTableFormColumnsDialog(): 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,
},
// 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;
return (
<div className="flex items-center gap-3">
{/* Foto ou Iniciais */}
<div className="flex h-10 w-10 items-center justify-center overflow-hidden rounded-full bg-gray-200">
{pessoa.foto ? (
<img
src={pessoa.foto}
alt={pessoa.nome || 'Avatar'}
className="h-full w-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>
);
},
sortingFn: (a, b) =>
(a.original.nome?.toLowerCase() || '').localeCompare(b.original.nome?.toLowerCase() || ''),
},
// 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')),
},
// 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')),
},
// 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()),
},
// 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',
},
];
}

View file

@ -0,0 +1,128 @@
'use client';
import { useEffect, useState } from 'react';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
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 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,
onClose,
onSave,
buttonIsLoading,
}: TPessoaTableFormInterface) {
const { tPessoaFisica, fetchTPessoaFisica } = useTPessoaFisicaIndexHook();
const { tPessoaJuridica, fetchTPessoaJuridica } = useTPessoaJuridicaIndexHook();
const [pessoas, setPessoas] = useState<any>()
const form = useTPessoaRepresentanteFormHook();
const loadData = async (tipoPessoa: string) => {
switch (tipoPessoa) {
case "F":
await fetchTPessoaFisica();
break;
case "J":
await fetchTPessoaJuridica();
break;
}
};
useEffect(() => {
setPessoas(tPessoaFisica)
}, [tPessoaFisica])
useEffect(() => {
setPessoas(tPessoaJuridica)
}, [tPessoaJuridica])
useEffect(() => {
if (tipoPessoa) {
loadData(tipoPessoa);
}
}, [tipoPessoa]);
const columns = TPessoaTableFormColumnsDialog();
return (
<Dialog
open={isOpen}
onOpenChange={(open) => {
if (!open) onClose(null, false);
}}
>
<DialogContent className="max-h-[70vh] w-full max-w-full overflow-auto p-6 sm:max-w-4xl md:max-w-6xl lg:max-w-6xl">
<DialogHeader>
<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>
</DialogContent>
</Dialog>
);
}

View file

@ -1,6 +1,6 @@
'use client';
import React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { Button } from '@/components/ui/button';
import {
@ -17,11 +17,11 @@ import { ResetFormIfData } from '@/shared/actions/form/ResetFormIfData';
import { DataTable } from '@/shared/components/dataTable/DataTable';
import LoadingButton from '@/shared/components/loadingButton/LoadingButton';
import TPessoasRepresentanteFormColumns from './TPessoasRepresentanteFormColumns';
import { useTPessoaFisicaIndexHook } from '../../hooks/TPessoa/TPessoaFisica/useTPessoaFisicaIndexHook';
import { useTPessoaRepresentanteFormHook } from '../../hooks/TPessoaRepresentante/useTPessoaRepresentanteFormHook';
import TPessoaRepresentanteInterface from '../../interfaces/TPessoaRepresentante/TPessoaRepresentanteInterface';
import TPessoaRepresentanteFormInterface from '../../interfaces/TPessoaRepresentante/TPessoaRepresentnateFormInterface';
import TPessoasRepresentanteFormColumns from './TPessoasRepresentanteFormColumns';
export default function TPessoaRepresentanteForm({
isOpen,

View file

@ -60,6 +60,7 @@ export default function TServicoTipoSelect({ field }: TServicoTipoSelectInterfac
servico_tipo_id: Number(item.servico_tipo_id),
descricao: item.descricao,
tipo_item: item.tipo_item,
tipo_pessoa: item.tipo_pessoa,
};
field.onChange(selectedValue);

View file

@ -15,6 +15,8 @@ export const useTPessoaFisicaIndexHook = () => {
setTPessoa(response.data);
setResponse(response);
return response.data
};
return { tPessoaFisica, fetchTPessoaFisica };

View file

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

View file

@ -18,10 +18,12 @@ import {
import { Input } from '@/components/ui/input';
import GEmolumentoServicoSelect from '@/packages/administrativo/components/GEmolumento/GEmolumentoServicoSelect';
import GUsuarioSelect from '@/packages/administrativo/components/GUsuario/GUsuarioSelect';
import TPessoaTableFormDialog from '@/packages/administrativo/components/TPessoa/TPessoaTableFormDialog';
import TServicoTipoSelect from '@/packages/administrativo/components/TServicoTipo/TServicoTipoSelect';
import TServicoItemPedidoFormTable from '@/packages/servicos/components/TServicoItemPedido/TServicoItemPedidoFormTable';
import { useTServicoItemPedidoAddHook } from '@/packages/servicos/hooks/TServicoItemPedido/useTServicoItemPedidoAddHook';
import { useTServicoItemPedidoIndexHook } from '@/packages/servicos/hooks/TServicoItemPedido/useTServicoItemPedidoIndexHook';
import { useTServicoItemPedidoLocalAddHook } from '@/packages/servicos/hooks/TServicoItemPedido/useTServicoItemPedidoLocalAddHook';
import { useTServicoPedidoFormHook } from '@/packages/servicos/hooks/TServicoPedido/useTServicoPedidoFormHook';
import { useTServicoPedidoSaveHook } from '@/packages/servicos/hooks/TServicoPedido/useTServicoPedidoSaveHook';
import { useTServicoPedidoShowHook } from '@/packages/servicos/hooks/TServicoPedido/useTServicoPedidoShowHook';
@ -32,7 +34,6 @@ import LoadingButton from '@/shared/components/loadingButton/LoadingButton';
import { StepNavigator, StepNavigatorRef, StepSection } from '@/shared/components/step/stepNavigator';
import TipoPagamentoSelect from '@/shared/components/tipoPagamento/TipoPagamentoSelect';
import { useTServicoItemPedidoLocalAddHook } from '../../hooks/TServicoItemPedido/useTServicoItemPedidoLocalAddHook';
export default function TServicoPedidoForm({ servico_pedido_id }: TServicoPedidoFormInterface) {
@ -50,11 +51,31 @@ export default function TServicoPedidoForm({ servico_pedido_id }: TServicoPedido
const { TServicoItemPedido, addTServicoItemPedido, setTServicoItemPedido } = useTServicoItemPedidoAddHook(setValue);
const { TServicoItemPedido: itemPedidoShow, indexTServicoItemPedido } = useTServicoItemPedidoIndexHook()
const { TServicoPedido: pedidoShow, showTServicoPedido } = useTServicoPedidoShowHook();
const [isFormOpen, setIsFormOpen] = useState(false);
const [servicoTipoPessoa, setServicoTipoPessoa] = useState('');
// Campos para serem monitorados
const servicoAtual = watch('servico_tipo_id');
const emolumentoAtual = watch('emolumento_id');
/**
* Fecha o formulário e limpa o andamento selecionado
*/
const handleCloseForm = useCallback(() => {
console.log('handleCloseForm');
setIsFormOpen(false);
}, []);
/**
* Salva os dados do formulário
*/
const handleSave = useCallback(
async (formData: any) => {
console.log('handleSave');
},
[handleCloseForm],
);
// BUsca o o pedido desejado
const TServicoPedidoShowData = async () => {
@ -84,39 +105,45 @@ export default function TServicoPedidoForm({ servico_pedido_id }: TServicoPedido
// Busca os itens do pedido
const TServicoPedidoItemIndexData = async (servico_pedido_id: number) => {
// Cria objeto de envio
const servicoPedido: TServicoPedidoInterface = {
const servicoPedido: TServicoPedidoInterface = { servico_pedido_id };
servico_pedido_id: servico_pedido_id
const response = await indexTServicoItemPedido(servicoPedido);
if (response?.data?.length > 0) {
// Adiciona todos de uma vez, criando uma nova referência de estado
setLocalTServicoItemPedido(response.data);
// (opcional) sincroniza com o react-hook-form
setValue('itens', response.data);
}
// Busca os itens
const response = await indexTServicoItemPedido(servicoPedido)
// Percorre todos os itens localizados
for (const item of response.data) {
// Adiciona os itens locais
localAddTServicoItemPedido(item)
}
}
};
const handleAddItem = async () => {
if (servicoAtual && emolumentoAtual) {
// Cria um novo objeto
const servicoEEmolumento = {
servico_tipo: servicoAtual,
emolumento: emolumentoAtual
}
// const servicoEEmolumento = {
await addTServicoItemPedido(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);
setServicoTipoPessoa(servicoAtual.tipo_pessoa)
setIsFormOpen(true);
}
};
const sections: StepSection[] = [
@ -168,6 +195,12 @@ export default function TServicoPedidoForm({ servico_pedido_id }: TServicoPedido
}, [])
React.useEffect(() => {
setValue('itens', TServicoItemPedidoLocal, { shouldDirty: true });
}, [TServicoItemPedidoLocal]);
return (
<div>
@ -464,6 +497,16 @@ export default function TServicoPedidoForm({ servico_pedido_id }: TServicoPedido
</div>
</form>
</Form>
{/* Formulário de criação/edição */}
{isFormOpen && (
<TPessoaTableFormDialog
isOpen={isFormOpen}
tipoPessoa={servicoTipoPessoa}
onClose={handleCloseForm}
onSave={handleSave}
buttonIsLoading={buttonIsLoading}
/>
)}
</div>
);
}

View file

@ -7,47 +7,64 @@ import { GCalculoServicoService } from '@/packages/administrativo/services/GCalc
import TServicoItemPedidoAddInterface from '@/packages/servicos/interfaces/TServicoItemPedido/TServicoItemPedidoAddInterface';
import TServicoItemPedidoIndexResponseInterface from '@/packages/servicos/interfaces/TServicoItemPedido/TServicoItemPedidoIndexResponseInterface';
import { useTServicoItemPedidoLocalAddHook } from './useTServicoItemPedidoLocalAddHook';
export const useTServicoItemPedidoAddHook = (setValue: UseFormSetValue<TServicoItemPedidoAddInterface>) => {
export const useTServicoItemPedidoAddHook = (setValue?: UseFormSetValue<TServicoItemPedidoAddInterface>) => {
const [TServicoItemPedido, setTServicoItemPedido] = useState<TServicoItemPedidoIndexResponseInterface[]>([]);
const { TServicoItemPedidoLocal, localAddTServicoItemPedido } = useTServicoItemPedidoLocalAddHook(setValue)
const addTServicoItemPedido = async (data: TServicoItemPedidoAddInterface) => {
data.sistema_id = 2
data.valor_documento = 0
data.quantidade = 1
data.emolumento_id = data.emolumento.emolumento_id
const response = await GCalculoServicoService(data)
const item = {
emolumento_id: data.emolumento.emolumento_id,
emolumento_item_id: response.data.emolumento_item_id,
servico_tipo_id: data.servico_tipo.servico_tipo_id,
tipo_item: data.servico_tipo.tipo_item,
descricao: data.servico_tipo.descricao,
tabela: data.emolumento.descricao,
situacao: 'F',
qtd: 1,
valor: response.data.valor_total,
emolumento: response.data.valor_emolumento,
fundesp: response.data.valor_fundos,
taxa_judiciaria: response.data.taxa_judiciaria,
valor_iss: response.data.valor_iss,
// Verifica dados obrigatórios
if (!data?.emolumento?.emolumento_id || !data?.servico_tipo?.servico_tipo_id) {
console.warn('⚠️ Dados inválidos em addTServicoItemPedido', data);
return null;
}
// Monta o payload normalizado
const payload = {
...data,
sistema_id: data.sistema_id ?? 2,
valor_documento: data.valor_documento ?? 0,
quantidade: data.quantidade ?? 1,
emolumento_id: data.emolumento.emolumento_id,
};
const response = await GCalculoServicoService(payload);
const result = response?.data ?? {};
const item: TServicoItemPedidoIndexResponseInterface = {
emolumento_id: result.emolumento_id ?? payload.emolumento_id ?? 0,
emolumento_item_id: result.emolumento_item_id ?? null,
servico_tipo_id: data.servico_tipo.servico_tipo_id,
tipo_item: data.servico_tipo.tipo_item ?? '',
descricao: data.servico_tipo.descricao ?? '',
tabela: data.emolumento?.descricao ?? '',
situacao: 'F',
qtd: 1,
valor: result.valor_total ?? 0,
emolumento: result.valor_emolumento ?? 0,
fundesp: result.valor_fundos ?? 0,
taxa_judiciaria: result.taxa_judiciaria ?? 0,
valor_iss: result.valor_iss ?? 0,
};
// Atualiza o estado com fallback seguro
setTServicoItemPedido((prev) => {
const novoItem = [...prev, item]
if (setValue) setValue('itens', novoItem)
return novoItem
const safePrev = Array.isArray(prev) ? prev : [];
const novoArray = [...safePrev, item];
if (setValue) {
// Protege contra sobrescrita incorreta do formulário
setValue('itens', novoArray, { shouldDirty: true });
}
return novoArray;
});
localAddTServicoItemPedido(item)
return item;
};

View file

@ -6,39 +6,36 @@ import { UseFormSetValue } from 'react-hook-form';
import TServicoItemPedidoAddInterface from '@/packages/servicos/interfaces/TServicoItemPedido/TServicoItemPedidoAddInterface';
import TServicoItemPedidoIndexResponseInterface from '@/packages/servicos/interfaces/TServicoItemPedido/TServicoItemPedidoIndexResponseInterface';
export const useTServicoItemPedidoLocalAddHook = (setValue: UseFormSetValue<TServicoItemPedidoAddInterface>) => {
export const useTServicoItemPedidoLocalAddHook = (setValue?: UseFormSetValue<TServicoItemPedidoAddInterface>) => {
const [TServicoItemPedidoLocal, setLocalTServicoItemPedido] = useState<TServicoItemPedidoIndexResponseInterface[]>([]);
const localAddTServicoItemPedido = async (data: TServicoItemPedidoAddInterface) => {
const item = {
emolumento_id: data.emolumento_id,
emolumento_item_id: data.emolumento_item_id,
servico_tipo_id: data.servico_tipo_id,
tipo_item: data.tipo_item,
descricao: data.descricao,
tabela: data.descricao,
situacao: data.situacao,
qtd: data.qtd,
valor: data.valor,
emolumento: data.emolumento,
fundesp: data.fundesp,
taxa_judiciaria: data.taxa_judiciaria,
valor_iss: data.valor_iss,
}
const localAddTServicoItemPedido = (item: any) => {
setLocalTServicoItemPedido((prev) => {
const novoItem = [...prev, item]
if (setValue) setValue('itens', novoItem)
return novoItem
const updated = [...prev, item];
if (setValue) setValue("itens", updated);
return updated;
});
};
const localClearTServicoItemPedido = () => {
setLocalTServicoItemPedido([]);
if (setValue) setValue("itens", []);
};
return {
TServicoItemPedidoLocal,
setLocalTServicoItemPedido,
localAddTServicoItemPedido,
setLocalTServicoItemPedido,
localClearTServicoItemPedido,
};
};

View file

@ -2,16 +2,16 @@
import {
ColumnDef,
ColumnFiltersState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
SortingState,
ColumnFiltersState,
VisibilityState,
RowSelectionState,
SortingState,
useReactTable,
VisibilityState,
} from '@tanstack/react-table';
import { ChevronLeftIcon, ChevronRightIcon, EyeIcon } from 'lucide-react';
import React from 'react';
@ -60,25 +60,25 @@ export function DataTable<TData>({
...columns,
...(onEdit || onDelete
? [
{
id: 'actions',
header: 'Ações',
cell: ({ row }: any) => (
<div className="flex gap-2">
{onEdit && (
<Button variant="ghost" size="sm" onClick={() => onEdit(row.original)}>
Editar
</Button>
)}
{onDelete && (
<Button variant="destructive" size="sm" onClick={() => onDelete(row.original)}>
Excluir
</Button>
)}
</div>
),
} as ColumnDef<TData, any>,
]
{
id: 'actions',
header: 'Ações',
cell: ({ row }: any) => (
<div className="flex gap-2">
{onEdit && (
<Button variant="ghost" size="sm" onClick={() => onEdit(row.original)}>
Editar
</Button>
)}
{onDelete && (
<Button variant="destructive" size="sm" onClick={() => onDelete(row.original)}>
Excluir
</Button>
)}
</div>
),
} as ColumnDef<TData, any>,
]
: []),
],
state: {