feat(resellers): campos de representante legal (preparação p/ contrato)

Usuário: o cadastro de revenda precisa ficar completo porque essa
mesma base vai alimentar a criação de contrato com a revenda dentro
do ERP. resellers ganha rep_full_name/rep_cpf/rep_role/rep_email/
rep_mobile_phone/rep_authorization_ack, mesmo shape de
client_registrations.pj_rep_* — um contrato precisa de um signatário
nomeado e autorizado, não só a lista geral de sócios.

Divergência deliberada: ao contrário de pj_rep_cpf (bloqueado do PATCH
genérico em client_registrations, esperando um fluxo de submissão
pública que o EDEN ainda não construiu — hoje uma lacuna conhecida,
nunca preenchido de fato), rep_cpf aqui fica livremente editável —
revenda não tem esse fluxo de submissão pública pra travar atrás.

Nova tela: card "Representante legal" entre "Dados da revenda" e
"Sócios". Suíte: 107/107 (106→107). pnpm turbo run typecheck build:
28/28. Testado ao vivo em navegador real.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 15:41:16 -03:00
parent ec13dccee6
commit d81553fcfe
8 changed files with 240 additions and 4 deletions

View File

@@ -39,6 +39,12 @@ export interface Reseller {
contact_technical_name: string | null;
contact_technical_email: string | null;
contact_technical_phone: string | null;
rep_full_name: string | null;
rep_cpf: string | null;
rep_role: string | null;
rep_email: string | null;
rep_mobile_phone: string | null;
rep_authorization_ack: boolean;
}
/** Request body — camelCase, matches `CreateResellerDto` exactly. */
@@ -74,6 +80,13 @@ export interface CreateResellerInput {
contactTechnicalName?: string;
contactTechnicalEmail?: string;
contactTechnicalPhone?: string;
repFullName?: string;
repCpf?: string;
repRole?: string;
repEmail?: string;
repMobilePhone?: string;
repAuthorizationAck?: boolean;
}
/** Matches `UpdateResellerDto`: everything optional. */

View File

@@ -23,7 +23,15 @@ import { ResellerPartnersSection } from "./ResellerPartnersSection.js";
*
* Section order when editing (per product direction, same order as
* `ClientRegistrationDetailPage`): Dados da revenda (incl. CNPJ lookup) ->
* Sócios -> Endereço -> Contatos -> Status.
* Representante legal -> Sócios -> Endereço -> Contatos -> Status.
*
* "Representante legal" (`rep_*`) mirrors `client_registrations.pj_rep_*`
* — added because reseller registration data will feed reseller contract
* creation later; a contract needs a named, authorized signer, not just
* the general Sócios list. Unlike `pj_rep_cpf` there, `repCpf` here is a
* plain editable field — see the migration
* `1789700100000_resellers-legal-representative.cjs` for why the same
* structural lock doesn't apply to resellers.
*/
interface FormState {
name: string;
@@ -39,6 +47,13 @@ interface FormState {
cnpjSituation: string;
mainActivity: string;
repFullName: string;
repCpf: string;
repRole: string;
repEmail: string;
repMobilePhone: string;
repAuthorizationAck: boolean;
addressZip: string;
addressStreet: string;
addressNumber: string;
@@ -73,6 +88,13 @@ const EMPTY_FORM: FormState = {
cnpjSituation: "",
mainActivity: "",
repFullName: "",
repCpf: "",
repRole: "",
repEmail: "",
repMobilePhone: "",
repAuthorizationAck: false,
addressZip: "",
addressStreet: "",
addressNumber: "",
@@ -112,6 +134,13 @@ function toFormState(reseller: Reseller): FormState {
cnpjSituation: reseller.cnpj_situation ?? "",
mainActivity: reseller.main_activity ?? "",
repFullName: reseller.rep_full_name ?? "",
repCpf: reseller.rep_cpf ?? "",
repRole: reseller.rep_role ?? "",
repEmail: reseller.rep_email ?? "",
repMobilePhone: reseller.rep_mobile_phone ?? "",
repAuthorizationAck: reseller.rep_authorization_ack,
addressZip: reseller.address_zip ?? "",
addressStreet: reseller.address_street ?? "",
addressNumber: reseller.address_number ?? "",
@@ -206,6 +235,13 @@ export function ResellerFormPage() {
cnpjSituation: toOptional(form.cnpjSituation),
mainActivity: toOptional(form.mainActivity),
repFullName: toOptional(form.repFullName),
repCpf: toOptional(form.repCpf),
repRole: toOptional(form.repRole),
repEmail: toOptional(form.repEmail),
repMobilePhone: toOptional(form.repMobilePhone),
repAuthorizationAck: form.repAuthorizationAck,
addressZip: toOptional(form.addressZip),
addressStreet: toOptional(form.addressStreet),
addressNumber: toOptional(form.addressNumber),
@@ -418,6 +454,43 @@ export function ResellerFormPage() {
) : null}
</div>
</Card>
{isEditing ? (
<Card title="Representante legal">
<p className="mb-3 text-xs text-default">
Quem assina pela revenda — usado depois na confecção de contrato com a revenda.
</p>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
<Input
label="Nome completo"
value={form.repFullName}
onChange={(e) => updateField("repFullName", e.target.value)}
/>
<Input label="CPF" value={form.repCpf} onChange={(e) => updateField("repCpf", e.target.value)} />
<Input label="Cargo" value={form.repRole} onChange={(e) => updateField("repRole", e.target.value)} />
<Input
label="E-mail"
type="email"
value={form.repEmail}
onChange={(e) => updateField("repEmail", e.target.value)}
/>
<Input
label="Celular"
value={form.repMobilePhone}
onChange={(e) => updateField("repMobilePhone", e.target.value)}
/>
<label className="flex items-center gap-2 py-2.5 text-sm text-title">
<input
type="checkbox"
className="shrink-0 rounded-sm border-border-color text-primary focus:ring-primary"
checked={form.repAuthorizationAck}
onChange={(e) => updateField("repAuthorizationAck", e.target.checked)}
/>
Representante confirmou autorização
</label>
</div>
</Card>
) : null}
</form>
{isEditing && id ? (