feat(resellers): UI de CNPJ/sócios/endereço/contatos, na ordem certa

Mesma UI construída pro Cadastro de Cliente, aplicada a Revenda, já
direto na ordem pedida (Dados -> Sócios -> Endereço -> Contatos), sem
precisar de uma segunda leva de reordenação. Criar uma revenda agora
redireciona pra edição dela (não pra lista) já que CNPJ-lookup/Sócios
só fazem sentido com um id existente. PartnersSection generalizada
para reuso entre Cliente e Revenda.

Corrige um bug real encontrado na verificação ao vivo: o rótulo do
botão de busca estava amarrado ao que estava sendo digitado, não ao
que já foi salvo — digitar um CNPJ válido trocava pra "Buscar
novamente" antes de qualquer busca real acontecer.

Suíte: 106/106 (inalterada). pnpm turbo run typecheck build: 28/28.
Testado ao vivo com CNPJ real (Petrobras).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 15:18:34 -03:00
parent 2ae61dba22
commit ec13dccee6
5 changed files with 943 additions and 46 deletions

View File

@@ -1,7 +1,7 @@
/**
* Client for `apps/api` `/v1/resellers` (see apps/api/src/resellers/resellers.controller.ts,
* resellers.service.ts and packages/database/src/repositories/resellers.ts for the source of
* truth — shapes below are mirrored, not invented).
* resellers.service.ts and packages/database/src/repositories/resellers.ts +
* reseller-partners.ts for the source of truth — shapes below are mirrored, not invented).
*/
import { apiRequest } from "./http.js";
@@ -15,6 +15,30 @@ export interface Reseller {
document: string | null;
contact_email: string | null;
active: boolean;
trade_name: string | null;
state_registration: string | null;
municipal_registration: string | null;
opening_date: string | null;
legal_nature: string | null;
cnpj_situation: string | null;
main_activity: string | null;
address_zip: string | null;
address_street: string | null;
address_number: string | null;
address_complement: string | null;
address_neighborhood: string | null;
address_city: string | null;
address_state: string | null;
address_country: string;
contact_principal_name: string | null;
contact_principal_email: string | null;
contact_principal_phone: string | null;
contact_financial_name: string | null;
contact_financial_email: string | null;
contact_financial_phone: string | null;
contact_technical_name: string | null;
contact_technical_email: string | null;
contact_technical_phone: string | null;
}
/** Request body — camelCase, matches `CreateResellerDto` exactly. */
@@ -23,6 +47,33 @@ export interface CreateResellerInput {
document?: string;
contactEmail?: string;
active?: boolean;
tradeName?: string;
stateRegistration?: string;
municipalRegistration?: string;
openingDate?: string;
legalNature?: string;
cnpjSituation?: string;
mainActivity?: string;
addressZip?: string;
addressStreet?: string;
addressNumber?: string;
addressComplement?: string;
addressNeighborhood?: string;
addressCity?: string;
addressState?: string;
addressCountry?: string;
contactPrincipalName?: string;
contactPrincipalEmail?: string;
contactPrincipalPhone?: string;
contactFinancialName?: string;
contactFinancialEmail?: string;
contactFinancialPhone?: string;
contactTechnicalName?: string;
contactTechnicalEmail?: string;
contactTechnicalPhone?: string;
}
/** Matches `UpdateResellerDto`: everything optional. */
@@ -76,3 +127,88 @@ export function unassignUserFromReseller(
{ accessToken },
);
}
/** `reseller_partners` row — see packages/database/src/repositories/reseller-partners.ts. */
export interface ResellerPartner {
id: string;
reseller_id: string;
name: string;
document: string | null;
qualification: string | null;
source: string;
cpf: string | null;
will_sign: boolean;
signature_email: string | null;
created_at: string;
}
/** Body of `POST /v1/resellers/:id/partners` — matches `CreateResellerPartnerDto`. */
export interface CreateResellerPartnerInput {
name: string;
document?: string;
qualification?: string;
cpf?: string;
willSign?: boolean;
signatureEmail?: string;
}
/** Body of `PATCH /v1/resellers/:id/partners/:partnerId` — matches `UpdateResellerPartnerDto`. */
export interface UpdateResellerPartnerInput {
cpf?: string;
willSign?: boolean;
signatureEmail?: string;
}
export function listResellerPartners(id: string, accessToken: string): Promise<ResellerPartner[]> {
return apiRequest<ResellerPartner[]>(`/resellers/${id}/partners`, { method: "GET" }, { accessToken });
}
export function createResellerPartner(
id: string,
input: CreateResellerPartnerInput,
accessToken: string,
): Promise<ResellerPartner> {
return apiRequest<ResellerPartner>(
`/resellers/${id}/partners`,
{ method: "POST", body: JSON.stringify(input) },
{ accessToken },
);
}
export function updateResellerPartner(
id: string,
partnerId: string,
input: UpdateResellerPartnerInput,
accessToken: string,
): Promise<ResellerPartner> {
return apiRequest<ResellerPartner>(
`/resellers/${id}/partners/${partnerId}`,
{ method: "PATCH", body: JSON.stringify(input) },
{ accessToken },
);
}
/** Result of `POST /v1/resellers/:id/cnpj-lookup` on a match (201). */
export interface ResellerCnpjLookupResult {
reseller: Reseller;
partners: ResellerPartner[];
}
/**
* Looks up a CNPJ against the external source and applies the result to the
* reseller (company data, address, partners) server-side. 404 means the CNPJ
* wasn't found — caller should surface the API's message and leave the
* reseller untouched (caller catches `ApiError` and inspects
* `.status`/`.message`, same pattern as client-registrations).
*/
export function applyResellerCnpjLookup(
id: string,
cnpj: string,
accessToken: string,
): Promise<ResellerCnpjLookupResult> {
return apiRequest<ResellerCnpjLookupResult>(
`/resellers/${id}/cnpj-lookup`,
{ method: "POST", body: JSON.stringify({ cnpj }) },
{ accessToken },
);
}