feat(web+api): tela de cadastro de cliente novo (CNPJ/CPF) com sync ERP
Web (NewClientPage):
- Segmentado PJ/CPF com campos específicos por categoria
- PJ: razão social, nome fantasia, CNPJ, IE, indicador IE, consumidor final
- PF: nome completo, CPF
- Endereço com auto-preenchimento via ViaCEP (CEP → rua/bairro/município)
- Município: Select com busca server-side (/catalog/municipios)
- Contato: DDD, telefone, email
- Comercial: forma pagamento, tabela de preços, limite crédito
- Feedback: código ERP exibido no sucesso; alerta de erro de sync sem bloquear
API:
- GET /catalog/municipios?q= — busca municipios da vw_municipios
- POST /clients/novo — INSERT em sar.clientes_novos; trigger sincroniza para sig.corrent
- Retorna { id, idCorrentErp, sincronizado, erroSync } após o trigger disparar
- Fix trigger fn_sync_cliente_to_erp: NULLIF para cgcpf vazio (evita violação
do unique index key_corrent_cgcpf quando cliente não tem CPF/CNPJ)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1291,7 +1291,7 @@ export function ClientsPage() {
|
||||
icon={<PlusOutlined />}
|
||||
size="large"
|
||||
style={{ borderRadius: 8, fontWeight: 600 }}
|
||||
onClick={() => void msg.info('Cadastro de clientes em breve.')}
|
||||
onClick={() => void navigate({ to: '/clientes/novo' })}
|
||||
>
|
||||
Cadastrar Cliente
|
||||
</Button>
|
||||
@@ -1318,7 +1318,7 @@ export function ClientsPage() {
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
style={{ borderRadius: 8, fontWeight: 600 }}
|
||||
onClick={() => void msg.info('Cadastro de clientes em breve.')}
|
||||
onClick={() => void navigate({ to: '/clientes/novo' })}
|
||||
>
|
||||
Cadastrar Cliente
|
||||
</Button>
|
||||
@@ -1549,7 +1549,7 @@ export function ClientsPage() {
|
||||
shape="circle"
|
||||
icon={<PlusOutlined />}
|
||||
size="large"
|
||||
onClick={() => void msg.info('Cadastro de clientes em breve.')}
|
||||
onClick={() => void navigate({ to: '/clientes/novo' })}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
bottom: 24,
|
||||
|
||||
430
apps/web/src/cockpits/rep/NewClientPage.tsx
Normal file
430
apps/web/src/cockpits/rep/NewClientPage.tsx
Normal file
@@ -0,0 +1,430 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Row,
|
||||
Segmented,
|
||||
Select,
|
||||
Spin,
|
||||
Switch,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
BankOutlined,
|
||||
CheckCircleOutlined,
|
||||
EnvironmentOutlined,
|
||||
PhoneOutlined,
|
||||
ShopOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import type { CreateClientNovo } from '@sar/api-interface';
|
||||
import { useMunicipios, usePautas, useFormasPagamento } from '../../lib/queries/catalog';
|
||||
import { useCreateClientNovo } from '../../lib/queries/clients';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
type Pessoa = 'PJ' | 'PF';
|
||||
|
||||
function Section({
|
||||
icon,
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 16,
|
||||
paddingBottom: 8,
|
||||
borderBottom: '1px solid #EBF0F5',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: '#003B8E', fontSize: 16 }}>{icon}</span>
|
||||
<Text strong style={{ fontSize: 14, color: '#003B8E' }}>
|
||||
{title}
|
||||
</Text>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function NewClientPage() {
|
||||
const navigate = useNavigate();
|
||||
const { message } = App.useApp();
|
||||
const [form] = Form.useForm();
|
||||
const [pessoa, setPessoa] = useState<Pessoa>('PJ');
|
||||
const [municipioSearch, setMunicipioSearch] = useState('');
|
||||
const [syncError, setSyncError] = useState<string | null>(null);
|
||||
|
||||
const { data: municipios = [], isFetching: loadingMun } = useMunicipios(
|
||||
municipioSearch || undefined,
|
||||
);
|
||||
const { data: pautas = [] } = usePautas();
|
||||
const { data: formas = [] } = useFormasPagamento();
|
||||
const createMutation = useCreateClientNovo();
|
||||
|
||||
async function handleCepBlur(cep: string) {
|
||||
const digits = cep.replace(/\D/g, '');
|
||||
if (digits.length !== 8) return;
|
||||
try {
|
||||
const res = await fetch(`https://viacep.com.br/ws/${digits}/json/`);
|
||||
const data = (await res.json()) as {
|
||||
erro?: boolean;
|
||||
logradouro?: string;
|
||||
bairro?: string;
|
||||
localidade?: string;
|
||||
uf?: string;
|
||||
};
|
||||
if (data.erro) return;
|
||||
if (data.logradouro) form.setFieldValue('endereco', data.logradouro);
|
||||
if (data.bairro) form.setFieldValue('bairro', data.bairro);
|
||||
if (data.localidade && data.uf) {
|
||||
setMunicipioSearch(data.localidade);
|
||||
}
|
||||
} catch {
|
||||
// silencioso — CEP inválido não bloqueia
|
||||
}
|
||||
}
|
||||
|
||||
async function onFinish(values: Record<string, unknown>) {
|
||||
setSyncError(null);
|
||||
|
||||
const payload: CreateClientNovo = {
|
||||
pesso: pessoa === 'PJ' ? 0 : 1,
|
||||
consfinal: values.consfinal ? 1 : 0,
|
||||
nome: String(values.nome ?? '').slice(0, 40),
|
||||
razao: String(values.razao ?? values.nome ?? '').slice(0, 65),
|
||||
cgcpf: values.cgcpf ? String(values.cgcpf) : undefined,
|
||||
sufCgcpf: '',
|
||||
inscr: values.inscr ? String(values.inscr) : '',
|
||||
indicadorIe: values.indicadorIe as 1 | 2 | 9 | undefined,
|
||||
endereco: String(values.endereco ?? '').slice(0, 60),
|
||||
numEndereco: String(values.numEndereco ?? '').slice(0, 10),
|
||||
bairro: String(values.bairro ?? '').slice(0, 60),
|
||||
idMunicipio: Number(values.idMunicipio),
|
||||
cep: String(values.cep ?? '')
|
||||
.replace(/\D/g, '')
|
||||
.slice(0, 9),
|
||||
ddd: String(values.ddd ?? '').slice(0, 4),
|
||||
telefone: String(values.telefone ?? '').slice(0, 35),
|
||||
email: String(values.email ?? '').slice(0, 80),
|
||||
limiteCredito: Number(values.limiteCredito ?? 0),
|
||||
codFormapag: values.codFormapag ? Number(values.codFormapag) : undefined,
|
||||
codPauta: values.codPauta ? Number(values.codPauta) : undefined,
|
||||
};
|
||||
|
||||
const result = await createMutation.mutateAsync(payload);
|
||||
|
||||
if (!result.sincronizado && result.erroSync) {
|
||||
setSyncError(result.erroSync);
|
||||
void message.warning('Cliente cadastrado, mas houve erro na sincronização com o ERP.');
|
||||
} else {
|
||||
void message.success(
|
||||
`Cliente cadastrado com sucesso! Código ERP: ${result.idCorrentErp ?? '—'}`,
|
||||
);
|
||||
void navigate({ to: '/clientes' });
|
||||
}
|
||||
}
|
||||
|
||||
const isPJ = pessoa === 'PJ';
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 860, margin: '0 auto', padding: '24px 16px' }}>
|
||||
{/* Cabeçalho */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 24 }}>
|
||||
<Button
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => void navigate({ to: '/clientes' })}
|
||||
style={{ borderRadius: 8 }}
|
||||
>
|
||||
Voltar
|
||||
</Button>
|
||||
<div>
|
||||
<Title level={3} style={{ margin: 0, color: '#003B8E' }}>
|
||||
Cadastrar Cliente
|
||||
</Title>
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
Após salvar, o cliente é criado automaticamente no ERP.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Seletor PJ / PF */}
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<Segmented
|
||||
size="large"
|
||||
value={pessoa}
|
||||
onChange={(v) => {
|
||||
setPessoa(v as Pessoa);
|
||||
form.resetFields(['cgcpf', 'inscr', 'indicadorIe', 'consfinal', 'razao']);
|
||||
}}
|
||||
options={[
|
||||
{ value: 'PJ', icon: <BankOutlined />, label: 'CNPJ — Pessoa Jurídica' },
|
||||
{ value: 'PF', icon: <UserOutlined />, label: 'CPF — Pessoa Física' },
|
||||
]}
|
||||
style={{ borderRadius: 10 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={(v) => void onFinish(v as Record<string, unknown>)}
|
||||
requiredMark="optional"
|
||||
initialValues={{ consfinal: false, limiteCredito: 0 }}
|
||||
>
|
||||
<Card
|
||||
style={{ borderRadius: 12, border: '1px solid #EBF0F5', marginBottom: 16 }}
|
||||
styles={{ body: { padding: '20px 24px' } }}
|
||||
>
|
||||
{/* ── Identificação ─────────────────────────────────────────── */}
|
||||
<Section icon={isPJ ? <ShopOutlined /> : <UserOutlined />} title="Identificação">
|
||||
<Row gutter={[16, 0]}>
|
||||
{isPJ ? (
|
||||
<>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item
|
||||
name="razao"
|
||||
label="Razão Social"
|
||||
rules={[{ required: true, message: 'Informe a razão social' }]}
|
||||
>
|
||||
<Input placeholder="Razão Social Ltda" maxLength={65} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item
|
||||
name="nome"
|
||||
label="Nome Fantasia"
|
||||
rules={[{ required: true, message: 'Informe o nome fantasia' }]}
|
||||
>
|
||||
<Input placeholder="Nome fantasia" maxLength={40} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Form.Item name="cgcpf" label="CNPJ">
|
||||
<Input placeholder="00.000.000/0000-00" maxLength={18} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Form.Item name="inscr" label="Inscrição Estadual">
|
||||
<Input placeholder="000.000.000" maxLength={18} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Form.Item name="indicadorIe" label="Indicador IE">
|
||||
<Select placeholder="Selecione" allowClear>
|
||||
<Select.Option value={1}>Contribuinte</Select.Option>
|
||||
<Select.Option value={2}>Isento</Select.Option>
|
||||
<Select.Option value={9}>Não contribuinte</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Form.Item name="consfinal" label="Consumidor Final" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Col xs={24} md={16}>
|
||||
<Form.Item
|
||||
name="nome"
|
||||
label="Nome Completo"
|
||||
rules={[{ required: true, message: 'Informe o nome' }]}
|
||||
>
|
||||
<Input placeholder="Nome completo" maxLength={40} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Form.Item name="cgcpf" label="CPF">
|
||||
<Input placeholder="000.000.000-00" maxLength={14} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
{/* ── Endereço ──────────────────────────────────────────────── */}
|
||||
<Section icon={<EnvironmentOutlined />} title="Endereço">
|
||||
<Row gutter={[16, 0]}>
|
||||
<Col xs={24} md={6}>
|
||||
<Form.Item name="cep" label="CEP">
|
||||
<Input
|
||||
placeholder="00000-000"
|
||||
maxLength={9}
|
||||
onBlur={(e) => void handleCepBlur(e.target.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={14}>
|
||||
<Form.Item name="endereco" label="Endereço">
|
||||
<Input placeholder="Rua / Av." maxLength={60} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={4}>
|
||||
<Form.Item name="numEndereco" label="Número">
|
||||
<Input placeholder="123" maxLength={10} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={10}>
|
||||
<Form.Item name="bairro" label="Bairro">
|
||||
<Input placeholder="Bairro" maxLength={60} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={14}>
|
||||
<Form.Item
|
||||
name="idMunicipio"
|
||||
label="Município"
|
||||
rules={[{ required: true, message: 'Selecione o município' }]}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
placeholder="Digite para buscar..."
|
||||
filterOption={false}
|
||||
onSearch={setMunicipioSearch}
|
||||
notFoundContent={loadingMun ? <Spin size="small" /> : 'Nenhum resultado'}
|
||||
optionLabelProp="label"
|
||||
>
|
||||
{municipios.map((m) => (
|
||||
<Select.Option
|
||||
key={m.idMunicipio}
|
||||
value={m.idMunicipio}
|
||||
label={`${m.nome} — ${m.uf}`}
|
||||
>
|
||||
{m.nome}{' '}
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{m.uf}
|
||||
</Text>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
{/* ── Contato ───────────────────────────────────────────────── */}
|
||||
<Section icon={<PhoneOutlined />} title="Contato">
|
||||
<Row gutter={[16, 0]}>
|
||||
<Col xs={8} md={4}>
|
||||
<Form.Item name="ddd" label="DDD">
|
||||
<Input placeholder="48" maxLength={4} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={16} md={8}>
|
||||
<Form.Item name="telefone" label="Telefone">
|
||||
<Input placeholder="99999-9999" maxLength={35} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="email" label="E-mail">
|
||||
<Input placeholder="contato@empresa.com.br" maxLength={80} type="email" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
{/* ── Comercial ─────────────────────────────────────────────── */}
|
||||
<Section icon={<BankOutlined />} title="Comercial">
|
||||
<Row gutter={[16, 0]}>
|
||||
<Col xs={24} md={8}>
|
||||
<Form.Item name="codFormapag" label="Forma de Pagamento">
|
||||
<Select placeholder="Selecione" allowClear>
|
||||
{formas.map((f) => (
|
||||
<Select.Option key={f.codigo} value={f.codigo}>
|
||||
{f.descricao}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Form.Item name="codPauta" label="Tabela de Preços">
|
||||
<Select placeholder="Selecione" allowClear>
|
||||
{pautas.map((p) => (
|
||||
<Select.Option key={p.idPauta} value={p.idPauta}>
|
||||
{p.descricao}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Form.Item name="limiteCredito" label="Limite de Crédito">
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
min={0}
|
||||
step={100}
|
||||
formatter={(v) => `R$ ${v ?? 0}`.replace(/\B(?=(\d{3})+(?!\d))/g, '.')}
|
||||
parser={(v) => Number((v ?? '').replace(/R\$\s?\.*/g, '').replace(',', '.'))}
|
||||
placeholder="0,00"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
{syncError && (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="Falha na sincronização com o ERP"
|
||||
description={syncError}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Rodapé sticky */}
|
||||
<div
|
||||
style={{
|
||||
position: 'sticky',
|
||||
bottom: 0,
|
||||
background: '#fff',
|
||||
borderTop: '1px solid #EBF0F5',
|
||||
padding: '12px 24px',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 12,
|
||||
borderRadius: '0 0 12px 12px',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={() => void navigate({ to: '/clientes' })}
|
||||
style={{ borderRadius: 8 }}
|
||||
disabled={createMutation.isPending}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
icon={<CheckCircleOutlined />}
|
||||
loading={createMutation.isPending}
|
||||
style={{ borderRadius: 8, fontWeight: 600, minWidth: 160 }}
|
||||
>
|
||||
{createMutation.isPending ? 'Cadastrando...' : 'Cadastrar Cliente'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
FormaPagamentoSchema,
|
||||
MunicipioSchema,
|
||||
PautaSchema,
|
||||
ProdutoListResponseSchema,
|
||||
ProdutoDetailSchema,
|
||||
type FormaPagamento,
|
||||
type Municipio,
|
||||
type ProdutoListQuery,
|
||||
type ProdutoListResponse,
|
||||
type ProdutoDetail,
|
||||
@@ -13,6 +15,19 @@ import {
|
||||
import { z } from 'zod';
|
||||
import { apiFetch } from '../api-client';
|
||||
|
||||
export function useMunicipios(q?: string) {
|
||||
return useQuery<Municipio[]>({
|
||||
queryKey: ['catalog', 'municipios', q ?? ''],
|
||||
queryFn: async () => {
|
||||
const qs = q ? `?q=${encodeURIComponent(q)}` : '';
|
||||
const res = await apiFetch(`/catalog/municipios${qs}`);
|
||||
return z.array(MunicipioSchema).parse(res);
|
||||
},
|
||||
staleTime: 24 * 60 * 60 * 1000,
|
||||
enabled: !q || q.length >= 2,
|
||||
});
|
||||
}
|
||||
|
||||
export function usePautas() {
|
||||
return useQuery<Pauta[]>({
|
||||
queryKey: ['catalog', 'pautas'],
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
ClientListResponseSchema,
|
||||
ClientDetailSchema,
|
||||
ClientListResponseSchema,
|
||||
ClientNovoResultSchema,
|
||||
type ClientDetail,
|
||||
type ClientListQuery,
|
||||
type ClientListResponse,
|
||||
type ClientDetail,
|
||||
type ClientNovoResult,
|
||||
type CreateClientNovo,
|
||||
} from '@sar/api-interface';
|
||||
import { apiFetch } from '../api-client';
|
||||
|
||||
@@ -42,3 +45,20 @@ export function useClientDetail(id: number | string | undefined) {
|
||||
enabled: !!id,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateClientNovo() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<ClientNovoResult, Error, CreateClientNovo>({
|
||||
mutationFn: async (data) => {
|
||||
const res = await apiFetch('/clients/novo', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return ClientNovoResultSchema.parse(res);
|
||||
},
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: CLIENT_KEYS.all });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ClientDetailPage } from '../cockpits/rep/ClientDetailPage';
|
||||
import { OrdersPage } from '../cockpits/rep/OrdersPage';
|
||||
import { OrderDetailPage } from '../cockpits/rep/OrderDetailPage';
|
||||
import { OrderPrintPage } from '../cockpits/rep/OrderPrintPage';
|
||||
import { NewClientPage } from '../cockpits/rep/NewClientPage';
|
||||
import { NewOrderPage } from '../cockpits/rep/NewOrderPage';
|
||||
import { CatalogPage } from '../cockpits/rep/CatalogPage';
|
||||
import { ApprovalQueuePage } from '../cockpits/supervisor/ApprovalQueuePage';
|
||||
@@ -75,6 +76,12 @@ const clientesRoute = createRoute({
|
||||
component: ClientsPage,
|
||||
});
|
||||
|
||||
const novoClienteRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/clientes/novo',
|
||||
component: NewClientPage,
|
||||
});
|
||||
|
||||
const clienteDetailRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/clientes/$id',
|
||||
@@ -121,6 +128,7 @@ const routeTree = rootRoute.addChildren([
|
||||
indexRoute,
|
||||
rafaelRoute,
|
||||
clientesRoute,
|
||||
novoClienteRoute,
|
||||
clienteDetailRoute,
|
||||
pedidosRoute,
|
||||
novoOrderRoute,
|
||||
|
||||
Reference in New Issue
Block a user