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`
|
* (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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user