420 lines
13 KiB
TypeScript
420 lines
13 KiB
TypeScript
import axios from "axios";
|
|
|
|
import { buildCommanderHeaders, resolveCommanderBaseUrl } from "./commanderHttp";
|
|
|
|
export type FechamentoTarefaItem = {
|
|
id: string;
|
|
fechamentoId: string;
|
|
asanaTaskGid?: string | null;
|
|
tipo: string;
|
|
numeroTicket: string | null;
|
|
descricao: string;
|
|
cliente: string | null;
|
|
linkAsana: string | null;
|
|
etiquetas: { gid: string; name: string }[] | null;
|
|
tempoMinutos: number | null;
|
|
pontuacao: number;
|
|
pontuacaoOriginal: number;
|
|
estaRevisada: boolean;
|
|
dataInicio: string | null;
|
|
dataVencimento: string | null;
|
|
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;
|
|
tarefasRemovidasForaCompetencia: number;
|
|
chamadasSubtarefasAsana: number;
|
|
};
|
|
};
|
|
|
|
export type ExportarPlanilhaResponse = {
|
|
buffer: ArrayBuffer;
|
|
filename: string | null;
|
|
};
|
|
|
|
type ApiErrorShape = {
|
|
error?: {
|
|
code?: string;
|
|
message?: string;
|
|
};
|
|
};
|
|
|
|
class FechamentoFechamentosService {
|
|
private normalizeEtiquetas(
|
|
value: FechamentoTarefaItem["etiquetas"] | undefined,
|
|
tarefaId: string,
|
|
): { gid: string; name: string }[] | null {
|
|
if (!Array.isArray(value) || value.length === 0) {
|
|
return null;
|
|
}
|
|
const out: { gid: string; name: string }[] = [];
|
|
for (let i = 0; i < value.length; i += 1) {
|
|
const item = value[i];
|
|
const name = typeof item?.name === "string" ? item.name.trim() : "";
|
|
if (!name) continue;
|
|
const gidRaw = typeof item?.gid === "string" ? item.gid.trim() : "";
|
|
out.push({ gid: gidRaw || `${tarefaId}-etiqueta-${i}`, name });
|
|
}
|
|
return out.length > 0 ? out : null;
|
|
}
|
|
|
|
private extractFilenameFromContentDisposition(value: string | undefined): string | null {
|
|
if (!value) return null;
|
|
const utf8Match = value.match(/filename\*\s*=\s*(?:UTF-8|utf-8)''([^;]+)/i);
|
|
if (utf8Match?.[1]) {
|
|
try {
|
|
return decodeURIComponent(utf8Match[1].trim());
|
|
} catch {
|
|
return utf8Match[1].trim();
|
|
}
|
|
}
|
|
const regularMatch = value.match(/filename\s*=\s*"([^"]+)"/i) ?? value.match(/filename\s*=\s*([^;\s]+)/i);
|
|
return regularMatch?.[1]?.trim() ?? null;
|
|
}
|
|
|
|
private contentDispositionFromHeaders(headers: unknown): string | undefined {
|
|
if (!headers || typeof headers !== "object") return undefined;
|
|
const h = headers as { get?: (name: string) => string | undefined };
|
|
const fromGet = h.get?.("content-disposition") ?? h.get?.("Content-Disposition");
|
|
if (fromGet) return fromGet;
|
|
const rec = headers as Record<string, string | undefined>;
|
|
return rec["content-disposition"] ?? rec["Content-Disposition"];
|
|
}
|
|
|
|
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,
|
|
});
|
|
const tarefas = response.data.data ?? [];
|
|
return tarefas.map((tarefa) => ({
|
|
...tarefa,
|
|
etiquetas: this.normalizeEtiquetas(tarefa.etiquetas, tarefa.id),
|
|
}));
|
|
} 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 }
|
|
| { tipo: "bonus" | "desconto"; descricao: string; valorReal: number },
|
|
): Promise<FechamentoTarefaItem> {
|
|
try {
|
|
const headers = await buildCommanderHeaders();
|
|
const baseUrl = resolveCommanderBaseUrl();
|
|
const body =
|
|
"valorReal" in input
|
|
? {
|
|
tipo: input.tipo,
|
|
descricao: input.descricao,
|
|
valorReal: input.valorReal,
|
|
}
|
|
: {
|
|
tipo: input.tipo,
|
|
descricao: input.descricao,
|
|
pontuacao: input.pontuacao,
|
|
};
|
|
const response = await axios.post<CriarLancamentoResponse>(
|
|
`${baseUrl}/fechamentos/${fechamentoId}/tarefas`,
|
|
body,
|
|
{ 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(
|
|
this.contentDispositionFromHeaders(response.headers),
|
|
),
|
|
};
|
|
} catch (error) {
|
|
this.handleAxiosError(error, "Erro ao exportar planilha do fechamento.");
|
|
}
|
|
}
|
|
|
|
async exportarPdf(fechamentoId: string): Promise<ExportarPlanilhaResponse> {
|
|
try {
|
|
const headers = await buildCommanderHeaders();
|
|
const baseUrl = resolveCommanderBaseUrl();
|
|
const response = await axios.get<ArrayBuffer>(`${baseUrl}/fechamentos/${fechamentoId}/exportar-pdf`, {
|
|
headers,
|
|
responseType: "arraybuffer",
|
|
});
|
|
return {
|
|
buffer: response.data,
|
|
filename: this.extractFilenameFromContentDisposition(
|
|
this.contentDispositionFromHeaders(response.headers),
|
|
),
|
|
};
|
|
} catch (error) {
|
|
this.handleAxiosError(error, "Erro ao exportar PDF 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,
|
|
motivo: input.motivo.trim(),
|
|
},
|
|
{ headers },
|
|
);
|
|
return response.data.data;
|
|
} catch (error) {
|
|
this.handleAxiosError(error, "Erro ao reabrir fechamento.");
|
|
}
|
|
}
|
|
|
|
async excluirFechamento(
|
|
fechamentoId: string,
|
|
input: { excluidoPorId: string; motivo: string },
|
|
): Promise<void> {
|
|
try {
|
|
const headers = await buildCommanderHeaders();
|
|
const baseUrl = resolveCommanderBaseUrl();
|
|
await axios.delete(`${baseUrl}/fechamentos/${fechamentoId}`, {
|
|
headers,
|
|
data: {
|
|
excluido_por_id: input.excluidoPorId,
|
|
motivo: input.motivo.trim(),
|
|
},
|
|
});
|
|
} catch (error) {
|
|
this.handleAxiosError(error, "Erro ao excluir 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.");
|
|
}
|
|
}
|
|
|
|
async alterarCompetenciaTarefa(
|
|
fechamentoId: string,
|
|
tarefaId: string,
|
|
input: { competenciaDestinoId: string; alteradoPorId: string; motivo?: string },
|
|
): Promise<FechamentoTarefaItem> {
|
|
try {
|
|
const headers = await buildCommanderHeaders();
|
|
const baseUrl = resolveCommanderBaseUrl();
|
|
const response = await axios.post<PatchTarefaResponse>(
|
|
`${baseUrl}/fechamentos/${fechamentoId}/tarefas/${tarefaId}/alterar-competencia`,
|
|
{
|
|
competencia_destino_id: input.competenciaDestinoId,
|
|
alterado_por_id: input.alteradoPorId,
|
|
...(input.motivo?.trim() ? { motivo: input.motivo.trim() } : {}),
|
|
},
|
|
{ headers },
|
|
);
|
|
return response.data.data;
|
|
} catch (error) {
|
|
this.handleAxiosError(error, "Erro ao alterar competência da tarefa.");
|
|
}
|
|
}
|
|
|
|
async alterarCompetenciaTarefasEmLote(
|
|
fechamentoId: string,
|
|
input: { tarefaIds: string[]; competenciaDestinoId: string; alteradoPorId: string; motivo?: string },
|
|
): Promise<{ alteradas: number; tarefas: FechamentoTarefaItem[] }> {
|
|
try {
|
|
const headers = await buildCommanderHeaders();
|
|
const baseUrl = resolveCommanderBaseUrl();
|
|
const response = await axios.post<{
|
|
data: { alteradas: number; tarefas: FechamentoTarefaItem[] };
|
|
}>(`${baseUrl}/fechamentos/${fechamentoId}/tarefas/alterar-competencia`, {
|
|
tarefa_ids: input.tarefaIds,
|
|
competencia_destino_id: input.competenciaDestinoId,
|
|
alterado_por_id: input.alteradoPorId,
|
|
...(input.motivo?.trim() ? { motivo: input.motivo.trim() } : {}),
|
|
}, { headers });
|
|
return response.data.data;
|
|
} catch (error) {
|
|
this.handleAxiosError(error, "Erro ao alterar competência das tarefas.");
|
|
}
|
|
}
|
|
}
|
|
|
|
export const fechamentoFechamentosService = new FechamentoFechamentosService();
|