atualizacoes modulo fechamento

This commit is contained in:
Vitex Tecnologia
2026-04-26 22:54:56 -03:00
parent 7a01c6f97b
commit 6da067c641
29 changed files with 5973 additions and 3 deletions
@@ -0,0 +1,62 @@
import axios from "axios";
import { buildCommanderHeaders, resolveCommanderBaseUrl } from "./commanderHttp";
export type AsanaWorkspaceUser = {
id: string;
name: string;
};
type ApiErrorShape = {
error?: {
message?: string;
};
};
type AsanaWorkspaceUsersResponse = {
data: Array<{
gid: string;
name?: string;
email?: string;
}>;
};
class FechamentoAsanaWorkspaceUsersService {
private handleAxiosError(error: unknown, fallback: string): never {
if (axios.isAxiosError(error)) {
if (!error.response) {
throw new Error(
"Não foi possível conectar na API do Commander. Verifique VITE_API_BASE_URL_COMMANDER e se o backend está acessível.",
);
}
const message = ((error.response.data as ApiErrorShape | undefined)?.error?.message ??
error.message ??
fallback) as string;
throw new Error(message);
}
throw new Error(fallback);
}
async listarUsuariosDoWorkspace(workspaceId: string): Promise<AsanaWorkspaceUser[]> {
if (!workspaceId.trim()) {
return [];
}
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.get<AsanaWorkspaceUsersResponse>(
`${baseUrl}/asana/workspaces/${encodeURIComponent(workspaceId)}/users`,
{ headers },
);
return (response.data.data ?? []).map((item) => ({
id: item.gid,
name: item.name?.trim() || item.email?.trim() || item.gid,
}));
} catch (error) {
this.handleAxiosError(error, "Erro ao listar usuários do workspace do Asana.");
}
}
}
export const fechamentoAsanaWorkspaceUsersService = new FechamentoAsanaWorkspaceUsersService();
+123
View File
@@ -0,0 +1,123 @@
import axios from "axios";
import { buildCommanderHeaders, resolveCommanderBaseUrl } from "./commanderHttp";
export type PapelUsuario = "admin" | "parceiro";
export type MeData = {
id: string;
nome: string;
email: string;
papel: PapelUsuario;
parceiroId: string | null;
estaAtivo: boolean;
unidadeId: string;
};
export type BootstrapStatusData = {
requiresBootstrap: boolean;
estabelecimentoId: string;
unidade: { id: string; nome: string; estabelecimentoId: string } | null;
};
export type BootstrapInitInput = {
estabelecimentoId: string;
unidadeNome: string;
adminNome: string;
adminEmail: string;
};
type MeResponse = {
success: boolean;
data: MeData;
};
type BootstrapStatusResponse = {
success: boolean;
data: BootstrapStatusData;
};
type BootstrapInitResponse = {
success: boolean;
data: {
unidadeId: string;
adminId: string;
};
};
type ApiErrorShape = {
status?: number;
data?: {
error?: {
code?: string;
message?: string;
};
};
message?: string;
};
function isApiError(value: unknown): value is ApiErrorShape {
return typeof value === "object" && value !== null;
}
class AuthMeService {
async getMe(usuarioEmail: string): Promise<MeData | null> {
const baseUrl = resolveCommanderBaseUrl();
const headers = await buildCommanderHeaders({ omitUnidadeId: true });
try {
const response = await axios.get<MeResponse>(`${baseUrl}/me`, {
params: { usuarioEmail },
headers,
});
return response.data.data;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
const status = error.response?.status;
const code = (error.response?.data as ApiErrorShape["data"] | undefined)?.error?.code;
if (status === 404 || code === "USUARIO_NAO_CADASTRADO") {
return null;
}
if (!error.response) {
throw new Error(
"Não foi possível conectar na API do Commander. Verifique VITE_API_BASE_URL_COMMANDER e se o backend está acessível.",
);
}
const message =
(error.response?.data as ApiErrorShape["data"] | undefined)?.error?.message ?? error.message;
throw new Error(message || "Erro ao consultar acesso do usuário.");
}
if (isApiError(error)) {
const status = error.status;
const code = error.data?.error?.code;
if (status === 404 || code === "USUARIO_NAO_CADASTRADO") {
return null;
}
const message = error.data?.error?.message ?? error.message;
throw new Error(message || "Erro ao consultar acesso do usuário.");
}
throw new Error("Erro ao consultar acesso do usuário.");
}
}
async getBootstrapStatus(estabelecimentoId: string): Promise<BootstrapStatusData> {
const baseUrl = resolveCommanderBaseUrl();
const headers = await buildCommanderHeaders({ omitUnidadeId: true });
const response = await axios.get<BootstrapStatusResponse>(`${baseUrl}/bootstrap-status`, {
headers,
params: { estabelecimentoId },
});
return response.data.data;
}
async bootstrapInitialize(input: BootstrapInitInput): Promise<{ unidadeId: string; adminId: string }> {
const baseUrl = resolveCommanderBaseUrl();
const headers = await buildCommanderHeaders({ omitUnidadeId: true });
const response = await axios.post<BootstrapInitResponse>(`${baseUrl}/bootstrap`, input, {
headers,
});
return response.data.data;
}
}
export const authMeService = new AuthMeService();
+27
View File
@@ -0,0 +1,27 @@
import { GlobalFunctions } from "@/GlobalFunctions";
import { resolveCommanderUnidadeId } from "./unidadeContext";
import { resolveCommanderBaseUrl } from "./fechamentoBaseUrl";
export { resolveCommanderBaseUrl } from "./fechamentoBaseUrl";
export type BuildCommanderHeadersOptions = {
/** Rotas allowlisted no Commander (ex.: `/me`, lookup de `/unidades`) não devem enviar `X-Unidade-Id`. */
omitUnidadeId?: boolean;
};
export async function buildCommanderHeaders(
opts?: BuildCommanderHeadersOptions,
): Promise<Record<string, string>> {
const token = await GlobalFunctions.getToken();
const apiKey = import.meta.env.VITE_API_KEY || "";
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(apiKey ? { apikey: apiKey } : {}),
...(token ? { Authorization: `Bearer ${token}` } : {}),
};
if (!opts?.omitUnidadeId) {
headers["X-Unidade-Id"] = await resolveCommanderUnidadeId();
}
return headers;
}
+159
View File
@@ -0,0 +1,159 @@
import axios from "axios";
import { buildCommanderHeaders, resolveCommanderBaseUrl } from "./commanderHttp";
export type CompetenciaStatus = "em_aberto" | "concluido";
export type CompetenciaItem = {
id: string;
mes: number;
ano: number;
status: CompetenciaStatus;
quantidadeFechamentos: number;
criadoEm: string;
atualizadoEm: string;
};
export type FechamentoDaCompetenciaItem = {
id: string;
competenciaId: string;
parceiroId: string;
parceiroNome: string;
parceiroCodinome: string | null;
parceiroLogoUrl: string | null;
horasTotal: number;
status: "em_aberto" | "fechado";
pontuacaoTotalEntregue: number;
pontuacaoMeta: number;
pontuacaoPaga: number;
pontuacaoBanco: number;
exportadoFinanceiro: boolean;
fechadoEm: string | null;
criadoEm: string;
atualizadoEm: string;
};
type ListarCompetenciasResponse = {
data: CompetenciaItem[];
};
type ListarFechamentosDaCompetenciaResponse = {
data: FechamentoDaCompetenciaItem[];
};
type CriarCompetenciaResponse = {
data: CompetenciaItem;
};
type ImportarCompetenciaResponse = {
data: {
competenciaId: string;
tarefasRecebidas: number;
tarefasImportadas: number;
fechamentosCriados: number;
tarefasIgnoradasSemParceiro: number;
chamadasSubtarefasAsana: number;
};
};
export type ImportacaoAsanaModo =
| "reprocessar_tudo"
| "reprocessar_alguns"
| "buscar_novos_fechamentos";
type ApiErrorShape = {
error?: {
message?: string;
};
};
class FechamentoCompetenciasService {
private handleAxiosError(error: unknown, fallback: string): never {
if (axios.isAxiosError(error)) {
if (!error.response) {
throw new Error(
"Não foi possível conectar na API do Commander. Verifique VITE_API_BASE_URL_COMMANDER e se o backend está acessível.",
);
}
const message = ((error.response.data as ApiErrorShape | undefined)?.error?.message ??
error.message ??
fallback) as string;
throw new Error(message);
}
throw new Error(fallback);
}
async listarCompetencias(params: { ano?: number; status?: CompetenciaStatus }): Promise<CompetenciaItem[]> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.get<ListarCompetenciasResponse>(`${baseUrl}/competencias`, {
headers,
params: {
ano: params.ano,
status: params.status,
},
});
return response.data.data ?? [];
} catch (error) {
this.handleAxiosError(error, "Erro ao listar competências.");
}
}
async listarFechamentosDaCompetencia(competenciaId: string): Promise<FechamentoDaCompetenciaItem[]> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.get<ListarFechamentosDaCompetenciaResponse>(
`${baseUrl}/competencias/${competenciaId}/fechamentos`,
{ headers },
);
return response.data.data ?? [];
} catch (error) {
this.handleAxiosError(error, "Erro ao listar fechamentos da competência.");
}
}
async criarCompetencia(input: { mes: number; ano: number }): Promise<CompetenciaItem> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.post<CriarCompetenciaResponse>(
`${baseUrl}/competencias`,
{ mes: input.mes, ano: input.ano },
{ headers },
);
return response.data.data;
} catch (error) {
this.handleAxiosError(error, "Erro ao criar competência.");
}
}
async importarDoAsana(
competenciaId: string,
opcoes?: { modo?: ImportacaoAsanaModo; parceiroIds?: string[] },
): Promise<{ tarefasRecebidas: number; tarefasImportadas: number; fechamentosCriados: number; tarefasIgnoradasSemParceiro: number }> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.post<ImportarCompetenciaResponse>(
`${baseUrl}/competencias/${competenciaId}/importar`,
{
modo: opcoes?.modo ?? "reprocessar_tudo",
parceiroIds: opcoes?.parceiroIds,
},
{ headers },
);
return {
tarefasRecebidas: response.data.data.tarefasRecebidas,
tarefasImportadas: response.data.data.tarefasImportadas,
fechamentosCriados: response.data.data.fechamentosCriados,
tarefasIgnoradasSemParceiro: response.data.data.tarefasIgnoradasSemParceiro,
};
} catch (error) {
this.handleAxiosError(error, "Erro ao importar tasks do Asana.");
}
}
}
export const fechamentoCompetenciasService = new FechamentoCompetenciasService();
+83
View File
@@ -0,0 +1,83 @@
import axios from "axios";
import { buildCommanderHeaders, resolveCommanderBaseUrl } from "./commanderHttp";
export type ConfiguracaoPublica = {
id: string;
asanaWorkspaceId: string | null;
asanaWorkspaceNome: string | null;
asanaToken: string | null;
asanaTokenConfigured: boolean;
atualizadoEm: string;
atualizadoPorId: string | null;
};
type GetConfiguracoesResponse = {
data: ConfiguracaoPublica | null;
};
type SalvarConfiguracoesBody = {
asanaToken?: string;
asanaWorkspaceId?: string | null;
asanaWorkspaceNome?: string | null;
};
type SalvarConfiguracoesResponse = {
data: ConfiguracaoPublica;
};
class FechamentoConfiguracoesService {
async getConfiguracoes(): Promise<ConfiguracaoPublica | null> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.get<GetConfiguracoesResponse>(`${baseUrl}/configuracoes`, {
headers,
});
return response.data.data;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
if (!error.response) {
throw new Error(
"Não foi possível conectar na API do Commander. Verifique VITE_API_BASE_URL_COMMANDER e se o backend está acessível.",
);
}
const message =
((error.response.data as { error?: { message?: string } } | undefined)?.error?.message ??
error.message) ||
"Erro ao carregar configurações.";
throw new Error(message);
}
throw new Error("Erro ao carregar configurações.");
}
}
async salvarConfiguracoes(body: SalvarConfiguracoesBody): Promise<ConfiguracaoPublica> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.post<SalvarConfiguracoesResponse>(
`${baseUrl}/configuracoes`,
body,
{ headers },
);
return response.data.data;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
if (!error.response) {
throw new Error(
"Não foi possível conectar na API do Commander. Verifique VITE_API_BASE_URL_COMMANDER e se o backend está acessível.",
);
}
const message =
((error.response.data as { error?: { message?: string } } | undefined)?.error?.message ??
error.message) ||
"Erro ao salvar configurações.";
throw new Error(message);
}
throw new Error("Erro ao salvar configurações.");
}
}
}
export const fechamentoConfiguracoesService = new FechamentoConfiguracoesService();
@@ -0,0 +1,9 @@
export function resolveCommanderBaseUrl(): string {
const raw = import.meta.env.VITE_API_BASE_URL_COMMANDER?.trim();
if (!raw) {
throw new Error(
"A variável VITE_API_BASE_URL_COMMANDER não está configurada. Defina, por exemplo, http://localhost:3333/api/",
);
}
return raw.endsWith("/") ? raw.slice(0, -1) : raw;
}
+288
View File
@@ -0,0 +1,288 @@
import axios from "axios";
import { buildCommanderHeaders, resolveCommanderBaseUrl } from "./commanderHttp";
export type FechamentoTarefaItem = {
id: string;
fechamentoId: string;
tipo: string;
numeroTicket: string | null;
descricao: string;
cliente: string | null;
linkAsana: string | null;
tempoMinutos: number | null;
pontuacao: number;
pontuacaoOriginal: number;
estaRevisada: boolean;
dataConclusao: string | null;
editadoEm: string | null;
editadoPorId: string | null;
criadoEm: string;
};
type ListarTarefasResponse = {
data: FechamentoTarefaItem[];
};
type PatchTarefaResponse = {
data: FechamentoTarefaItem;
};
type CriarLancamentoResponse = {
data: FechamentoTarefaItem;
};
export type FechamentoFechadoResponse = {
id: string;
competenciaId: string;
parceiroId: string;
status: "fechado";
versao: number;
pontuacaoTotalEntregue: number;
pontuacaoMeta: number;
pontuacaoPaga: number;
pontuacaoBanco: number;
exportadoFinanceiro: boolean;
fechadoPorId: string | null;
fechadoEm: string | null;
criadoEm: string;
atualizadoEm: string;
};
export type FechamentoReabertoResponse = {
id: string;
competenciaId: string;
parceiroId: string;
status: "em_aberto";
versao: number;
pontuacaoTotalEntregue: number;
pontuacaoMeta: number;
pontuacaoPaga: number;
pontuacaoBanco: number;
exportadoFinanceiro: boolean;
fechadoPorId: string | null;
fechadoEm: string | null;
criadoEm: string;
atualizadoEm: string;
};
type ConcluirFechamentoResponse = {
data: FechamentoFechadoResponse;
};
type ReabrirFechamentoResponse = {
data: FechamentoReabertoResponse;
};
type ReprocessarAsanaResponse = {
data: {
fechamentoId: string;
tarefasRecebidas: number;
tarefasImportadas: number;
tarefasIgnoradasSemParceiro: number;
chamadasSubtarefasAsana: number;
};
};
export type ExportarPlanilhaResponse = {
buffer: ArrayBuffer;
filename: string | null;
};
type ApiErrorShape = {
error?: {
message?: string;
};
};
class FechamentoFechamentosService {
private extractFilenameFromContentDisposition(value: string | undefined): string | null {
if (!value) return null;
const utf8Match = value.match(/filename\*=UTF-8''([^;]+)/i);
if (utf8Match?.[1]) {
try {
return decodeURIComponent(utf8Match[1]);
} catch {
return utf8Match[1];
}
}
const regularMatch = value.match(/filename="?([^";]+)"?/i);
return regularMatch?.[1] ?? null;
}
private handleAxiosError(error: unknown, fallback: string): never {
if (axios.isAxiosError(error)) {
if (!error.response) {
throw new Error(
"Não foi possível conectar na API do Commander. Verifique VITE_API_BASE_URL_COMMANDER e se o backend está acessível.",
);
}
const message = ((error.response.data as ApiErrorShape | undefined)?.error?.message ??
error.message ??
fallback) as string;
throw new Error(message);
}
throw new Error(fallback);
}
async listarTarefas(fechamentoId: string): Promise<FechamentoTarefaItem[]> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.get<ListarTarefasResponse>(`${baseUrl}/fechamentos/${fechamentoId}/tarefas`, {
headers,
});
return response.data.data ?? [];
} catch (error) {
this.handleAxiosError(error, "Erro ao listar tarefas do fechamento.");
}
}
async patchTarefa(
fechamentoId: string,
tarefaId: string,
input: {
estaRevisada?: boolean;
pontuacao?: number;
numeroTicket?: string | null;
descricao?: string;
cliente?: string | null;
tempoMinutos?: number | null;
editadoPorId: string;
},
): Promise<FechamentoTarefaItem> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.patch<PatchTarefaResponse>(
`${baseUrl}/fechamentos/${fechamentoId}/tarefas/${tarefaId}`,
{
...(input.estaRevisada !== undefined ? { esta_revisada: input.estaRevisada } : {}),
...(input.pontuacao !== undefined ? { pontuacao: input.pontuacao } : {}),
...(input.numeroTicket !== undefined ? { numero_ticket: input.numeroTicket } : {}),
...(input.descricao !== undefined ? { descricao: input.descricao } : {}),
...(input.cliente !== undefined ? { cliente: input.cliente } : {}),
...(input.tempoMinutos !== undefined ? { tempo_minutos: input.tempoMinutos } : {}),
editado_por_id: input.editadoPorId,
},
{ headers },
);
return response.data.data;
} catch (error) {
this.handleAxiosError(error, "Erro ao atualizar aprovação da tarefa.");
}
}
async criarLancamento(
fechamentoId: string,
input: { tipo: "bonus" | "desconto"; descricao: string; pontuacao: number },
): Promise<FechamentoTarefaItem> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.post<CriarLancamentoResponse>(
`${baseUrl}/fechamentos/${fechamentoId}/tarefas`,
{
tipo: input.tipo,
descricao: input.descricao,
pontuacao: input.pontuacao,
},
{ headers },
);
return response.data.data;
} catch (error) {
this.handleAxiosError(error, "Erro ao criar lançamento no fechamento.");
}
}
async excluirLancamento(fechamentoId: string, tarefaId: string): Promise<FechamentoTarefaItem> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.delete<PatchTarefaResponse>(
`${baseUrl}/fechamentos/${fechamentoId}/tarefas/${tarefaId}`,
{ headers },
);
return response.data.data;
} catch (error) {
this.handleAxiosError(error, "Erro ao excluir lançamento.");
}
}
async concluirFechamento(
fechamentoId: string,
input: { pontuacaoPaga: number; fechadoPorId?: string; motivoAjuste?: string },
): Promise<FechamentoFechadoResponse> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.post<ConcluirFechamentoResponse>(
`${baseUrl}/fechamentos/${fechamentoId}/concluir`,
{
pontuacao_paga: input.pontuacaoPaga,
...(input.fechadoPorId ? { fechado_por_id: input.fechadoPorId } : {}),
...(input.motivoAjuste?.trim() ? { motivo_ajuste: input.motivoAjuste.trim() } : {}),
},
{ headers },
);
return response.data.data;
} catch (error) {
this.handleAxiosError(error, "Erro ao concluir fechamento.");
}
}
async exportarPlanilha(fechamentoId: string): Promise<ExportarPlanilhaResponse> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.get<ArrayBuffer>(`${baseUrl}/fechamentos/${fechamentoId}/exportar`, {
headers,
responseType: "arraybuffer",
});
return {
buffer: response.data,
filename: this.extractFilenameFromContentDisposition(response.headers["content-disposition"]),
};
} catch (error) {
this.handleAxiosError(error, "Erro ao exportar planilha do fechamento.");
}
}
async reabrirFechamento(
fechamentoId: string,
input: { reabertoPorId: string; motivo?: string },
): Promise<FechamentoReabertoResponse> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.post<ReabrirFechamentoResponse>(
`${baseUrl}/fechamentos/${fechamentoId}/reabrir`,
{
reaberto_por_id: input.reabertoPorId,
...(input.motivo?.trim() ? { motivo: input.motivo.trim() } : {}),
},
{ headers },
);
return response.data.data;
} catch (error) {
this.handleAxiosError(error, "Erro ao reabrir fechamento.");
}
}
async reprocessarAsana(fechamentoId: string): Promise<ReprocessarAsanaResponse["data"]> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.post<ReprocessarAsanaResponse>(
`${baseUrl}/fechamentos/${fechamentoId}/reprocessar-asana`,
{},
{ headers },
);
return response.data.data;
} catch (error) {
this.handleAxiosError(error, "Erro ao reprocessar tarefas do Asana.");
}
}
}
export const fechamentoFechamentosService = new FechamentoFechamentosService();
+157
View File
@@ -0,0 +1,157 @@
import axios from "axios";
import { buildCommanderHeaders, resolveCommanderBaseUrl } from "./commanderHttp";
export type ParceiroTipoPessoa = "fisica" | "juridica";
export type ParceiroStatusFiltro = "all" | "true" | "false";
export type ParceiroItem = {
id: string;
nome: string;
codinome: string | null;
tipoPessoa: ParceiroTipoPessoa;
cpf: string | null;
cnpj: string | null;
email: string;
whatsapp: string | null;
asanaId: string | null;
logoUrl: string | null;
observacoes: string | null;
pontuacaoMeta: number;
estaAtivo: boolean;
criadoEm: string;
atualizadoEm: string;
};
type MetaResponse = {
total: number;
paginaAtual: number;
totalPaginas: number;
};
export type ListarParceirosResponse = {
data: ParceiroItem[];
meta: MetaResponse;
};
type ApiErrorShape = {
error?: {
message?: string;
};
};
export type ListarParceirosParams = {
busca?: string;
estaAtivo?: ParceiroStatusFiltro;
page: number;
perPage: number;
};
export type SalvarParceiroPayload = {
nome: string;
codinome?: string | null;
tipoPessoa: ParceiroTipoPessoa;
cpf?: string | null;
cnpj?: string | null;
email: string;
whatsapp?: string | null;
asanaId?: string | null;
logoUrl?: string | null;
observacoes?: string | null;
pontuacaoMeta: number;
estaAtivo?: boolean;
};
type SalvarParceiroResponse = {
data: ParceiroItem;
};
class FechamentoParceirosService {
private handleAxiosError(error: unknown, fallback: string): never {
if (axios.isAxiosError(error)) {
if (!error.response) {
throw new Error(
"Não foi possível conectar na API do Commander. Verifique VITE_API_BASE_URL_COMMANDER e se o backend está acessível.",
);
}
const message = ((error.response.data as ApiErrorShape | undefined)?.error?.message ??
error.message ??
fallback) as string;
throw new Error(message);
}
throw new Error(fallback);
}
async listarParceiros(params: ListarParceirosParams): Promise<ListarParceirosResponse> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.get<ListarParceirosResponse>(`${baseUrl}/parceiros`, {
headers,
params: {
busca: params.busca || undefined,
estaAtivo: params.estaAtivo ?? "all",
page: params.page,
perPage: params.perPage,
},
});
return response.data;
} catch (error) {
this.handleAxiosError(error, "Erro ao listar parceiros.");
}
}
async listarParceirosAtivos(): Promise<ParceiroItem[]> {
const response = await this.listarParceiros({
estaAtivo: "true",
page: 1,
perPage: 100,
});
return response.data ?? [];
}
async criarParceiro(payload: SalvarParceiroPayload): Promise<ParceiroItem> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.post<SalvarParceiroResponse>(`${baseUrl}/parceiros`, payload, {
headers,
});
return response.data.data;
} catch (error) {
this.handleAxiosError(error, "Erro ao criar parceiro.");
}
}
async editarParceiro(id: string, payload: Partial<SalvarParceiroPayload>): Promise<ParceiroItem> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.patch<SalvarParceiroResponse>(`${baseUrl}/parceiros/${id}`, payload, {
headers,
});
return response.data.data;
} catch (error) {
this.handleAxiosError(error, "Erro ao editar parceiro.");
}
}
async toggleAtivoParceiro(id: string): Promise<ParceiroItem> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.patch<SalvarParceiroResponse>(
`${baseUrl}/parceiros/${id}/toggle-ativo`,
{},
{
headers,
},
);
return response.data.data;
} catch (error) {
this.handleAxiosError(error, "Erro ao atualizar status do parceiro.");
}
}
}
export const fechamentoParceirosService = new FechamentoParceirosService();
+106
View File
@@ -0,0 +1,106 @@
import axios from "axios";
import { GlobalFunctions, TransferAreaProperties } from "@/GlobalFunctions";
import { resolveCommanderBaseUrl } from "./fechamentoBaseUrl";
export type UnidadeLookupRow = {
id: string;
nome: string;
estabelecimentoId: string;
};
const cacheByCodigo = new Map<string, UnidadeLookupRow>();
type UnidadesListResponse = {
data: Array<{
id: string;
nome: string;
estabelecimentoId: string;
}>;
};
async function buildLookupHeaders(): Promise<Record<string, string>> {
const token = await GlobalFunctions.getToken();
const apiKey = import.meta.env.VITE_API_KEY || "";
return {
"Content-Type": "application/json",
...(apiKey ? { apikey: apiKey } : {}),
...(token ? { Authorization: `Bearer ${token}` } : {}),
};
}
export function clearCommanderUnidadeIdCache(): void {
cacheByCodigo.clear();
}
/** Código de estabelecimento vindo do Transfer (host), ou string vazia. */
export function getEstabelecimentoCodigoFromTransfer(): string {
const raw = GlobalFunctions.getTransferProperty(TransferAreaProperties.EstabelecimentoCodigo);
const codigo =
typeof raw === "string" ? raw.trim() : raw != null && String(raw).trim() !== "" ? String(raw).trim() : "";
return codigo;
}
/**
* Consulta `GET /unidades?estabelecimentoId=…` sem lançar; retorna `null` se não houver linha.
* Resultado é cacheado por código.
*/
export async function lookupUnidadeByEstabelecimento(codigo: string): Promise<UnidadeLookupRow | null> {
const key = codigo.trim();
if (!key) {
return null;
}
const cached = cacheByCodigo.get(key);
if (cached) {
return cached;
}
const baseUrl = resolveCommanderBaseUrl();
const headers = await buildLookupHeaders();
const response = await axios.get<UnidadesListResponse>(`${baseUrl}/unidades`, {
headers,
params: {
estabelecimentoId: key,
page: 1,
perPage: 10,
},
});
const row =
response.data.data?.find((u) => u.estabelecimentoId === key) ?? response.data.data?.[0];
if (!row?.id) {
return null;
}
const mapped: UnidadeLookupRow = {
id: row.id,
nome: row.nome,
estabelecimentoId: row.estabelecimentoId,
};
cacheByCodigo.set(key, mapped);
return mapped;
}
/**
* Resolve o UUID da unidade no Commander a partir do código de estabelecimento
* disponibilizado pelo host (mesmo padrão de `audioGeneration.ts` / Codex).
* Lança se o código estiver ausente ou não existir unidade cadastrada.
*/
export async function resolveCommanderUnidadeId(): Promise<string> {
const codigo = getEstabelecimentoCodigoFromTransfer();
if (!codigo) {
throw new Error(
"EstabelecimentoCodigo não disponível (TransferArea). Não foi possível resolver a unidade para o Commander.",
);
}
const row = await lookupUnidadeByEstabelecimento(codigo);
if (!row) {
throw new Error(
`Nenhuma unidade encontrada no Commander para o estabelecimento "${codigo}". Cadastre a unidade ou verifique o código.`,
);
}
return row.id;
}
+79
View File
@@ -0,0 +1,79 @@
import axios from "axios";
import { GlobalFunctions } from "@/GlobalFunctions";
import { buildCommanderHeaders } from "./commanderHttp";
import { resolveCommanderBaseUrl } from "./fechamentoBaseUrl";
import { clearCommanderUnidadeIdCache, type UnidadeLookupRow } from "./unidadeContext";
async function buildLookupHeaders(): Promise<Record<string, string>> {
const token = await GlobalFunctions.getToken();
const apiKey = import.meta.env.VITE_API_KEY || "";
return {
"Content-Type": "application/json",
...(apiKey ? { apikey: apiKey } : {}),
...(token ? { Authorization: `Bearer ${token}` } : {}),
};
}
function getUsuarioEmailParaCommander(): string {
const email = GlobalFunctions.getUsuarioLogado().email?.trim();
if (!email) {
throw new Error("E-mail do usuário logado não disponível para cadastro de unidade.");
}
return email;
}
export type CriarUnidadeInput = {
nome: string;
estabelecimentoId: string;
};
export type AtualizarUnidadeInput = {
nome?: string;
estabelecimentoId?: string;
};
type UnidadeResponse = { data: UnidadeLookupRow & { criadoEm: string } };
class FechamentoUnidadesService {
/**
* `POST /unidades` sem `X-Unidade-Id`. Exige `usuarioEmail` na query (validação admin no Commander).
*/
async createUnidade(input: CriarUnidadeInput): Promise<UnidadeLookupRow & { criadoEm: string }> {
const baseUrl = resolveCommanderBaseUrl();
const headers = await buildLookupHeaders();
const usuarioEmail = getUsuarioEmailParaCommander();
const response = await axios.post<UnidadeResponse>(
`${baseUrl}/unidades`,
{
nome: input.nome.trim(),
estabelecimentoId: input.estabelecimentoId.trim(),
},
{
headers,
params: { usuarioEmail },
},
);
clearCommanderUnidadeIdCache();
return response.data.data;
}
async updateUnidade(
unidadeId: string,
input: AtualizarUnidadeInput,
): Promise<UnidadeLookupRow & { criadoEm: string }> {
const baseUrl = resolveCommanderBaseUrl();
const headers = await buildCommanderHeaders();
const body: Record<string, string> = {};
if (input.nome !== undefined) body.nome = input.nome.trim();
if (input.estabelecimentoId !== undefined) body.estabelecimentoId = input.estabelecimentoId.trim();
const response = await axios.patch<UnidadeResponse>(`${baseUrl}/unidades/${unidadeId}`, body, {
headers,
});
clearCommanderUnidadeIdCache();
return response.data.data;
}
}
export const fechamentoUnidadesService = new FechamentoUnidadesService();
+72
View File
@@ -0,0 +1,72 @@
import axios from "axios";
import { buildCommanderHeaders, resolveCommanderBaseUrl } from "./commanderHttp";
type ApiErrorShape = {
error?: {
message?: string;
};
};
type PresignParceiroLogoResponse = {
data: {
uploadUrl: string;
publicUrl: string;
objectKey: string;
expiresIn: number;
};
};
export type PresignParceiroLogoPayload = {
fileName: string;
contentType: "image/jpeg" | "image/png" | "image/webp";
};
class FechamentoUploadsService {
private handleAxiosError(error: unknown, fallback: string): never {
if (axios.isAxiosError(error)) {
if (!error.response) {
throw new Error(
"Não foi possível conectar na API do Commander. Verifique VITE_API_BASE_URL_COMMANDER e se o backend está acessível.",
);
}
const message = ((error.response.data as ApiErrorShape | undefined)?.error?.message ??
error.message ??
fallback) as string;
throw new Error(message);
}
throw new Error(fallback);
}
async presignUploadParceiroLogo(payload: PresignParceiroLogoPayload) {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.post<PresignParceiroLogoResponse>(
`${baseUrl}/uploads/parceiros/logo/presign`,
payload,
{ headers },
);
return response.data.data;
} catch (error) {
this.handleAxiosError(error, "Erro ao preparar upload da logo.");
}
}
async uploadFileToSignedUrl(uploadUrl: string, file: File): Promise<void> {
try {
await axios.put(uploadUrl, file, {
headers: {
"Content-Type": file.type,
},
});
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(error.message || "Falha ao enviar arquivo para armazenamento.");
}
throw new Error("Falha ao enviar arquivo para armazenamento.");
}
}
}
export const fechamentoUploadsService = new FechamentoUploadsService();
+132
View File
@@ -0,0 +1,132 @@
import axios from "axios";
import { buildCommanderHeaders, resolveCommanderBaseUrl } from "./commanderHttp";
export type UsuarioPapel = "admin" | "parceiro";
export type UsuarioStatusFiltro = "all" | "true" | "false";
export type UsuarioItem = {
id: string;
parceiroId: string | null;
nome: string | null;
email: string | null;
papel: UsuarioPapel;
estaAtivo: boolean;
criadoEm: string;
atualizadoEm: string;
};
type MetaResponse = {
total: number;
paginaAtual: number;
totalPaginas: number;
};
type ListarUsuariosResponse = {
data: UsuarioItem[];
meta: MetaResponse;
};
type ApiErrorShape = {
error?: {
message?: string;
};
};
export type ListarUsuariosParams = {
busca?: string;
papel?: UsuarioPapel;
estaAtivo?: UsuarioStatusFiltro;
page: number;
perPage: number;
};
export type SalvarUsuarioPayload = {
nome?: string | null;
email?: string | null;
papel: UsuarioPapel;
parceiroId?: string | null;
estaAtivo?: boolean;
};
type SalvarUsuarioResponse = {
data: UsuarioItem;
};
class FechamentoUsuariosService {
private handleAxiosError(error: unknown, fallback: string): never {
if (axios.isAxiosError(error)) {
if (!error.response) {
throw new Error(
"Não foi possível conectar na API do Commander. Verifique VITE_API_BASE_URL_COMMANDER e se o backend está acessível.",
);
}
const message = ((error.response.data as ApiErrorShape | undefined)?.error?.message ??
error.message ??
fallback) as string;
throw new Error(message);
}
throw new Error(fallback);
}
async listarUsuarios(params: ListarUsuariosParams): Promise<ListarUsuariosResponse> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.get<ListarUsuariosResponse>(`${baseUrl}/usuarios`, {
headers,
params: {
busca: params.busca || undefined,
papel: params.papel || undefined,
estaAtivo: params.estaAtivo ?? "all",
page: params.page,
perPage: params.perPage,
},
});
return response.data;
} catch (error) {
this.handleAxiosError(error, "Erro ao listar usuários.");
}
}
async criarUsuario(payload: SalvarUsuarioPayload): Promise<UsuarioItem> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.post<SalvarUsuarioResponse>(`${baseUrl}/usuarios`, payload, { headers });
return response.data.data;
} catch (error) {
this.handleAxiosError(error, "Erro ao criar usuário.");
}
}
async editarUsuario(id: string, payload: Partial<SalvarUsuarioPayload>): Promise<UsuarioItem> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.patch<SalvarUsuarioResponse>(`${baseUrl}/usuarios/${id}`, payload, {
headers,
});
return response.data.data;
} catch (error) {
this.handleAxiosError(error, "Erro ao editar usuário.");
}
}
async toggleAtivoUsuario(id: string): Promise<UsuarioItem> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.patch<SalvarUsuarioResponse>(
`${baseUrl}/usuarios/${id}/toggle-ativo`,
{},
{ headers },
);
return response.data.data;
} catch (error) {
this.handleAxiosError(error, "Erro ao atualizar status do usuário.");
}
}
}
export const fechamentoUsuariosService = new FechamentoUsuariosService();