[MVPTN-37] feat(Pedido): Ajusta o forumlário para utilizar o endpoint para salvar o pedido

This commit is contained in:
Keven 2025-11-06 17:32:40 -03:00
parent e50818e52a
commit 06d55ec125
13 changed files with 419 additions and 355 deletions

29
package-lock.json generated
View file

@ -124,6 +124,7 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@ -2972,6 +2973,7 @@
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@ -2982,6 +2984,7 @@
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@ -2998,6 +3001,7 @@
"integrity": "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.46.1",
@ -3028,6 +3032,7 @@
"integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.46.1",
"@typescript-eslint/types": "8.46.1",
@ -3502,6 +3507,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -3854,6 +3860,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.9",
"caniuse-lite": "^1.0.30001746",
@ -4658,6 +4665,7 @@
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@ -4718,6 +4726,7 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@ -4844,6 +4853,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@ -6998,6 +7008,7 @@
"resolved": "https://registry.npmjs.org/next/-/next-15.5.5.tgz",
"integrity": "sha512-OQVdBPtpBfq7HxFN0kOVb7rXXOSIkt5lTzDJDGRBcOyVvNRIWFauMqi1gIHd1pszq1542vMOGY0HP4CaiALfkA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@next/env": "15.5.5",
"@swc/helpers": "0.5.15",
@ -7394,6 +7405,7 @@
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@ -7551,6 +7563,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -7560,6 +7573,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@ -7572,6 +7586,7 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.65.0.tgz",
"integrity": "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@ -7587,7 +7602,8 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/react-masked-text": {
"version": "1.0.5",
@ -7600,6 +7616,7 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
@ -7718,7 +7735,8 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/redux-thunk": {
"version": "3.1.0",
@ -8478,6 +8496,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -8489,7 +8508,8 @@
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/tinymce/-/tinymce-8.1.2.tgz",
"integrity": "sha512-KITxHEEHRlxC5xOnxA123eAJ67NgsWxNphtItWt9TRu07DiTZrWIqJeIKRX9euE51/l3kJO4WQiqoBXKTJJGsA==",
"license": "GPL-2.0-or-later"
"license": "GPL-2.0-or-later",
"peer": true
},
"node_modules/to-regex-range": {
"version": "5.0.1",
@ -8643,6 +8663,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -8708,6 +8729,7 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"napi-postinstall": "^0.3.0"
},
@ -8994,6 +9016,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View file

@ -1,6 +1,6 @@
'use client';
import React, { useEffect } from 'react';
import { useEffect } from 'react';
import { Button } from '@/components/ui/button';
import {

View file

@ -1,6 +1,6 @@
'use client';
import { useEffect, useState, useCallback } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useGGramaticaDeleteHook } from '@/packages/administrativo/hooks/GGramatica/useGGramaticaDeleteHook';

View file

