[MVPTN-27] feat(CRUD): Cria o CRUD inicial da Natureza de Títulos

This commit is contained in:
Keven Willian Pereira de Souza 2025-10-20 14:58:28 -03:00
parent 325ca69317
commit 64b897e7e9
21 changed files with 662 additions and 0 deletions

View file

@ -0,0 +1,11 @@
import GNaturezaTituloIndex from "@/packages/administrativo/components/GNaturezaTitulo/GNaturezaTituloIndex";
export default function GNaturezaPage() {
return (
<GNaturezaTituloIndex
sistema_id={2}
/>
);
}

View file

@ -149,6 +149,10 @@ const data = {
title: "Atos/Partes Tipos", title: "Atos/Partes Tipos",
url: "/administrativo/atos/partes-tipos", url: "/administrativo/atos/partes-tipos",
}, },
{
title: "Valores de Serviços",
url: "/administrativo/valores-de-servicos",
}
], ],
}, },

View file

@ -0,0 +1,89 @@
import { ColumnDef } from '@tanstack/react-table';
import { EllipsisIcon, PencilIcon, Trash2Icon } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import GetCapitalize from '@/shared/actions/text/GetCapitalize';
import { SortableHeader } from '@/shared/components/dataTable/SortableHeader';
import GNaturezaTituloInterface from '../../interfaces/GNaturezaTitulo/GNaturezaTituloInterface';
import { SituacoesEnum } from '@/shared/enums/SituacoesEnum';
export default function GNaturezaTituloColumns(
onEdit: (item: GNaturezaTituloInterface, isEditingFormStatus: boolean) => void,
onDelete: (item: GNaturezaTituloInterface, isEditingFormStatus: boolean) => void,
): ColumnDef<GNaturezaTituloInterface>[] {
return [
// ID
{
accessorKey: 'natureza_titulo_id',
header: ({ column }) => SortableHeader('ID', column),
cell: ({ row }) => Number(row.getValue('natureza_titulo_id')),
enableSorting: true,
},
// Situação
{
accessorKey: 'situacao',
header: ({ column }) => SortableHeader('Situação', column),
cell: ({ row }) => {
const value = String(row.getValue('situacao') || '').toUpperCase();
return SituacoesEnum[value as keyof typeof SituacoesEnum] ?? '-';
},
},
// Descrição
{
accessorKey: 'descricao',
header: ({ column }) => SortableHeader('Descrição', column),
cell: ({ row }) => GetCapitalize(String(row.getValue('descricao') || '')),
},
// Ações
{
id: 'actions',
header: 'Ações',
cell: ({ row }) => {
const natureza = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<EllipsisIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="left" align="start">
<DropdownMenuGroup>
<DropdownMenuItem onSelect={() => onEdit(natureza, true)}>
<PencilIcon className="mr-2 h-4 w-4" />
Editar
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-red-600"
onSelect={() => onDelete(natureza, true)}
>
<Trash2Icon className="mr-2 h-4 w-4" />
Remover
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
},
enableSorting: false,
enableHiding: false,
},
];
}

View file

