feat(client-registrations): reordena tela — Dados PJ, Sócios, Endereço, Contatos
Ordem pedida pelo usuário. O <form> único virou dois (Sócios renderiza seu próprio <form> interno entre eles, e form aninhado é inválido em HTML) — ambos chamam o mesmo handleSubmit, que sempre lê o estado React completo, então salvar continua gravando tudo de uma vez independente de qual dos dois disparou. Testado ao vivo. pnpm turbo run typecheck build: 28/28. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,10 +21,14 @@ import { PartnersSection } from "./PartnersSection.js";
|
||||
* (breadcrumb + title/status header, grouped info blocks) and `customers.tsx`
|
||||
* (status badge) — rebuilt with `@eden/ui` Card sections, one per group of
|
||||
* fields, same pattern as QuoteDetailPage/CompanyFormPage. Unlike those two
|
||||
* (pure display or pure form), this screen mixes both: everything below the
|
||||
* header is one editable form (single PATCH on submit, matching how the
|
||||
* API accepts a fully partial `UpdateClientRegistrationDto`), plus a
|
||||
* separate Sócios sub-section with its own independent list/add/edit calls.
|
||||
* (pure display or pure form), this screen mixes both: one editable form
|
||||
* (single PATCH on submit, matching how the API accepts a fully partial
|
||||
* `UpdateClientRegistrationDto`) split across two `<form>` elements —
|
||||
* Status/Dados PF-PJ, then (after the non-form Sócios sub-section) Endereço/
|
||||
* Contatos/Portabilidade — both calling the same `handleSubmit`, which
|
||||
* always reads the complete React `form` state regardless of which one
|
||||
* triggered it, so the split is purely visual (see the JSX comment above
|
||||
* the first `<form>`). Sócios has its own independent list/add/edit calls.
|
||||
*/
|
||||
const STATUS_LABEL: Record<RegistrationStatus, string> = {
|
||||
rascunho: "Rascunho",
|
||||
@@ -350,6 +354,15 @@ export function ClientRegistrationDetailPage() {
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{/*
|
||||
Ordem pedida pelo usuário: Dados PJ/PF -> Sócios -> Endereço ->
|
||||
Contatos. `Status` fica primeiro (não mencionado, posição natural) e
|
||||
`Portabilidade`/botão de salvar ficam no final (idem). `PartnersSection`
|
||||
renderiza seu próprio <form> interno (form aninhado é inválido em
|
||||
HTML), então o form único original virou dois — ambos chamam o mesmo
|
||||
`handleSubmit`, que sempre lê o estado React completo (`form`), não o
|
||||
DOM, então dividir em dois <form> não muda o que é salvo.
|
||||
*/}
|
||||
<form className="space-y-4" onSubmit={handleSubmit}>
|
||||
<Card title="Status">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
@@ -381,113 +394,6 @@ export function ClientRegistrationDetailPage() {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="Endereço">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<Input label="CEP" value={form.addressZip} onChange={(e) => updateField("addressZip", e.target.value)} />
|
||||
<div className="sm:col-span-2">
|
||||
<Input
|
||||
label="Logradouro"
|
||||
value={form.addressStreet}
|
||||
onChange={(e) => updateField("addressStreet", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
label="Número"
|
||||
value={form.addressNumber}
|
||||
onChange={(e) => updateField("addressNumber", e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="Complemento"
|
||||
value={form.addressComplement}
|
||||
onChange={(e) => updateField("addressComplement", e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="Bairro"
|
||||
value={form.addressNeighborhood}
|
||||
onChange={(e) => updateField("addressNeighborhood", e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="Cidade"
|
||||
value={form.addressCity}
|
||||
onChange={(e) => updateField("addressCity", e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="UF"
|
||||
maxLength={2}
|
||||
value={form.addressState}
|
||||
onChange={(e) => updateField("addressState", e.target.value.toUpperCase())}
|
||||
/>
|
||||
<Input
|
||||
label="País"
|
||||
value={form.addressCountry}
|
||||
onChange={(e) => updateField("addressCountry", e.target.value)}
|
||||
placeholder="Brasil"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="Contatos">
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs font-semibold uppercase text-default">Principal</p>
|
||||
<Input
|
||||
label="Nome"
|
||||
value={form.contactPrincipalName}
|
||||
onChange={(e) => updateField("contactPrincipalName", e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="E-mail"
|
||||
type="email"
|
||||
value={form.contactPrincipalEmail}
|
||||
onChange={(e) => updateField("contactPrincipalEmail", e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="Telefone"
|
||||
value={form.contactPrincipalPhone}
|
||||
onChange={(e) => updateField("contactPrincipalPhone", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs font-semibold uppercase text-default">Financeiro</p>
|
||||
<Input
|
||||
label="Nome"
|
||||
value={form.contactFinancialName}
|
||||
onChange={(e) => updateField("contactFinancialName", e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="E-mail"
|
||||
type="email"
|
||||
value={form.contactFinancialEmail}
|
||||
onChange={(e) => updateField("contactFinancialEmail", e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="Telefone"
|
||||
value={form.contactFinancialPhone}
|
||||
onChange={(e) => updateField("contactFinancialPhone", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs font-semibold uppercase text-default">Técnico</p>
|
||||
<Input
|
||||
label="Nome"
|
||||
value={form.contactTechnicalName}
|
||||
onChange={(e) => updateField("contactTechnicalName", e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="E-mail"
|
||||
type="email"
|
||||
value={form.contactTechnicalEmail}
|
||||
onChange={(e) => updateField("contactTechnicalEmail", e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="Telefone"
|
||||
value={form.contactTechnicalPhone}
|
||||
onChange={(e) => updateField("contactTechnicalPhone", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{registration.person_type === "pf" ? (
|
||||
<Card title="Dados PF">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
@@ -666,6 +572,125 @@ export function ClientRegistrationDetailPage() {
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</form>
|
||||
|
||||
{registration.person_type === "pj" ? (
|
||||
<div className="mt-4">
|
||||
<PartnersSection
|
||||
registrationId={registration.id}
|
||||
accessToken={accessToken}
|
||||
refreshKey={partnersRefreshKey}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<form className="mt-4 space-y-4" onSubmit={handleSubmit}>
|
||||
<Card title="Endereço">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<Input label="CEP" value={form.addressZip} onChange={(e) => updateField("addressZip", e.target.value)} />
|
||||
<div className="sm:col-span-2">
|
||||
<Input
|
||||
label="Logradouro"
|
||||
value={form.addressStreet}
|
||||
onChange={(e) => updateField("addressStreet", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
label="Número"
|
||||
value={form.addressNumber}
|
||||
onChange={(e) => updateField("addressNumber", e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="Complemento"
|
||||
value={form.addressComplement}
|
||||
onChange={(e) => updateField("addressComplement", e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="Bairro"
|
||||
value={form.addressNeighborhood}
|
||||
onChange={(e) => updateField("addressNeighborhood", e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="Cidade"
|
||||
value={form.addressCity}
|
||||
onChange={(e) => updateField("addressCity", e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="UF"
|
||||
maxLength={2}
|
||||
value={form.addressState}
|
||||
onChange={(e) => updateField("addressState", e.target.value.toUpperCase())}
|
||||
/>
|
||||
<Input
|
||||
label="País"
|
||||
value={form.addressCountry}
|
||||
onChange={(e) => updateField("addressCountry", e.target.value)}
|
||||
placeholder="Brasil"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="Contatos">
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs font-semibold uppercase text-default">Principal</p>
|
||||
<Input
|
||||
label="Nome"
|
||||
value={form.contactPrincipalName}
|
||||
onChange={(e) => updateField("contactPrincipalName", e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="E-mail"
|
||||
type="email"
|
||||
value={form.contactPrincipalEmail}
|
||||
onChange={(e) => updateField("contactPrincipalEmail", e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="Telefone"
|
||||
value={form.contactPrincipalPhone}
|
||||
onChange={(e) => updateField("contactPrincipalPhone", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs font-semibold uppercase text-default">Financeiro</p>
|
||||
<Input
|
||||
label="Nome"
|
||||
value={form.contactFinancialName}
|
||||
onChange={(e) => updateField("contactFinancialName", e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="E-mail"
|
||||
type="email"
|
||||
value={form.contactFinancialEmail}
|
||||
onChange={(e) => updateField("contactFinancialEmail", e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="Telefone"
|
||||
value={form.contactFinancialPhone}
|
||||
onChange={(e) => updateField("contactFinancialPhone", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs font-semibold uppercase text-default">Técnico</p>
|
||||
<Input
|
||||
label="Nome"
|
||||
value={form.contactTechnicalName}
|
||||
onChange={(e) => updateField("contactTechnicalName", e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="E-mail"
|
||||
type="email"
|
||||
value={form.contactTechnicalEmail}
|
||||
onChange={(e) => updateField("contactTechnicalEmail", e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="Telefone"
|
||||
value={form.contactTechnicalPhone}
|
||||
onChange={(e) => updateField("contactTechnicalPhone", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{registration.is_portability ? (
|
||||
<Card title="Portabilidade" bodyClassName="p-5">
|
||||
@@ -686,16 +711,6 @@ export function ClientRegistrationDetailPage() {
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{registration.person_type === "pj" ? (
|
||||
<div className="mt-4">
|
||||
<PartnersSection
|
||||
registrationId={registration.id}
|
||||
accessToken={accessToken}
|
||||
refreshKey={partnersRefreshKey}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -720,3 +720,14 @@ Verificado: `pnpm turbo run typecheck build` 28/28, suíte 100/100 (95→100, 5
|
||||
|
||||
### Pendências
|
||||
Lookup de CPF pra pessoa física — fora de escopo, não foi pedido. Refresh automático da situação cadastral/endereço (ex. um job periódico) não existe — "Buscar novamente" é manual, sob demanda.
|
||||
|
||||
---
|
||||
|
||||
## Reordenação do formulário de Cadastro de Cliente
|
||||
|
||||
Data: 2026-09-18. Usuário pediu: "primeiro vem os dados PJ depois vem os sócios depois endereço e depois contatos". Implementado diretamente (mudança de layout, sem lógica nova). Ordem final: Status (não mencionado, mantido primeiro) → Dados PF/PJ → Sócios (só PJ) → Endereço → Contatos → Portabilidade/botão de salvar (não mencionados, mantidos no final).
|
||||
|
||||
`ClientRegistrationDetailPage.tsx`: o `<form>` único virou dois — `PartnersSection` (Sócios) já renderiza seu próprio `<form>` interno (form aninhado é HTML inválido), então não dá pra intercalar sem quebrar essa regra. Os dois `<form>` chamam o mesmo `handleSubmit`, que sempre lê o estado React completo (`form`), nunca o DOM — testado ao vivo que salvar a partir do botão (agora depois de Contatos, no segundo `<form>`) grava corretamente campos da seção Status (primeiro `<form>`, antes de Sócios) junto com campos de Endereço (segundo `<form>`), confirmando que a divisão é só visual.
|
||||
|
||||
### Verificação
|
||||
`pnpm turbo run typecheck build` 28/28. Suíte 100/100 (inalterada — mudança é só de frontend). Testado ao vivo em navegador real via Playwright: ordem das seções confirmada por leitura do DOM (`Status, Dados PJ, Sócios, Endereço, Contatos`) e por screenshot; ciclo completo com CNPJ real (Petrobras) mostrando os 10 sócios na nova posição; salvamento testado preenchendo um campo de cada `<form>` e confirmando após reload que os dois persistiram. Cadastro de teste desativado ao final.
|
||||
|
||||
Reference in New Issue
Block a user