@ -2,8 +2,8 @@
import { DataTable } from '@/shared/components/dataTable/DataTable';
import GGramaticaColumns from './GGramaticaColumns';
import GGramaticaTableInterface from '../../interfaces/GGramatica/GGramaticaTableInterface';
import GGramaticaColumns from './GGramaticaColumns';
/**
* Componente principal da tabela de Naturezas

View file

@ -15,7 +15,7 @@ export default function TServicoItemPedidoList({ items }: TServicoItemPedidoList
<CardContent>
{/* Altura máxima + scroll vertical */}
<div className="space-y-4 max-h-[60vh] overflow-y-auto pr-1">
{items.map((item) => (
{items?.map((item) => (
<div
key={item.servico_itempedido_id}
className="bg-cart-item border-cart-border flex items-start gap-4 rounded-lg border p-4"

View file

@ -7,16 +7,16 @@ import { useCallback, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import TServicoItemPedidoList from '@/packages/servicos/components/TServicoItemPedido/TServicoItemPedidoList';
import TServicoPedidoDetailsPagamento from '@/packages/servicos/components/TServicoPedido/TServicoPedidoDetailsPagamento';
import { useTServicoItemPedidoIndexHook } from '@/packages/servicos/hooks/TServicoItemPedido/useTServicoItemPedidoIndexHook';
import { useTServicoPedidoShowHook } from '@/packages/servicos/hooks/TServicoPedido/useTServicoPedidoShowHook';
import TServicoPedidoInterface from '@/packages/servicos/interfaces/TServicoPedido/TServicoPedidoInterface';
import { FormatCPF } from '@/shared/actions/CPF/FormatCPF';
import { FormatDateTime } from '@/shared/actions/dateTime/FormatDateTime';
import GetCapitalize from '@/shared/actions/text/GetCapitalize';
import GetNameInitials from '@/shared/actions/text/GetNameInitials';
import { useTServicoItemPedidoIndexHook } from '../../hooks/TServicoItemPedido/useTServicoItemPedidoIndexHook';
import { useTServicoPedidoShowHook } from '../../hooks/TServicoPedido/useTServicoPedidoShowHook';
import TServicoItemPedidoList from '../TServicoItemPedido/TServicoItemPedidoList';
import TServicoPedidoDetailsPagamento from './TServicoPedidoDetailsPagamento';

View file

@ -1,7 +1,9 @@
'use client';
import { CreditCard, Package, UserSquare2 } from 'lucide-react';
import { useRouter } from 'next/navigation';
import * as React from 'react';
import { useCallback, useState } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@ -14,239 +16,73 @@ import {
FormMessage
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
import GUsuarioSelect from '@/packages/administrativo/components/GUsuario/GUsuarioSelect';
import TServicoTipoSelect from '@/packages/administrativo/components/TServicoTipo/TServicoTipoSelect';
import { useTServicoPedidoFormHook } from '@/packages/servicos/hooks/TServicoPedido/useTServicoPedidoFormHook';
import { ResetFormIfData } from '@/shared/actions/form/ResetFormIfData';
import { useTServicoPedidoSaveHook } from '@/packages/servicos/hooks/TServicoPedido/useTServicoPedidoSaveHook';
import TServicoPedidoInterface from '@/packages/servicos/interfaces/TServicoPedido/TServicoPedidoInterface';
import { parseNumberInput } from '@/shared/actions/form/parseNumberInput';
import LoadingButton from '@/shared/components/loadingButton/LoadingButton';
import { StepNavigator, StepNavigatorRef, StepSection } from '@/shared/components/step/stepNavigator';
import TipoPagamentoSelect from '@/shared/components/tipoPagamento/TipoPagamentoSelect';
import TServicoItemPedidoFormTable from '../TServicoItemPedido/TServicoItemPedidoFormTable';
type StepKey = 'pedido' | 'servicoPedidoItem' | 'payment';
const SECTION_ORDER: { key: StepKey; id: string }[] = [
{ key: 'pedido', id: 'selectPedido' },
{ key: 'servicoPedidoItem', id: 'selectServicoPedidoItem' },
{ key: 'payment', id: 'selectPayment' },
];
const SCROLL_OFFSET = 16;
const SPY_LOCK_MS = 600;
export default function TServicoPedidoForm() {
const data = {};
// Controle de rotas
const router = useRouter();
// Controle de estado do botão
const [buttonIsLoading, setButtonIsLoading] = useState(false);
const { TServicoPedido, saveTServicoPedido } = useTServicoPedidoSaveHook()
const form = useTServicoPedidoFormHook({});
const [active, setActive] = React.useState<StepKey>('pedido');
const sections: StepSection[] = [
{ key: 'pedido', id: 'selectPedido', icon: <Package className="h-4 w-4" />, title: 'Pedido', description: 'Dados gerais do pedido.' },
{ key: 'servicoPedidoItem', id: 'selectServicoPedidoItem', icon: <UserSquare2 className="h-4 w-4" />, title: 'Itens', description: 'Itens/serviços do pedido.' },
{ key: 'payment', id: 'selectPayment', icon: <CreditCard className="h-4 w-4" />, title: 'Pagamento', description: 'Forma e dados de pagamento.' },
];
const ref = React.useRef<StepNavigatorRef>(null);
// submit e error do RHF
function onSubmit(values: any) {
console.log('Submit:', values);
}
const onSave = useCallback(
async (formData: TServicoPedidoInterface) => {
console.log(formData);
// Coloca o botão em estado de loading
setButtonIsLoading(true);
// Aguarda salvar o registro
const response = await saveTServicoPedido(formData);
// Verifica se pode redirecionar o usuário
if (response.servico_pedido_id > 0) {
// Redirecionamento de página
router.replace('/servicos/balcao/detalhes/' + response.servico_pedido_id)
}
// Remove o botão em estado de loading
setButtonIsLoading(false);
},
[saveTServicoPedido, TServicoPedido],
);
function onError(error: any) {
console.log('Erro no formulário:', error);
}
// === Bloqueio do scroll-spy durante rolagem programática ===
const spyLockedRef = React.useRef(false);
const spyTimerRef = React.useRef<number | null>(null);
const lockSpy = React.useCallback(() => {
spyLockedRef.current = true;
if (spyTimerRef.current) window.clearTimeout(spyTimerRef.current);
spyTimerRef.current = window.setTimeout(() => {
spyLockedRef.current = false;
}, SPY_LOCK_MS) as unknown as number;
}, []);
const scrollToSection = React.useCallback(
(id: string) => {
const el = document.getElementById(id);
if (!el) return;
lockSpy();
const y = el.getBoundingClientRect().top + window.scrollY - SCROLL_OFFSET;
window.scrollTo({ top: y, behavior: 'smooth' });
},
[lockSpy]
);
// Scroll spy
React.useEffect(() => {
const handler = () => {
if (spyLockedRef.current) return;
let current: StepKey = 'pedido';
let best = Number.POSITIVE_INFINITY;
for (const { key, id } of SECTION_ORDER) {
const el = document.getElementById(id);
if (!el) continue;
const dist = Math.abs(el.getBoundingClientRect().top - SCROLL_OFFSET - 8);
if (dist < best) {
best = dist;
current = key;
}
}
if (current !== active) setActive(current);
};
handler();
window.addEventListener('scroll', handler, { passive: true });
return () => window.removeEventListener('scroll', handler);
}, [active]);
// Atualiza o formulário quando recebe dados para edição
React.useEffect(() => {
ResetFormIfData(form, data);
}, [data, form]);
const dataItens = [
{
'emolumento': 4.99,
'fundesp': 1.21,
'taxa_judiciaria': 0,
'valor_iss': 0.25,
'valor': 6.45,
'servico': 'Autenticação Original',
'tabela': 'Autenticação por página'
},
{
'emolumento': 3.50,
'fundesp': 0.85,
'taxa_judiciaria': 0,
'valor_iss': 0.18,
'valor': 4.53,
'servico': 'Autenticação de Cópia',
'tabela': 'Autenticação por página'
},
{
'emolumento': 5.90,
'fundesp': 1.20,
'taxa_judiciaria': 0,
'valor_iss': 0.30,
'valor': 7.40,
'servico': 'Reconhecimento de Firma (Semelhança)',
'tabela': 'Reconhecimento de firma'
},
{
'emolumento': 8.50,
'fundesp': 1.75,
'taxa_judiciaria': 0,
'valor_iss': 0.43,
'valor': 10.68,
'servico': 'Reconhecimento de Firma (Autenticidade)',
'tabela': 'Reconhecimento de firma'
},
{
'emolumento': 12.00,
'fundesp': 2.50,
'taxa_judiciaria': 0,
'valor_iss': 0.60,
'valor': 15.10,
'servico': 'Abertura de Firma',
'tabela': 'Ficha de assinatura'
},
{
'emolumento': 18.00,
'fundesp': 3.60,
'taxa_judiciaria': 0,
'valor_iss': 0.90,
'valor': 22.50,
'servico': 'Procuração Particular',
'tabela': 'Procuração por folha'
},
{
'emolumento': 35.00,
'fundesp': 7.00,
'taxa_judiciaria': 0,
'valor_iss': 1.75,
'valor': 43.75,
'servico': 'Procuração Pública',
'tabela': 'Procuração (tabela geral)'
},
{
'emolumento': 20.00,
'fundesp': 4.00,
'taxa_judiciaria': 0,
'valor_iss': 1.00,
'valor': 25.00,
'servico': 'Certidão Simples',
'tabela': 'Certidões'
},
{
'emolumento': 45.00,
'fundesp': 9.00,
'taxa_judiciaria': 0,
'valor_iss': 2.25,
'valor': 56.25,
'servico': 'Certidão em Inteiro Teor',
'tabela': 'Certidões'
},
{
'emolumento': 80.00,
'fundesp': 16.00,
'taxa_judiciaria': 0,
'valor_iss': 4.00,
'valor': 100.00,
'servico': 'Ata Notarial',
'tabela': 'Ata por página'
},
{
'emolumento': 7.50,
'fundesp': 1.50,
'taxa_judiciaria': 0,
'valor_iss': 0.38,
'valor': 9.38,
'servico': 'Testemunha em Documento',
'tabela': 'Outras declarações'
},
{
'emolumento': 6.20,
'fundesp': 1.24,
'taxa_judiciaria': 0,
'valor_iss': 0.31,
'valor': 7.75,
'servico': 'Autenticação de Documento Eletrônico',
'tabela': 'Autenticação digital'
},
{
'emolumento': 25.00,
'fundesp': 5.00,
'taxa_judiciaria': 0,
'valor_iss': 1.25,
'valor': 31.25,
'servico': 'Reconhecimento de Sinal Público',
'tabela': 'Sinal público'
},
{
'emolumento': 10.00,
'fundesp': 2.00,
'taxa_judiciaria': 0,
'valor_iss': 0.50,
'valor': 12.50,
'servico': 'Certificação de Cópia Digital',
'tabela': 'Certificação digital'
},
{
'emolumento': 9.90,
'fundesp': 1.98,
'taxa_judiciaria': 0,
'valor_iss': 0.50,
'valor': 12.38,
'servico': 'Arquivamento de Documento',
'tabela': 'Arquivamento'
}
];
return (
<div>
<h3 className='text-4xl font-bold mb-4'>
Pedido
</h3>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit, onError)}>
<form onSubmit={form.handleSubmit(onSave, onError)}>
<div className="flex gap-4">
{/* Sidebar - Sticky */}
<aside className="hidden w-[360px] lg:block">
@ -254,35 +90,13 @@ export default function TServicoPedidoForm() {
<Card role="presentation" className="card-border">
<CardContent>
<nav className="flex flex-col gap-2">
<StepLink
active={active === 'pedido'}
onClick={() => {
setActive('pedido');
scrollToSection('selectPedido');
}}
icon={<Package className="h-4 w-4" />}
title="Pedido"
description="Dados gerais do pedido."
/>
<StepLink
active={active === 'servicoPedidoItem'}
onClick={() => {
setActive('servicoPedidoItem');
scrollToSection('selectServicoPedidoItem');
}}
icon={<UserSquare2 className="h-4 w-4" />}
title="Itens"
description="Itens/serviços do pedido."
/>
<StepLink
active={active === 'payment'}
onClick={() => {
setActive('payment');
scrollToSection('selectPayment');
}}
icon={<CreditCard className="h-4 w-4" />}
title="Pagamento"
description="Forma e dados de pagamento."
<StepNavigator
ref={ref}
sections={sections}
defaultActive="pedido"
onChange={(k) => console.log('active =>', k)}
scrollOffset={16}
spyLockMs={600}
/>
</nav>
</CardContent>
@ -306,7 +120,7 @@ export default function TServicoPedidoForm() {
<div className="col-span-12 sm:col-span-6 md:col-span-12">
<FormField
control={form.control}
name="apresentante"
name="escrevente_id"
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel className='font-semibold'>Escrevente/Tabelião</FormLabel>
@ -391,53 +205,6 @@ export default function TServicoPedidoForm() {
</div>
</CardContent>
</Card>
{/* Seção: Itens */}
<Card role="presentation" id="selectServicoPedidoItem" className="scroll-mt-6">
<CardHeader>
<CardTitle>
<h4 className='text-3xl'>
Itens
</h4>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid w-full grid-cols-12 gap-4">
<div className="col-span-12 sm:col-span-12 md:col-span-12">
<FormField
control={form.control}
name="apresentante"
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Serviços</FormLabel>
<TServicoTipoSelect field={field} />
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="col-span-12 sm:col-span-12 md:col-span-12">
<FormField
control={form.control}
name="apresentante"
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Emolumentos</FormLabel>
<TServicoTipoSelect field={field} />
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="col-span-12 sm:col-span-12 md:col-span-12">
<TServicoItemPedidoFormTable
data={dataItens}
onEdit={() => { }}
onDelete={() => { }}
/>
</div>
</div>
</CardContent>
</Card>
{/* Seção: Pagamento */}
<Card role="presentation" id="selectPayment" className="scroll-mt-6">
<CardHeader>
@ -470,7 +237,7 @@ export default function TServicoPedidoForm() {
<div className="col-span-12 sm:col-span-12 md:col-span-4">
<FormField
control={form.control}
name="apresentante"
name="cpfcnpj_apresentante"
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>CPF/CNPJ Requerente</FormLabel>
@ -485,10 +252,48 @@ export default function TServicoPedidoForm() {
)}
/>
</div>
<div className="col-span-12 sm:col-span-6 md:col-span-6">
<FormField
control={form.control}
name="valor_pedido"
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Valor do Pedido</FormLabel>
<FormControl>
<Input
{...field}
type="number"
onChange={(e) => field.onChange(parseNumberInput(e))}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="col-span-12 sm:col-span-6 md:col-span-6">
<FormField
control={form.control}
name="valor_pago"
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Valor Pago</FormLabel>
<FormControl>
<Input
{...field}
type="number"
onChange={(e) => field.onChange(parseNumberInput(e))}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="col-span-12 sm:col-span-12 md:col-span-12">
<FormField
control={form.control}
name="apresentante"
name="tipo_pagamento"
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Forma Pagamento</FormLabel>
@ -502,9 +307,12 @@ export default function TServicoPedidoForm() {
<Button type="button" variant="outline">
Cancelar
</Button>
<Button type="submit">
Confirmar
</Button>
<LoadingButton
text="Salvar"
textLoading="Salvando..."
type="submit"
loading={buttonIsLoading}
/>
</div>
</div>
</CardContent>
@ -516,48 +324,4 @@ export default function TServicoPedidoForm() {
</Form>
</div>
);
}
/* ---------- Components auxiliares ---------- */
function StepLink({
active,
onClick,
icon,
title,
description,
}: {
active?: boolean;
onClick?: () => void;
icon: React.ReactNode;
title: string;
description: string;
}) {
return (
<button
type="button"
onClick={onClick}
className={cn(
'group w-full cursor-pointer rounded-xl p-4 text-left hover:bg-gray-100 dark:hover:bg-gray-700',
active && 'ring-1 ring-primary/60'
)}
aria-current={active ? 'step' : undefined}
>
<span className="flex items-center gap-2">
<span
className={cn(
'flex h-9 w-9 items-center justify-center rounded-full',
'bg-gray-100 text-gray-900 dark:bg-gray-700 dark:text-gray-100',
'group-hover:bg-white dark:group-hover:bg-gray-800'
)}
>
{icon}
</span>
<span className="flex flex-1 flex-col">
<span className="font-bold">{title}</span>
<span className="text-sm text-muted-foreground">{description}</span>
</span>
</span>
</button>
);
}
}

View file

@ -8,6 +8,19 @@ export function useTServicoPedidoFormHook(defaults?: Partial<TServicoPedidoFormV
resolver: zodResolver(TServicoPedidoSchema),
defaultValues: {
servico_pedido_id: 0,
valor_pedido: 0,
valor_pago: 0,
data_pedido: "",
observacao: "",
escrevente_id: 0,
situacao: "",
estornado: "",
apresentante: "",
cpfcnpj_apresentante: "",
selo_pessoa_nome: "",
selo_pessoa_cpfcnpj: "",
login: "",
funcao: "",
...defaults,
},
});

View file

@ -0,0 +1,5 @@
export default interface TServicoInterface {
servico_pedido_id: number;
escrevente_id: number;
apre
}

View file

@ -1,3 +1,4 @@
export default interface TServicoPedidoInterface {
servico_pedido_id?: number;
valor_pedido?: number;
@ -9,8 +10,8 @@ export default interface TServicoPedidoInterface {
escrevente_id?: number;
situacao?: string;
estornado?: string;
apresentante?: string;
nfse_id?: number;
apresentante?: string;
cpfcnpj_apresentante?: string;
selo_pessoa_nome?: string;
selo_pessoa_cpfcnpj?: string;

View file

@ -1,5 +1,6 @@
import z from "zod";
export const TServicoPedidoSchema = z.object({
servico_pedido_id: z.number().optional(),
valor_pedido: z.number().optional(),
@ -11,12 +12,13 @@ export const TServicoPedidoSchema = z.object({
escrevente_id: z.number().optional(),
situacao: z.string().optional(),
estornado: z.string().optional(),
apresentante: z.string().optional(),
nfse_id: z.number().optional(),
apresentante: z.string().optional(),
cpfcnpj_apresentante: z.string().optional(),
login: z.string().optional(),
selo_pessoa_nome: z.string().optional(),
selo_pessoa_cpfcnpj: z.string().optional(),
login: z.string().optional(),
funcao: z.string().optional()
});
export type TServicoPedidoFormValues = z.infer<typeof TServicoPedidoSchema>;

View file

@ -1,6 +1,6 @@
'use client';
import React, { createContext, useContext, useState, ReactNode } from 'react';
import React, { createContext, ReactNode, useContext, useState } from 'react';
interface ResponseState {
message?: string;

View file

@ -0,0 +1,256 @@
'use client';
import * as React from 'react';
import { cn } from '@/lib/utils'; // ajuste o caminho conforme seu projeto
/** =========================
* Constantes (com defaults)
* ========================= */
const DEFAULT_SCROLL_OFFSET = 16;
const DEFAULT_SPY_LOCK_MS = 600;
/** ===========
* Tipos
* =========== */
export type StepKey = string;
export type StepSection = {
/** chave lógica do step (ex.: 'pedido') */
key: StepKey;
/** id do elemento alvo no DOM (ex.: 'selectPedido') */
id: string;
/** ícone (lucide ou outro) */
icon: React.ReactNode;
/** título do step */
title: string;
/** descrição curta do step */
description?: string;
};
export type StepNavigatorProps = {
/** Lista de seções em ordem */
sections: StepSection[];
/** Ativo controlado externamente */
active?: StepKey;
/** Ativo padrão (modo não-controlado) */
defaultActive?: StepKey;
/** Callback ao mudar de seção (por scroll ou clique) */
onChange?: (key: StepKey) => void;
/** Offset do topo ao calcular posicionamento (ex.: header fixo) */
scrollOffset?: number;
/** Tempo de bloqueio do spy após scroll programático */
spyLockMs?: number;
/** Classe extra no container */
className?: string;
/** Se `false`, desabilita o scroll-spy */
enableScrollSpy?: boolean;
};
/** =================
* Hook: useScrollSpy
* ================= */
function useScrollSpy(opts: {
sections: StepSection[];
active: StepKey | undefined;
setActive: (key: StepKey) => void;
scrollOffset: number;
enable: boolean;
lockMs: number;
}) {
const { sections, active, setActive, scrollOffset, enable, lockMs } = opts;
// bloqueio do spy durante rolagem programática
const spyLockedRef = React.useRef(false);
const spyTimerRef = React.useRef<number | null>(null);
const lockSpy = React.useCallback(() => {
spyLockedRef.current = true;
if (spyTimerRef.current) window.clearTimeout(spyTimerRef.current);
spyTimerRef.current = window.setTimeout(() => {
spyLockedRef.current = false;
}, lockMs) as unknown as number;
}, [lockMs]);
const scrollToId = React.useCallback(
(id: string) => {
const el = document.getElementById(id);
if (!el) return;
lockSpy();
const y = el.getBoundingClientRect().top + window.scrollY - scrollOffset;
window.scrollTo({ top: y, behavior: 'smooth' });
},
[lockSpy, scrollOffset]
);
React.useEffect(() => {
if (!enable) return;
const handler = () => {
if (spyLockedRef.current) return;
let current: StepKey | undefined = undefined;
let best = Number.POSITIVE_INFINITY;
for (const { key, id } of sections) {
const el = document.getElementById(id);
if (!el) continue;
const dist = Math.abs(el.getBoundingClientRect().top - scrollOffset - 8);
if (dist < best) {
best = dist;
current = key;
}
}
if (current && current !== active) {
setActive(current);
}
};
handler(); // avalia logo ao montar
window.addEventListener('scroll', handler, { passive: true });
return () => window.removeEventListener('scroll', handler);
}, [enable, sections, active, scrollOffset, setActive]);
return { scrollToId, lockSpy };
}
/** ==================
* Subcomponente: StepLink
* ================== */
function StepLink({
active,
onClick,
icon,
title,
description,
}: {
active?: boolean;
onClick?: () => void;
icon: React.ReactNode;
title: string;
description?: string;
}) {
return (
<button
type="button"
onClick={onClick}
className={cn(
'group w-full cursor-pointer rounded-xl p-4 text-left hover:bg-gray-100 dark:hover:bg-gray-700',
active && 'ring-1 ring-primary/60'
)}
aria-current={active ? 'step' : undefined}
>
<span className="flex items-center gap-2">
<span
className={cn(
'flex h-9 w-9 items-center justify-center rounded-full',
'bg-gray-100 text-gray-900 dark:bg-gray-700 dark:text-gray-100',
'group-hover:bg-white dark:group-hover:bg-gray-800'
)}
>
{icon}
</span>
<span className="flex flex-1 flex-col">
<span className="font-bold">{title}</span>
{description ? (
<span className="text-sm text-muted-foreground">{description}</span>
) : null}
</span>
</span>
</button>
);
}
/** ===================
* Componente principal
* =================== */
export type StepNavigatorRef = {
/** Rola até a seção pela key ou id */
scrollTo: (target: StepKey | { id: string }) => void;
};
export const StepNavigator = React.forwardRef<StepNavigatorRef, StepNavigatorProps>(
(
{
sections,
active: activeProp,
defaultActive,
onChange,
scrollOffset = DEFAULT_SCROLL_OFFSET,
spyLockMs = DEFAULT_SPY_LOCK_MS,
className,
enableScrollSpy = true,
},
ref
) => {
// estado controlado/ não-controlado
const [activeState, setActiveState] = React.useState<StepKey | undefined>(
activeProp ?? defaultActive ?? sections[0]?.key
);
// sincroniza quando for controlado
React.useEffect(() => {
if (activeProp !== undefined) setActiveState(activeProp);
}, [activeProp]);
const setActive = React.useCallback(
(key: StepKey) => {
if (activeProp === undefined) {
setActiveState(key);
}
onChange?.(key);
},
[activeProp, onChange]
);
const { scrollToId, lockSpy } = useScrollSpy({
sections,
active: activeState,
setActive,
scrollOffset,
enable: enableScrollSpy,
lockMs: spyLockMs,
});
const scrollTo = React.useCallback(
(target: StepKey | { id: string }) => {
const id =
typeof target === 'string'
? sections.find((s) => s.key === target)?.id
: target.id;
if (!id) return;
scrollToId(id);
},
[sections, scrollToId]
);
React.useImperativeHandle(ref, () => ({ scrollTo }), [scrollTo]);
return (
<nav className={cn('flex flex-col gap-2', className)} aria-label="Steps">
<ol className="flex flex-col gap-2">
{sections.map((s) => (
<li key={s.key}>
<StepLink
active={activeState === s.key}
icon={s.icon}
title={s.title}
description={s.description}
onClick={() => {
// ao clicar, trava o spy por um curto período e rola
lockSpy();
setActive(s.key);
scrollTo({ id: s.id });
}}
/>
</li>
))}
</ol>
</nav>
);
}
);
StepNavigator.displayName = 'StepNavigator';