@ -0,0 +1,127 @@
'use client';
import React, { useEffect } from 'react';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { ResetFormIfData } from '@/shared/actions/form/ResetFormIfData';
import LoadingButton from '@/shared/components/loadingButton/LoadingButton';
import SituacoesSelect from '@/shared/components/situacoes/SituacoesSelect';
import { useGNaturezaTituloFormHook } from '../../hooks/GNaturezaTitulo/useGNaturezaTituloFormHook';
import { GNaturezaTituloFormInterface } from '../../interfaces/GNaturezaTitulo/GNaturezaTituloFormInterface';
/**
* Formulário de cadastro/edição de Natureza
* Baseado nos campos da tabela G_NATUREZA
*/
export default function GNaturezaTituloForm({
isOpen,
data,
onClose,
onSave,
buttonIsLoading,
}: GNaturezaTituloFormInterface) {
const form = useGNaturezaTituloFormHook({});
// Atualiza o formulário quando recebe dados para edição
useEffect(() => {
ResetFormIfData(form, data);
}, [data, form]);
function onError(error: any) {
console.log('Erro no formulário:', error);
}
return (
<Dialog
open={isOpen}
onOpenChange={(open) => {
if (!open) onClose(null, false);
}}
>
<DialogContent className="w-full max-w-full p-6 sm:max-w-3xl md:max-w-2xl lg:max-w-2xl">
<DialogHeader>
<DialogTitle className="text-lg sm:text-xl">Valores de Serviços</DialogTitle>
<DialogDescription className="text-muted-foreground text-sm">
Valores de Serviços
</DialogDescription>
</DialogHeader>
{/* Formulário principal */}
<Form {...form}>
<form onSubmit={form.handleSubmit(onSave, onError)} className="space-y-6">
{/* GRID MOBILE FIRST */}
<div className="grid w-full grid-cols-12 gap-4">
{/* DESCRIÇÃO */}
<div className="col-span-12 sm:col-span-6 md:col-span-12">
<FormField
control={form.control}
name="descricao"
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Descrição</FormLabel>
<FormControl>
<Input
{...field}
type="text"
placeholder="Digite a descrição da natureza"
maxLength={60}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* SITUAÇÃO */}
<div className="col-span-12 sm:col-span-6 md:col-span-12">
<FormField
control={form.control}
name="situacao"
render={({ field }) => (
<FormItem>
<FormLabel>Situação</FormLabel>
<SituacoesSelect field={field} />
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
{/* Rodapé */}
<DialogFooter className="mt-6 flex flex-col justify-end gap-2 sm:flex-row">
<DialogClose asChild>
<Button variant="outline" type="button">
Cancelar
</Button>
</DialogClose>
<LoadingButton
text="Salvar"
textLoading="Salvando..."
type="submit"
loading={buttonIsLoading}
/>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}

View file

@ -0,0 +1,168 @@
'use client';
import { useEffect, useState, useCallback } from 'react';
import { useGNaturezaTituloDeleteHook } from '@/packages/administrativo/hooks/GNaturezaTitulo/useGNaturezaTituloDeleteHook';
import { useGNaturezaTituloIndexHook } from '@/packages/administrativo/hooks/GNaturezaTitulo/useGNaturezaTituloIndexHook';
import { useGNaturezaTituloSaveHook } from '@/packages/administrativo/hooks/GNaturezaTitulo/useGNaturezaTituloSaveHook';
import GNaturezaTituloInterface from '@/packages/administrativo/interfaces/GNaturezaTitulo/GNaturezaTituloInterface';
import ConfirmDialog from '@/shared/components/confirmDialog/ConfirmDialog';
import { useConfirmDialog } from '@/shared/components/confirmDialog/useConfirmDialog';
import Loading from '@/shared/components/loading/loading';
import Header from '@/shared/components/structure/Header';
import GNaturezaTituloForm from './GNaturezaTituloForm';
import GNaturezaTituloTable from './GNaturezaTituloTable';
import { GNaturezaTituloIndexInterface } from '../../interfaces/GNaturezaTitulo/GNaturezaTituloIndexInterface';
export default function GNaturezaTituloIndex({ sistema_id }: GNaturezaTituloIndexInterface) {
const GNaturezaTituloIndexParams: GNaturezaTituloIndexInterface = {
sistema_id: sistema_id,
};
// Controle de estado do botão
const [buttonIsLoading, setButtonIsLoading] = useState(false);
// Hooks para leitura e salvamento
const { gNaturezaTitulo, indexGNaturezaTitulo } = useGNaturezaTituloIndexHook();
const { saveGNaturezaTitulo } = useGNaturezaTituloSaveHook();
const { deleteGNaturezaTitulo } = useGNaturezaTituloDeleteHook();
// Estados
const [selectedData, setSelectedData] = useState<GNaturezaTituloInterface | null>(null);
const [isFormOpen, setIsFormOpen] = useState(false);
// Estado para saber qual item será deletado
const [itemToDelete, setItemToDelete] = useState<GNaturezaTituloInterface | null>(null);
/**
* Hook do modal de confirmação
*/
const { isOpen: isConfirmOpen, openDialog: openConfirmDialog, handleCancel } = useConfirmDialog();
/**
* Abre o formulário no modo de edição ou criação
*/
const handleOpenForm = useCallback((data: GNaturezaTituloInterface | null) => {
// Se não houver dados (criação), cria um objeto inicial com pessoa_tipo
const initialData: GNaturezaTituloInterface =
data ?? ({ sistema_id: GNaturezaTituloIndexParams.sistema_id } as GNaturezaTituloInterface);
setSelectedData(initialData);
setIsFormOpen(true);
}, []);
/**
* Fecha o formulário e limpa o andamento selecionado
*/
const handleCloseForm = useCallback(() => {
setSelectedData(null);
setIsFormOpen(false);
}, []);
/**
* Salva os dados do formulário
*/
const handleSave = useCallback(
async (formData: GNaturezaTituloInterface) => {
// Coloca o botão em estado de loading
setButtonIsLoading(true);
// Aguarda salvar o registro
await saveGNaturezaTitulo(formData);
// Remove o botão em estado de loading
setButtonIsLoading(false);
// Atualiza a lista de dados
indexGNaturezaTitulo(GNaturezaTituloIndexParams);
},
[saveGNaturezaTitulo, indexGNaturezaTitulo, handleCloseForm],
);
/**
* Quando o usuário clica em "remover" na tabela
*/
const handleConfirmDelete = useCallback(
(item: GNaturezaTituloInterface) => {
// 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 deleteGNaturezaTitulo(itemToDelete);
// Atualiza a lista
await indexGNaturezaTitulo(GNaturezaTituloIndexParams);
// Limpa o item selecionado
setItemToDelete(null);
// Fecha o modal
handleCancel();
}, [itemToDelete, indexGNaturezaTitulo, handleCancel]);
/**
* Busca inicial dos dados
*/
useEffect(() => {
indexGNaturezaTitulo(GNaturezaTituloIndexParams);
}, []);
/**
* Tela de loading enquanto carrega os dados
*/
if (gNaturezaTitulo?.length == 0) {
return <Loading type={2} />;
}
return (
<div>
{/* Cabeçalho */}
<Header
title={'Valores de Serviços'}
description={'Valores de Serviços'}
buttonText={'Novo valor'}
buttonAction={() => {
handleOpenForm(null);
}}
/>
{/* Tabela de andamentos */}
<GNaturezaTituloTable data={gNaturezaTitulo} onEdit={handleOpenForm} onDelete={handleConfirmDelete} />
{/* Modal de confirmação */}
{isConfirmOpen && (
<ConfirmDialog
isOpen={isConfirmOpen}
title="Confirmar exclusão"
description="Atenção"
message={`Deseja realmente excluir o valor "${itemToDelete?.descricao}"?`}
confirmText="Sim, excluir"
cancelText="Cancelar"
onConfirm={handleDelete}
onCancel={handleCancel}
/>
)}
{/* Formulário de criação/edição */}
{isFormOpen && (
<GNaturezaTituloForm
isOpen={isFormOpen}
data={selectedData}
onClose={handleCloseForm}
onSave={handleSave}
buttonIsLoading={buttonIsLoading}
/>
)}
</div>
);
}

View file

@ -0,0 +1,23 @@
'use client';
import { DataTable } from '@/shared/components/dataTable/DataTable';
import GNaturezaTituloColumns from './GNaturezaTituloColumns';
import GNaturezaTituloTableInterface from '../../interfaces/GNaturezaTitulo/GNaturezaTituloTableInterface';
/**
* Componente principal da tabela de Naturezas
*/
export default function GNaturezaTituloTable({ data, onEdit, onDelete }: GNaturezaTituloTableInterface) {
const columns = GNaturezaTituloColumns(onEdit, onDelete);
return (
<div>
<DataTable
data={data}
columns={columns}
filterColumn="descricao"
filterPlaceholder="Buscar por descrição da natureza..."
/>
</div>
);
}

View file

@ -0,0 +1,17 @@
import { withClientErrorHandler } from '@/shared/actions/withClientErrorHandler/withClientErrorHandler';
import API from '@/shared/services/api/Api';
import { Methods } from '@/shared/services/api/enums/ApiMethodEnum';
import ApiResponseInterface from '@/shared/services/api/interfaces/ApiResponseInterface';
import GNaturezaTituloInterface from '../../interfaces/GNaturezaTitulo/GNaturezaTituloInterface';
async function executeGNaturezaTituloDeleteData(data: GNaturezaTituloInterface): Promise<ApiResponseInterface> {
const api = new API();
return api.send({
method: Methods.DELETE,
endpoint: `administrativo/g_natureza_titulo/${data.natureza_titulo_id}`,
});
}
export const GNaturezaTituloDeleteData = withClientErrorHandler(executeGNaturezaTituloDeleteData);

View file

@ -0,0 +1,17 @@
import { withClientErrorHandler } from '@/shared/actions/withClientErrorHandler/withClientErrorHandler';
import API from '@/shared/services/api/Api';
import { Methods } from '@/shared/services/api/enums/ApiMethodEnum';
import ApiResponseInterface from '@/shared/services/api/interfaces/ApiResponseInterface';
import GNaturezaTituloInterface from '../../interfaces/GNaturezaTitulo/GNaturezaTituloInterface';
async function executeGNaturezaTituloIndexData(data: GNaturezaTituloInterface): Promise<ApiResponseInterface> {
const api = new API();
return api.send({
method: Methods.GET,
endpoint: `administrativo/g_natureza_titulo/sistema/${data.sistema_id}`,
});
}
export const GNaturezaTituloIndexData = withClientErrorHandler(executeGNaturezaTituloIndexData);

View file

@ -0,0 +1,23 @@
import { withClientErrorHandler } from '@/shared/actions/withClientErrorHandler/withClientErrorHandler';
import API from '@/shared/services/api/Api';
import { Methods } from '@/shared/services/api/enums/ApiMethodEnum';
import ApiResponseInterface from '@/shared/services/api/interfaces/ApiResponseInterface';
import GNaturezaTituloInterface from '../../interfaces/GNaturezaTitulo/GNaturezaTituloInterface';
async function executeGNaturezaTituloSaveData(data: GNaturezaTituloInterface): Promise<ApiResponseInterface> {
// Verifica se existe ID para decidir se é atualização (PUT) ou criação (POST)
const isUpdate = Boolean(data.natureza_titulo_id);
// Instancia o cliente da API
const api = new API();
// Executa a requisição para a API com o método apropriado e envia os dados no corpo
return api.send({
method: isUpdate ? Methods.PUT : Methods.POST, // PUT se atualizar, POST se criar
endpoint: `administrativo/g_natureza_titulo/${data.natureza_titulo_id || ''}`, // endpoint dinâmico
body: data, // payload enviado para a API
});
}
export const GNaturezaTituloSaveData = withClientErrorHandler(executeGNaturezaTituloSaveData);

View file

@ -0,0 +1,21 @@
import { useState } from 'react';
import { useResponse } from '@/shared/components/response/ResponseContext';
import GNaturezaTituloInterface from '../../interfaces/GNaturezaTitulo/GNaturezaTituloInterface';
import { GNaturezaTituloDeleteService } from '../../services/GNaturezaTitulo/GNaturezaTituloDeleteService';
export const useGNaturezaTituloDeleteHook = () => {
const { setResponse } = useResponse();
const [gNaturezaTitulo, setGNaturezaTitulo] = useState<GNaturezaTituloInterface>();
const deleteGNaturezaTitulo = async (data: GNaturezaTituloInterface) => {
const response = await GNaturezaTituloDeleteService(data);
setGNaturezaTitulo(data);
setResponse(response);
};
return { gNaturezaTitulo, deleteGNaturezaTitulo };
};

View file

@ -0,0 +1,14 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { GNaturezaTituloFormValues, GNaturezaTituloSchema } from '../../schemas/GNaturezaTitulo/GNaturezaTituloSchema';
export function useGNaturezaTituloFormHook(defaults?: Partial<GNaturezaTituloFormValues>) {
return useForm<GNaturezaTituloFormValues>({
resolver: zodResolver(GNaturezaTituloSchema),
defaultValues: {
natureza_titulo_id: 0,
...defaults,
},
});
}

View file

@ -0,0 +1,27 @@
'use client';
import { useState } from 'react';
import { useResponse } from '@/shared/components/response/ResponseContext';
import GNaturezaTituloInterface from '../../interfaces/GNaturezaTitulo/GNaturezaTituloInterface';
import { GNaturezaTituloIndexService } from '../../services/GNaturezaTitulo/GNaturezaTituloIndexService';
export const useGNaturezaTituloIndexHook = () => {
const { setResponse } = useResponse();
const [gNaturezaTitulo, setGNaturezaTitulo] = useState<GNaturezaTituloInterface[]>([]);
const indexGNaturezaTitulo = async (data: GNaturezaTituloInterface) => {
const response = await GNaturezaTituloIndexService(data);
// Armazena os dados consultados
setGNaturezaTitulo(response.data);
// Define a resposta (toast, modal, feedback, etc.)
setResponse(response);
};
return {
gNaturezaTitulo,
indexGNaturezaTitulo,
};
};

View file

@ -0,0 +1,35 @@
'use client';
import { useState } from 'react';
import { useResponse } from '@/shared/components/response/ResponseContext';
import GNaturezaTituloInterface from '../../interfaces/GNaturezaTitulo/GNaturezaTituloInterface';
import { GNaturezaTituloSaveService } from '../../services/GNaturezaTitulo/GNaturezaTituloSaveService';
export const useGNaturezaTituloSaveHook = () => {
const { setResponse } = useResponse();
const [gNaturezaTitulo, setGNaturezaTitulo] = useState<GNaturezaTituloInterface | null>(null);
// controla se o formulário está aberto ou fechado
const [isOpen, setIsOpen] = useState(false);
const saveGNaturezaTitulo = async (data: GNaturezaTituloInterface) => {
const response = await GNaturezaTituloSaveService(data);
// Armazena os dados da resposta
setGNaturezaTitulo(response.data);
// Define os dados da resposta (toast, modal, etc.)
setResponse(response);
// Fecha o formulário automaticamente após salvar
setIsOpen(false);
// Retorna os valores de forma imediata
return response.data;
};
return { gNaturezaTitulo, saveGNaturezaTitulo, isOpen, setIsOpen };
};

View file

@ -0,0 +1,9 @@
import { GNaturezaTituloFormValues } from '../../schemas/GNaturezaTitulo/GNaturezaTituloSchema';
export interface GNaturezaTituloFormInterface {
isOpen: boolean;
data: GNaturezaTituloFormValues | null;
onClose: (item: null, isFormStatus: boolean) => void;
onSave: (data: GNaturezaTituloFormValues) => void;
buttonIsLoading: boolean;
}

View file

@ -0,0 +1,3 @@
export interface GNaturezaTituloIndexInterface {
sistema_id: number;
}

View file

@ -0,0 +1,14 @@
import { ConfirmacaoEnum } from "@/shared/enums/ConfirmacaoEnum";
import { SituacoesEnum } from "@/shared/enums/SituacoesEnum";
export default interface GNaturezaTituloInterface {
natureza_titulo_id?: number,
emolumento_id?: number,
descricao?: string,
situacao?: SituacoesEnum,
sistema_id?: number,
tipo_cobranca?: number,
possui_valor?: ConfirmacaoEnum,
pertence_registro_imovel?: ConfirmacaoEnum,
tipo_recibo?: number,
}

View file

@ -0,0 +1,7 @@
import GNaturezaTituloInterface from './GNaturezaTituloInterface';
export default interface GNaturezaTituloTableInterface {
data?: GNaturezaTituloInterface[];
onEdit: (item: GNaturezaTituloInterface, isEditingFormStatus: boolean) => void;
onDelete: (item: GNaturezaTituloInterface, isEditingFormStatus: boolean) => void;
}

View file

@ -0,0 +1,17 @@
import z from "zod";
import { ConfirmacaoEnum } from "@/shared/enums/ConfirmacaoEnum";
export const GNaturezaTituloSchema = z.object({
natureza_titulo_id: z.number().optional(),
emolumento_id: z.number().optional(),
descricao: z.string().optional(),
situacao: z.enum(["A", "I"]).optional(),
sistema_id: z.number().optional(),
tipo_cobranca: z.number().optional(),
possui_valor: z.enum(ConfirmacaoEnum).optional(),
pertence_registro_imovel: z.enum(ConfirmacaoEnum).optional(),
tipo_recibo: z.number().optional(),
});
export type GNaturezaTituloFormValues = z.infer<typeof GNaturezaTituloSchema>;

View file

@ -0,0 +1,12 @@
import { withClientErrorHandler } from '@/shared/actions/withClientErrorHandler/withClientErrorHandler';
import { GNaturezaTituloDeleteData } from '../../data/GNaturezaTitulo/GNaturezaTituloDeleteData';
import GNaturezaTituloInterface from '../../interfaces/GNaturezaTitulo/GNaturezaTituloInterface';
async function executeGNaturezaTituloDeleteService(data: GNaturezaTituloInterface) {
const response = await GNaturezaTituloDeleteData(data);
return response;
}
export const GNaturezaTituloDeleteService = withClientErrorHandler(executeGNaturezaTituloDeleteService);

View file

@ -0,0 +1,12 @@
import { withClientErrorHandler } from '@/shared/actions/withClientErrorHandler/withClientErrorHandler';
import { GNaturezaTituloIndexData } from '../../data/GNaturezaTitulo/GNaturezaTituloIndexData';
import GNaturezaTituloInterface from '../../interfaces/GNaturezaTitulo/GNaturezaTituloInterface';
export default async function executeGNaturezaTituloIndexService(data: GNaturezaTituloInterface) {
const response = await GNaturezaTituloIndexData(data);
return response;
}
export const GNaturezaTituloIndexService = withClientErrorHandler(executeGNaturezaTituloIndexService);

View file

@ -0,0 +1,12 @@
import { withClientErrorHandler } from '@/shared/actions/withClientErrorHandler/withClientErrorHandler';
import { GNaturezaTituloSaveData } from '../../data/GNaturezaTitulo/GNaturezaTituloSaveData';
import GNaturezaTituloInterface from '../../interfaces/GNaturezaTitulo/GNaturezaTituloInterface';
async function executeGNaturezaTituloSaveService(data: GNaturezaTituloInterface) {
const response = await GNaturezaTituloSaveData(data);
return response;
}
export const GNaturezaTituloSaveService = withClientErrorHandler(executeGNaturezaTituloSaveService);