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:
2026-09-18 13:30:35 -03:00
parent 93cd42b22e
commit 205d83a5f5
2 changed files with 147 additions and 121 deletions

View File

@@ -21,10 +21,14 @@ import { PartnersSection } from "./PartnersSection.js";
* (breadcrumb + title/status header, grouped info blocks) and `customers.tsx` * (breadcrumb + title/status header, grouped info blocks) and `customers.tsx`
* (status badge) — rebuilt with `@eden/ui` Card sections, one per group of * (status badge) — rebuilt with `@eden/ui` Card sections, one per group of
* fields, same pattern as QuoteDetailPage/CompanyFormPage. Unlike those two * fields, same pattern as QuoteDetailPage/CompanyFormPage. Unlike those two
* (pure display or pure form), this screen mixes both: everything below the * (pure display or pure form), this screen mixes both: one editable form
* header is one editable form (single PATCH on submit, matching how the * (single PATCH on submit, matching how the API accepts a fully partial
* API accepts a fully partial `UpdateClientRegistrationDto`), plus a * `UpdateClientRegistrationDto`) split across two `<form>` elements —
* separate Sócios sub-section with its own independent list/add/edit calls. * 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> = { const STATUS_LABEL: Record<RegistrationStatus, string> = {
rascunho: "Rascunho", rascunho: "Rascunho",
@@ -350,6 +354,15 @@ export function ClientRegistrationDetailPage() {
</Alert> </Alert>
) : null} ) : 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}> <form className="space-y-4" onSubmit={handleSubmit}>
<Card title="Status"> <Card title="Status">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
@@ -381,113 +394,6 @@ export function ClientRegistrationDetailPage() {
</div> </div>
</Card> </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" ? ( {registration.person_type === "pf" ? (
<Card title="Dados PF"> <Card title="Dados PF">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
@@ -666,6 +572,125 @@ export function ClientRegistrationDetailPage() {
</div> </div>
</Card> </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 ? ( {registration.is_portability ? (
<Card title="Portabilidade" bodyClassName="p-5"> <Card title="Portabilidade" bodyClassName="p-5">
@@ -686,16 +711,6 @@ export function ClientRegistrationDetailPage() {
</Button> </Button>
</div> </div>
</form> </form>
{registration.person_type === "pj" ? (
<div className="mt-4">
<PartnersSection
registrationId={registration.id}
accessToken={accessToken}
refreshKey={partnersRefreshKey}
/>
</div>
) : null}
</div> </div>
); );
} }

View File

@@ -720,3 +720,14 @@ Verificado: `pnpm turbo run typecheck build` 28/28, suíte 100/100 (95→100, 5
### Pendências ### 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. 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.