feat(web): telas de negócio reais no EDEN Core (Empresas, Produtos, Revendas, CRM, Ofertas)
Prioriza a parte web a pedido do usuário. 2 subagentes eden-frontend em paralelo sobre o shell autenticado já pronto: - Empresas/Produtos/Revendas: CRUD completo (lista + criar/editar), layout modelado nas páginas reais do tema DreamsERP comprado (purchase/vendors.tsx, inventory/products.tsx) — máscara de moeda para as 5 faixas de preço + implantação. Novo componente Table<T> genérico em packages/ui. - CRM: Leads (lista, criar/editar, converter em oportunidade) e Oportunidades, modelados em crm/leads.tsx/dealsDetails.tsx. - Ofertas/Quotes: o módulo mais rico — criação com lista dinâmica de itens produto/quantidade, fidelidade reduzida opcional, desconto opcional, totais calculados pelo servidor; detalhe com aprovar/rejeitar e fechar negócio. Modelado em sales/salesQuotes*.tsx. Novo componente Select em packages/ui. Correção de instrução no meio da leva: usuário pediu para seguir de perto o layout real do tema comprado, não só os tokens visuais já extraídos — as páginas React relevantes do tema foram extraídas para .tmp/theme-reference/ (nunca versionado) em cada worktree e os agentes já em execução foram redirecionados via mensagem (não relançados). Bug real do próprio agente, corrigindo meu brief: Leads/Opportunities respondem em camelCase (mapLead/mapOpportunity), não snake_case como o resto da API — verificado contra o código-fonte real antes de aceitar. Integração manual: um dos 2 agentes não conseguiu git merge no próprio sandbox e restaurou o shell existente por leitura+escrita — cada arquivo restaurado foi comparado byte a byte contra main (diff -rq) antes de aceitar. App.tsx/AuthenticatedLayout.tsx/packages/ui/index.ts reconciliados manualmente juntando as rotas/exports dos dois agentes. Verificado: pnpm turbo run typecheck build (28/28); containers eden-api/eden-core reconstruídos; bundle de produção contém todas as novas telas; login simulando Origin de browser real; listagens retornando dados reais; criação de lead confirmando o formato camelCase. Claude-Session: https://claude.ai/code/session_01BcWWuKfykGtUweXmHD6cKx Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
86
apps/core-web/src/lib/api/companies.ts
Normal file
86
apps/core-web/src/lib/api/companies.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Client for `apps/api` `/v1/companies` (see apps/api/src/companies/companies.controller.ts,
|
||||
* companies.service.ts and packages/database/src/repositories/companies.ts for the source of
|
||||
* truth — shapes below are mirrored, not invented).
|
||||
*/
|
||||
import { apiRequest } from "./http.js";
|
||||
|
||||
/** Response shape — snake_case, exactly as returned by the `companies` table. */
|
||||
export interface Company {
|
||||
id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by: string | null;
|
||||
company_name: string;
|
||||
trade_name: string | null;
|
||||
cnpj: string | null;
|
||||
state_registration: string | null;
|
||||
municipal_registration: 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;
|
||||
phone: string | null;
|
||||
email: string | null;
|
||||
website: string | null;
|
||||
legal_rep_name: string | null;
|
||||
legal_rep_cpf: string | null;
|
||||
legal_rep_role: string | null;
|
||||
legal_rep_email: string | null;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
/** Request body — camelCase, matches `CreateCompanyDto` exactly. */
|
||||
export interface CreateCompanyInput {
|
||||
companyName: string;
|
||||
tradeName?: string;
|
||||
cnpj?: string;
|
||||
stateRegistration?: string;
|
||||
municipalRegistration?: string;
|
||||
addressZip?: string;
|
||||
addressStreet?: string;
|
||||
addressNumber?: string;
|
||||
addressComplement?: string;
|
||||
addressNeighborhood?: string;
|
||||
addressCity?: string;
|
||||
addressState?: string;
|
||||
addressCountry?: string;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
website?: string;
|
||||
legalRepName?: string;
|
||||
legalRepCpf?: string;
|
||||
legalRepRole?: string;
|
||||
legalRepEmail?: string;
|
||||
}
|
||||
|
||||
/** Matches `UpdateCompanyDto`: everything optional plus `isActive`. */
|
||||
export type UpdateCompanyInput = Partial<CreateCompanyInput> & { isActive?: boolean };
|
||||
|
||||
export function listCompanies(accessToken: string): Promise<Company[]> {
|
||||
return apiRequest<Company[]>("/companies", { method: "GET" }, { accessToken });
|
||||
}
|
||||
|
||||
export function getCompany(id: string, accessToken: string): Promise<Company> {
|
||||
return apiRequest<Company>(`/companies/${id}`, { method: "GET" }, { accessToken });
|
||||
}
|
||||
|
||||
export function createCompany(input: CreateCompanyInput, accessToken: string): Promise<Company> {
|
||||
return apiRequest<Company>("/companies", { method: "POST", body: JSON.stringify(input) }, { accessToken });
|
||||
}
|
||||
|
||||
export function updateCompany(
|
||||
id: string,
|
||||
input: UpdateCompanyInput,
|
||||
accessToken: string,
|
||||
): Promise<Company> {
|
||||
return apiRequest<Company>(
|
||||
`/companies/${id}`,
|
||||
{ method: "PATCH", body: JSON.stringify(input) },
|
||||
{ accessToken },
|
||||
);
|
||||
}
|
||||
83
apps/core-web/src/lib/api/leads.ts
Normal file
83
apps/core-web/src/lib/api/leads.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Client for `apps/api` `/v1/leads` (see apps/api/src/leads/leads.controller.ts,
|
||||
* leads.service.ts, and packages/database/src/repositories/leads.ts for the
|
||||
* source of truth).
|
||||
*
|
||||
* NOTE: unlike Quotes/Resellers/Products (which pass DB rows through as-is,
|
||||
* snake_case), Leads/Opportunities go through a repository-level mapper
|
||||
* (`mapLead`/`mapOpportunity`) that returns camelCase fields — verified by
|
||||
* reading packages/database/src/repositories/leads.ts and opportunities.ts.
|
||||
*/
|
||||
import { apiRequest } from "./http.js";
|
||||
import type { Opportunity } from "./opportunities.js";
|
||||
|
||||
export const LEAD_STATUSES = ["new", "contacted", "qualified", "converted", "discarded"] as const;
|
||||
export type LeadStatus = (typeof LEAD_STATUSES)[number];
|
||||
|
||||
export interface Lead {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdBy: string | null;
|
||||
source: string | null;
|
||||
contactName: string;
|
||||
contactEmail: string | null;
|
||||
contactPhone: string | null;
|
||||
companyName: string | null;
|
||||
status: string;
|
||||
ownerId: string | null;
|
||||
notes: string | null;
|
||||
}
|
||||
|
||||
export interface CreateLeadInput {
|
||||
source?: string;
|
||||
contactName: string;
|
||||
contactEmail?: string;
|
||||
contactPhone?: string;
|
||||
companyName?: string;
|
||||
status?: LeadStatus;
|
||||
ownerId?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export type UpdateLeadInput = Partial<CreateLeadInput>;
|
||||
|
||||
export interface ConvertLeadInput {
|
||||
title: string;
|
||||
stage?: string;
|
||||
probability?: number;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface ConvertLeadResult {
|
||||
lead: Lead;
|
||||
opportunity: Opportunity;
|
||||
}
|
||||
|
||||
export function listLeads(accessToken: string): Promise<Lead[]> {
|
||||
return apiRequest<Lead[]>("/leads", { method: "GET" }, { accessToken });
|
||||
}
|
||||
|
||||
export function getLead(id: string, accessToken: string): Promise<Lead> {
|
||||
return apiRequest<Lead>(`/leads/${id}`, { method: "GET" }, { accessToken });
|
||||
}
|
||||
|
||||
export function createLead(input: CreateLeadInput, accessToken: string): Promise<Lead> {
|
||||
return apiRequest<Lead>("/leads", { method: "POST", body: JSON.stringify(input) }, { accessToken });
|
||||
}
|
||||
|
||||
export function updateLead(id: string, input: UpdateLeadInput, accessToken: string): Promise<Lead> {
|
||||
return apiRequest<Lead>(`/leads/${id}`, { method: "PATCH", body: JSON.stringify(input) }, { accessToken });
|
||||
}
|
||||
|
||||
export function convertLead(
|
||||
id: string,
|
||||
input: ConvertLeadInput,
|
||||
accessToken: string,
|
||||
): Promise<ConvertLeadResult> {
|
||||
return apiRequest<ConvertLeadResult>(
|
||||
`/leads/${id}/convert`,
|
||||
{ method: "POST", body: JSON.stringify(input) },
|
||||
{ accessToken },
|
||||
);
|
||||
}
|
||||
75
apps/core-web/src/lib/api/opportunities.ts
Normal file
75
apps/core-web/src/lib/api/opportunities.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Client for `apps/api` `/v1/opportunities` (see
|
||||
* apps/api/src/opportunities/opportunities.controller.ts and
|
||||
* packages/database/src/repositories/opportunities.ts for the source of
|
||||
* truth). Response fields are camelCase (repository-level `mapOpportunity`
|
||||
* mapper) — same note as lib/api/leads.ts.
|
||||
*/
|
||||
import { apiRequest } from "./http.js";
|
||||
|
||||
export const OPPORTUNITY_STAGES = [
|
||||
"prospecting",
|
||||
"qualification",
|
||||
"proposal",
|
||||
"negotiation",
|
||||
"won",
|
||||
"lost",
|
||||
] as const;
|
||||
export type OpportunityStage = (typeof OPPORTUNITY_STAGES)[number];
|
||||
|
||||
export interface Opportunity {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdBy: string | null;
|
||||
leadId: string | null;
|
||||
title: string;
|
||||
stage: string;
|
||||
probability: number | null;
|
||||
ownerId: string | null;
|
||||
lossReason: string | null;
|
||||
notes: string | null;
|
||||
}
|
||||
|
||||
export interface CreateOpportunityInput {
|
||||
leadId?: string;
|
||||
title: string;
|
||||
stage?: OpportunityStage;
|
||||
probability?: number;
|
||||
ownerId?: string;
|
||||
lossReason?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export type UpdateOpportunityInput = Partial<CreateOpportunityInput>;
|
||||
|
||||
export function listOpportunities(accessToken: string): Promise<Opportunity[]> {
|
||||
return apiRequest<Opportunity[]>("/opportunities", { method: "GET" }, { accessToken });
|
||||
}
|
||||
|
||||
export function getOpportunity(id: string, accessToken: string): Promise<Opportunity> {
|
||||
return apiRequest<Opportunity>(`/opportunities/${id}`, { method: "GET" }, { accessToken });
|
||||
}
|
||||
|
||||
export function createOpportunity(
|
||||
input: CreateOpportunityInput,
|
||||
accessToken: string,
|
||||
): Promise<Opportunity> {
|
||||
return apiRequest<Opportunity>(
|
||||
"/opportunities",
|
||||
{ method: "POST", body: JSON.stringify(input) },
|
||||
{ accessToken },
|
||||
);
|
||||
}
|
||||
|
||||
export function updateOpportunity(
|
||||
id: string,
|
||||
input: UpdateOpportunityInput,
|
||||
accessToken: string,
|
||||
): Promise<Opportunity> {
|
||||
return apiRequest<Opportunity>(
|
||||
`/opportunities/${id}`,
|
||||
{ method: "PATCH", body: JSON.stringify(input) },
|
||||
{ accessToken },
|
||||
);
|
||||
}
|
||||
89
apps/core-web/src/lib/api/products.ts
Normal file
89
apps/core-web/src/lib/api/products.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Client for `apps/api` `/v1/products` (see apps/api/src/products/products.controller.ts,
|
||||
* products.service.ts and packages/database/src/repositories/products.ts for the source of
|
||||
* truth — shapes below are mirrored, not invented).
|
||||
*/
|
||||
import { apiRequest } from "./http.js";
|
||||
|
||||
/**
|
||||
* Response shape — snake_case, exactly as returned by the `products` table.
|
||||
* Money fields (`price_*`, `impl_unit_price`) come back as `string` (Postgres
|
||||
* NUMERIC serialized as text by the pg driver) — never coerce to `number` for
|
||||
* storage, only for display formatting.
|
||||
*/
|
||||
export interface Product {
|
||||
id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by: string | null;
|
||||
product_code: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
product_type: string;
|
||||
product_group: string | null;
|
||||
product_subgroup: string | null;
|
||||
brand: string | null;
|
||||
unit: string;
|
||||
active: boolean;
|
||||
discontinued: boolean;
|
||||
tracks_stock: boolean;
|
||||
tracks_serial: boolean;
|
||||
tracks_asset_tag: boolean;
|
||||
tracks_mac: boolean;
|
||||
price_0: string | null;
|
||||
price_12: string | null;
|
||||
price_24: string | null;
|
||||
price_36: string | null;
|
||||
price_48: string | null;
|
||||
impl_unit_price: string;
|
||||
}
|
||||
|
||||
/** Request body — camelCase, matches `CreateProductDto` exactly. */
|
||||
export interface CreateProductInput {
|
||||
name: string;
|
||||
description?: string;
|
||||
productType: string;
|
||||
productGroup?: string;
|
||||
productSubgroup?: string;
|
||||
brand?: string;
|
||||
unit?: string;
|
||||
active?: boolean;
|
||||
discontinued?: boolean;
|
||||
tracksStock?: boolean;
|
||||
tracksSerial?: boolean;
|
||||
tracksAssetTag?: boolean;
|
||||
tracksMac?: boolean;
|
||||
price0?: number;
|
||||
price12?: number;
|
||||
price24?: number;
|
||||
price36?: number;
|
||||
price48?: number;
|
||||
implUnitPrice?: number;
|
||||
}
|
||||
|
||||
/** Matches `UpdateProductDto`: everything optional. */
|
||||
export type UpdateProductInput = Partial<CreateProductInput>;
|
||||
|
||||
export function listProducts(accessToken: string): Promise<Product[]> {
|
||||
return apiRequest<Product[]>("/products", { method: "GET" }, { accessToken });
|
||||
}
|
||||
|
||||
export function getProduct(id: string, accessToken: string): Promise<Product> {
|
||||
return apiRequest<Product>(`/products/${id}`, { method: "GET" }, { accessToken });
|
||||
}
|
||||
|
||||
export function createProduct(input: CreateProductInput, accessToken: string): Promise<Product> {
|
||||
return apiRequest<Product>("/products", { method: "POST", body: JSON.stringify(input) }, { accessToken });
|
||||
}
|
||||
|
||||
export function updateProduct(
|
||||
id: string,
|
||||
input: UpdateProductInput,
|
||||
accessToken: string,
|
||||
): Promise<Product> {
|
||||
return apiRequest<Product>(
|
||||
`/products/${id}`,
|
||||
{ method: "PATCH", body: JSON.stringify(input) },
|
||||
{ accessToken },
|
||||
);
|
||||
}
|
||||
124
apps/core-web/src/lib/api/quotes.ts
Normal file
124
apps/core-web/src/lib/api/quotes.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Client for `apps/api` `/v1/quotes` (see apps/api/src/quotes/quotes.controller.ts,
|
||||
* quotes.service.ts, dto/*, and packages/database/src/repositories/quotes.ts
|
||||
* for the source of truth).
|
||||
*
|
||||
* Unlike Leads/Opportunities, Quotes pass the DB row straight through with
|
||||
* no repository-level mapper — the top-level response is snake_case. The
|
||||
* one exception is `items`, a JSONB column whose element shape
|
||||
* (`QuoteItem`) IS camelCase — a real inconsistency in the API, not a typo
|
||||
* here. Money fields (`implementation_fee`, `impl_geral`, `monthly_total`,
|
||||
* `contract_total`, `proposed_monthly_total`) come back as NUMERIC-typed
|
||||
* strings from Postgres — always `Number(...)` before formatting/math.
|
||||
*/
|
||||
import { apiRequest } from "./http.js";
|
||||
|
||||
export const CONTRACT_PERIODS = [0, 12, 24, 36, 48] as const;
|
||||
export type ContractPeriod = (typeof CONTRACT_PERIODS)[number];
|
||||
|
||||
export const IMPL_PAYMENT_CONDITIONS = ["À vista", "1+1", "1+2"] as const;
|
||||
export type ImplPaymentCondition = (typeof IMPL_PAYMENT_CONDITIONS)[number];
|
||||
|
||||
export type ApprovalStatus = "none" | "pending" | "approved" | "rejected";
|
||||
export type DealStatus = "orcamento" | "fechado" | "perdido";
|
||||
|
||||
export interface QuoteItem {
|
||||
productId: string;
|
||||
productName: string;
|
||||
quantity: number;
|
||||
unitPrice: number;
|
||||
total: number;
|
||||
implUnitPrice: number;
|
||||
implTotal: number;
|
||||
price0: number | null;
|
||||
price12: number | null;
|
||||
price24: number | null;
|
||||
price36: number | null;
|
||||
price48: number | null;
|
||||
}
|
||||
|
||||
export interface Quote {
|
||||
id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by: string | null;
|
||||
quote_number: string;
|
||||
reseller_id: string;
|
||||
client_name: string;
|
||||
client_company: string | null;
|
||||
client_email: string | null;
|
||||
client_phone: string;
|
||||
contract_period: number;
|
||||
fidelity_period: number | null;
|
||||
items: QuoteItem[];
|
||||
implementation_fee: string;
|
||||
impl_geral: string;
|
||||
monthly_total: string;
|
||||
contract_total: string;
|
||||
proposed_monthly_total: string | null;
|
||||
approval_status: ApprovalStatus;
|
||||
approval_notes: string | null;
|
||||
approved_by_id: string | null;
|
||||
notes: string | null;
|
||||
deal_status: DealStatus;
|
||||
impl_payment_condition: ImplPaymentCondition;
|
||||
client_registration_id: string | null;
|
||||
}
|
||||
|
||||
export interface QuoteItemInput {
|
||||
productId: string;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export interface CreateQuoteInput {
|
||||
resellerId: string;
|
||||
clientName: string;
|
||||
clientCompany?: string;
|
||||
clientEmail?: string;
|
||||
clientPhone: string;
|
||||
contractPeriod: ContractPeriod;
|
||||
fidelityPeriod?: number;
|
||||
items: QuoteItemInput[];
|
||||
implGeral?: number;
|
||||
proposedMonthlyTotal?: number;
|
||||
implPaymentCondition?: ImplPaymentCondition;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface DecideApprovalInput {
|
||||
decision: "approved" | "rejected";
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface CloseDealInput {
|
||||
clientRegistrationId: string;
|
||||
}
|
||||
|
||||
export function listQuotes(accessToken: string, dealStatus?: string): Promise<Quote[]> {
|
||||
const query = dealStatus ? `?dealStatus=${encodeURIComponent(dealStatus)}` : "";
|
||||
return apiRequest<Quote[]>(`/quotes${query}`, { method: "GET" }, { accessToken });
|
||||
}
|
||||
|
||||
export function getQuote(id: string, accessToken: string): Promise<Quote> {
|
||||
return apiRequest<Quote>(`/quotes/${id}`, { method: "GET" }, { accessToken });
|
||||
}
|
||||
|
||||
export function createQuote(input: CreateQuoteInput, accessToken: string): Promise<Quote> {
|
||||
return apiRequest<Quote>("/quotes", { method: "POST", body: JSON.stringify(input) }, { accessToken });
|
||||
}
|
||||
|
||||
export function decideQuoteApproval(
|
||||
id: string,
|
||||
input: DecideApprovalInput,
|
||||
accessToken: string,
|
||||
): Promise<Quote> {
|
||||
return apiRequest<Quote>(
|
||||
`/quotes/${id}/approval`,
|
||||
{ method: "PATCH", body: JSON.stringify(input) },
|
||||
{ accessToken },
|
||||
);
|
||||
}
|
||||
|
||||
export function closeQuoteDeal(id: string, input: CloseDealInput, accessToken: string): Promise<Quote> {
|
||||
return apiRequest<Quote>(`/quotes/${id}/close`, { method: "POST", body: JSON.stringify(input) }, { accessToken });
|
||||
}
|
||||
78
apps/core-web/src/lib/api/resellers.ts
Normal file
78
apps/core-web/src/lib/api/resellers.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
import { apiRequest } from "./http.js";
|
||||
|
||||
/** Response shape — snake_case, exactly as returned by the `resellers` table. */
|
||||
export interface Reseller {
|
||||
id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by: string | null;
|
||||
name: string;
|
||||
document: string | null;
|
||||
contact_email: string | null;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
/** Request body — camelCase, matches `CreateResellerDto` exactly. */
|
||||
export interface CreateResellerInput {
|
||||
name: string;
|
||||
document?: string;
|
||||
contactEmail?: string;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
/** Matches `UpdateResellerDto`: everything optional. */
|
||||
export type UpdateResellerInput = Partial<CreateResellerInput>;
|
||||
|
||||
export function listResellers(accessToken: string): Promise<Reseller[]> {
|
||||
return apiRequest<Reseller[]>("/resellers", { method: "GET" }, { accessToken });
|
||||
}
|
||||
|
||||
export function getReseller(id: string, accessToken: string): Promise<Reseller> {
|
||||
return apiRequest<Reseller>(`/resellers/${id}`, { method: "GET" }, { accessToken });
|
||||
}
|
||||
|
||||
export function createReseller(input: CreateResellerInput, accessToken: string): Promise<Reseller> {
|
||||
return apiRequest<Reseller>("/resellers", { method: "POST", body: JSON.stringify(input) }, { accessToken });
|
||||
}
|
||||
|
||||
export function updateReseller(
|
||||
id: string,
|
||||
input: UpdateResellerInput,
|
||||
accessToken: string,
|
||||
): Promise<Reseller> {
|
||||
return apiRequest<Reseller>(
|
||||
`/resellers/${id}`,
|
||||
{ method: "PATCH", body: JSON.stringify(input) },
|
||||
{ accessToken },
|
||||
);
|
||||
}
|
||||
|
||||
/** `PATCH /v1/resellers/:id/assign-user` — `:id` in the URL is always the reseller, never the body. */
|
||||
export function assignUserToReseller(
|
||||
resellerId: string,
|
||||
userId: string,
|
||||
accessToken: string,
|
||||
): Promise<{ userId: string; resellerId: string }> {
|
||||
return apiRequest<{ userId: string; resellerId: string }>(
|
||||
`/resellers/${resellerId}/assign-user`,
|
||||
{ method: "PATCH", body: JSON.stringify({ userId }) },
|
||||
{ accessToken },
|
||||
);
|
||||
}
|
||||
|
||||
/** `POST /v1/resellers/unassign-user` — body `{ userId }`. */
|
||||
export function unassignUserFromReseller(
|
||||
userId: string,
|
||||
accessToken: string,
|
||||
): Promise<{ userId: string; resellerId: null }> {
|
||||
return apiRequest<{ userId: string; resellerId: null }>(
|
||||
"/resellers/unassign-user",
|
||||
{ method: "POST", body: JSON.stringify({ userId }) },
|
||||
{ accessToken },
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user