novas atualizacoes do sistema de fechamento
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import axios from "axios";
|
||||
|
||||
import { buildCommanderHeaders, resolveCommanderBaseUrl } from "./commanderHttp";
|
||||
import { normalizeBancoPontosExtratoItem } from "./bancoPontos";
|
||||
|
||||
export type PapelUsuario = "admin" | "parceiro";
|
||||
|
||||
@@ -27,11 +28,99 @@ export type BootstrapInitInput = {
|
||||
adminEmail: string;
|
||||
};
|
||||
|
||||
export type MeuParceiroPerfilData = {
|
||||
id: string;
|
||||
nome: string;
|
||||
tipoPessoa: "fisica" | "juridica";
|
||||
cpf: string | null;
|
||||
cnpj: string | null;
|
||||
email: string;
|
||||
whatsapp: string | null;
|
||||
logoUrl: string | null;
|
||||
};
|
||||
|
||||
export type AtualizarMeuParceiroPerfilInput = {
|
||||
nome?: string;
|
||||
cpf?: string | null;
|
||||
cnpj?: string | null;
|
||||
email?: string;
|
||||
whatsapp?: string | null;
|
||||
logoUrl?: string | null;
|
||||
};
|
||||
|
||||
export type MeuFechamentoResumoData = {
|
||||
parceiroId: string;
|
||||
competencias: Array<{
|
||||
competenciaId: string;
|
||||
mes: number;
|
||||
ano: number;
|
||||
statusCompetencia: "em_aberto" | "concluido";
|
||||
fechamento: {
|
||||
id: string;
|
||||
status: "em_aberto" | "fechado";
|
||||
pontuacaoTotalEntregue: number;
|
||||
pontuacaoMeta: number;
|
||||
pontuacaoPaga: number;
|
||||
pontuacaoBanco: number;
|
||||
fechadoEm: string | null;
|
||||
criadoEm: string;
|
||||
atualizadoEm: string;
|
||||
} | null;
|
||||
}>;
|
||||
indicadores: {
|
||||
totalPontos: number;
|
||||
saldoFechamentos: number;
|
||||
saldoBancoPontos: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type MeuBancoPontosExtratoData = {
|
||||
parceiro: {
|
||||
parceiroId: string;
|
||||
nome: string;
|
||||
codinome: string | null;
|
||||
logoUrl: string | null;
|
||||
fotoUrl: string | null;
|
||||
saldo: number;
|
||||
};
|
||||
data: Array<{
|
||||
id: string;
|
||||
parceiroId: string;
|
||||
fechamentoId: string | null;
|
||||
tipo: "credito" | "debito";
|
||||
quantidade: number;
|
||||
descricao: string;
|
||||
criadoEm: string;
|
||||
competenciaMes: number | null;
|
||||
competenciaAno: number | null;
|
||||
}>;
|
||||
meta: {
|
||||
total: number;
|
||||
paginaAtual: number;
|
||||
totalPaginas: number;
|
||||
};
|
||||
};
|
||||
|
||||
type MeResponse = {
|
||||
success: boolean;
|
||||
data: MeData;
|
||||
};
|
||||
|
||||
type MeuParceiroPerfilResponse = {
|
||||
success: boolean;
|
||||
data: MeuParceiroPerfilData;
|
||||
};
|
||||
|
||||
type MeuFechamentoResumoResponse = {
|
||||
success: boolean;
|
||||
data: MeuFechamentoResumoData;
|
||||
};
|
||||
|
||||
type MeuBancoPontosExtratoResponse = {
|
||||
success: boolean;
|
||||
data: MeuBancoPontosExtratoData;
|
||||
};
|
||||
|
||||
type BootstrapStatusResponse = {
|
||||
success: boolean;
|
||||
data: BootstrapStatusData;
|
||||
@@ -45,6 +134,11 @@ type BootstrapInitResponse = {
|
||||
};
|
||||
};
|
||||
|
||||
export type ExportarMeuBancoPontosExtratoResponse = {
|
||||
buffer: ArrayBuffer;
|
||||
filename: string | null;
|
||||
};
|
||||
|
||||
type ApiErrorShape = {
|
||||
status?: number;
|
||||
data?: {
|
||||
@@ -61,6 +155,36 @@ function isApiError(value: unknown): value is ApiErrorShape {
|
||||
}
|
||||
|
||||
class AuthMeService {
|
||||
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["data"] | undefined)?.error?.message ??
|
||||
error.message ??
|
||||
fallback;
|
||||
throw new Error(message);
|
||||
}
|
||||
throw new Error(fallback);
|
||||
}
|
||||
|
||||
async getMe(usuarioEmail: string): Promise<MeData | null> {
|
||||
const baseUrl = resolveCommanderBaseUrl();
|
||||
const headers = await buildCommanderHeaders({ omitUnidadeId: true });
|
||||
@@ -110,6 +234,100 @@ class AuthMeService {
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
async getMeuParceiroPerfil(usuarioEmail: string): Promise<MeuParceiroPerfilData> {
|
||||
const baseUrl = resolveCommanderBaseUrl();
|
||||
const headers = await buildCommanderHeaders();
|
||||
try {
|
||||
const response = await axios.get<MeuParceiroPerfilResponse>(`${baseUrl}/me/parceiro-perfil`, {
|
||||
headers,
|
||||
params: { usuarioEmail },
|
||||
});
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
this.handleAxiosError(error, "Erro ao carregar perfil do parceiro.");
|
||||
}
|
||||
}
|
||||
|
||||
async patchMeuParceiroPerfil(
|
||||
usuarioEmail: string,
|
||||
input: AtualizarMeuParceiroPerfilInput,
|
||||
): Promise<MeuParceiroPerfilData> {
|
||||
const baseUrl = resolveCommanderBaseUrl();
|
||||
const headers = await buildCommanderHeaders();
|
||||
try {
|
||||
const response = await axios.patch<MeuParceiroPerfilResponse>(`${baseUrl}/me/parceiro-perfil`, input, {
|
||||
headers,
|
||||
params: { usuarioEmail },
|
||||
});
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
this.handleAxiosError(error, "Erro ao atualizar perfil do parceiro.");
|
||||
}
|
||||
}
|
||||
|
||||
async getMeuFechamento(usuarioEmail: string, ano?: number): Promise<MeuFechamentoResumoData> {
|
||||
const baseUrl = resolveCommanderBaseUrl();
|
||||
const headers = await buildCommanderHeaders();
|
||||
try {
|
||||
const response = await axios.get<MeuFechamentoResumoResponse>(`${baseUrl}/me/fechamentos`, {
|
||||
headers,
|
||||
params: {
|
||||
usuarioEmail,
|
||||
ano,
|
||||
},
|
||||
});
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
this.handleAxiosError(error, "Erro ao carregar meu fechamento.");
|
||||
}
|
||||
}
|
||||
|
||||
async getMeuBancoPontosExtrato(
|
||||
usuarioEmail: string,
|
||||
page: number,
|
||||
perPage: number,
|
||||
): Promise<MeuBancoPontosExtratoData> {
|
||||
const baseUrl = resolveCommanderBaseUrl();
|
||||
const headers = await buildCommanderHeaders();
|
||||
try {
|
||||
const response = await axios.get<MeuBancoPontosExtratoResponse>(`${baseUrl}/me/banco-pontos/extrato`, {
|
||||
headers,
|
||||
params: {
|
||||
usuarioEmail,
|
||||
page,
|
||||
perPage,
|
||||
},
|
||||
});
|
||||
const payload = response.data.data;
|
||||
return {
|
||||
...payload,
|
||||
data: (payload.data ?? []).map((row) =>
|
||||
normalizeBancoPontosExtratoItem(row as unknown as Record<string, unknown>),
|
||||
),
|
||||
};
|
||||
} catch (error) {
|
||||
this.handleAxiosError(error, "Erro ao carregar extrato do banco de pontos.");
|
||||
}
|
||||
}
|
||||
|
||||
async exportarMeuBancoPontosExtrato(usuarioEmail: string): Promise<ExportarMeuBancoPontosExtratoResponse> {
|
||||
const baseUrl = resolveCommanderBaseUrl();
|
||||
const headers = await buildCommanderHeaders();
|
||||
try {
|
||||
const response = await axios.get<ArrayBuffer>(`${baseUrl}/me/banco-pontos/extrato/exportar`, {
|
||||
headers,
|
||||
responseType: "arraybuffer",
|
||||
params: { usuarioEmail },
|
||||
});
|
||||
return {
|
||||
buffer: response.data,
|
||||
filename: this.extractFilenameFromContentDisposition(response.headers["content-disposition"]),
|
||||
};
|
||||
} catch (error) {
|
||||
this.handleAxiosError(error, "Erro ao exportar extrato do banco de pontos.");
|
||||
}
|
||||
}
|
||||
|
||||
async bootstrapInitialize(input: BootstrapInitInput): Promise<{ unidadeId: string; adminId: string }> {
|
||||
const baseUrl = resolveCommanderBaseUrl();
|
||||
const headers = await buildCommanderHeaders({ omitUnidadeId: true });
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
import axios from "axios";
|
||||
|
||||
import { buildCommanderHeaders, resolveCommanderBaseUrl } from "./commanderHttp";
|
||||
|
||||
export type BancoPontosEstaAtivoFiltro = "all" | "true" | "false";
|
||||
|
||||
export type BancoPontosSaldoItem = {
|
||||
parceiroId: string;
|
||||
nome: string;
|
||||
codinome: string | null;
|
||||
estaAtivo: boolean;
|
||||
logoUrl: string | null;
|
||||
/** URL pública da foto (Commander pode enviar junto com `logoUrl`). */
|
||||
fotoUrl?: string | null;
|
||||
saldo: number;
|
||||
};
|
||||
|
||||
export type BancoPontosExtratoItem = {
|
||||
id: string;
|
||||
parceiroId: string;
|
||||
fechamentoId: string | null;
|
||||
tipo: "credito" | "debito";
|
||||
quantidade: number;
|
||||
descricao: string;
|
||||
criadoEm: string;
|
||||
/** Mês da competência do fechamento (1–12), se a linha estiver vinculada a um fechamento. */
|
||||
competenciaMes: number | null;
|
||||
/** Ano da competência do fechamento, se houver. */
|
||||
competenciaAno: number | null;
|
||||
};
|
||||
|
||||
export type BancoPontosParceiroExtrato = {
|
||||
parceiroId: string;
|
||||
nome: string;
|
||||
codinome: string | null;
|
||||
logoUrl: string | null;
|
||||
fotoUrl?: string | null;
|
||||
saldo: number;
|
||||
};
|
||||
|
||||
type MetaResponse = {
|
||||
total: number;
|
||||
paginaAtual: number;
|
||||
totalPaginas: number;
|
||||
};
|
||||
|
||||
export type ListarBancoPontosSaldosResponse = {
|
||||
data: BancoPontosSaldoItem[];
|
||||
meta: MetaResponse;
|
||||
};
|
||||
|
||||
export type ListarBancoPontosExtratoResponse = {
|
||||
parceiro: BancoPontosParceiroExtrato;
|
||||
data: BancoPontosExtratoItem[];
|
||||
meta: MetaResponse;
|
||||
};
|
||||
|
||||
type ApiErrorShape = {
|
||||
error?: {
|
||||
message?: string;
|
||||
};
|
||||
};
|
||||
|
||||
function pickLogoUrl(row: { logoUrl?: unknown; logo_url?: unknown }): string | null {
|
||||
const v = row.logoUrl ?? row.logo_url;
|
||||
if (v == null) return null;
|
||||
const s = String(v).trim();
|
||||
return s.length > 0 ? s : null;
|
||||
}
|
||||
|
||||
function pickSaldo(row: { saldo?: unknown; saldo_atual?: unknown }): number {
|
||||
const v = row.saldo ?? row.saldo_atual;
|
||||
if (v == null || v === "") return 0;
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
}
|
||||
|
||||
function pickFotoUrl(raw: { fotoUrl?: unknown; foto_url?: unknown }): string | null {
|
||||
const v = raw.fotoUrl ?? raw.foto_url;
|
||||
if (v == null) return null;
|
||||
const s = String(v).trim();
|
||||
return s.length > 0 ? s : null;
|
||||
}
|
||||
|
||||
function normalizeSaldoItem(raw: Record<string, unknown>): BancoPontosSaldoItem {
|
||||
const logo = pickLogoUrl(raw);
|
||||
const foto = pickFotoUrl(raw) ?? logo;
|
||||
return {
|
||||
parceiroId: String(raw.parceiroId ?? raw.parceiro_id ?? ""),
|
||||
nome: String(raw.nome ?? ""),
|
||||
codinome: raw.codinome != null ? String(raw.codinome) : null,
|
||||
estaAtivo: Boolean(raw.estaAtivo ?? raw.esta_ativo),
|
||||
logoUrl: foto,
|
||||
fotoUrl: foto,
|
||||
saldo: pickSaldo(raw),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeParceiroExtrato(raw: Record<string, unknown>): BancoPontosParceiroExtrato {
|
||||
const logo = pickLogoUrl(raw);
|
||||
const foto = pickFotoUrl(raw) ?? logo;
|
||||
return {
|
||||
parceiroId: String(raw.parceiroId ?? raw.parceiro_id ?? ""),
|
||||
nome: String(raw.nome ?? ""),
|
||||
codinome: raw.codinome != null ? String(raw.codinome) : null,
|
||||
logoUrl: foto,
|
||||
fotoUrl: foto,
|
||||
saldo: pickSaldo(raw),
|
||||
};
|
||||
}
|
||||
|
||||
function pickNullableInt(v: unknown): number | null {
|
||||
if (v == null || v === "") return null;
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? Math.trunc(n) : null;
|
||||
}
|
||||
|
||||
export function normalizeBancoPontosExtratoItem(raw: Record<string, unknown>): BancoPontosExtratoItem {
|
||||
const fechamentoRaw = raw.fechamentoId ?? raw.fechamento_id;
|
||||
return {
|
||||
id: String(raw.id ?? ""),
|
||||
parceiroId: String(raw.parceiroId ?? raw.parceiro_id ?? ""),
|
||||
fechamentoId: fechamentoRaw != null && String(fechamentoRaw).trim() !== "" ? String(fechamentoRaw) : null,
|
||||
tipo: raw.tipo === "debito" ? "debito" : "credito",
|
||||
quantidade: Number(raw.quantidade ?? 0),
|
||||
descricao: String(raw.descricao ?? ""),
|
||||
criadoEm: String(raw.criadoEm ?? raw.criado_em ?? ""),
|
||||
competenciaMes: pickNullableInt(raw.competenciaMes ?? raw.competencia_mes),
|
||||
competenciaAno: pickNullableInt(raw.competenciaAno ?? raw.competencia_ano),
|
||||
};
|
||||
}
|
||||
|
||||
export type ListarBancoPontosSaldosParams = {
|
||||
estaAtivo?: BancoPontosEstaAtivoFiltro;
|
||||
busca?: string;
|
||||
page: number;
|
||||
perPage: number;
|
||||
};
|
||||
|
||||
export type ListarBancoPontosExtratoParams = {
|
||||
page: number;
|
||||
perPage: number;
|
||||
};
|
||||
|
||||
export type ExportarBancoPontosExtratoResponse = {
|
||||
buffer: ArrayBuffer;
|
||||
filename: string | null;
|
||||
};
|
||||
|
||||
class FechamentoBancoPontosService {
|
||||
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 messageFromResponseData(data: unknown): string | null {
|
||||
if (data == null) return null;
|
||||
if (typeof data === "object" && !(data instanceof ArrayBuffer) && !ArrayBuffer.isView(data)) {
|
||||
const msg = (data as ApiErrorShape).error?.message;
|
||||
return typeof msg === "string" && msg.trim() ? msg : null;
|
||||
}
|
||||
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
|
||||
const buf = data instanceof ArrayBuffer ? new Uint8Array(data) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
||||
if (buf.byteLength === 0) return null;
|
||||
try {
|
||||
const text = new TextDecoder().decode(buf);
|
||||
const trimmed = text.trim();
|
||||
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
||||
const json = JSON.parse(trimmed) as ApiErrorShape;
|
||||
if (typeof json.error?.message === "string" && json.error.message.trim()) {
|
||||
return json.error.message;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return 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 status = error.response.status;
|
||||
const fromBody =
|
||||
this.messageFromResponseData(error.response.data) ??
|
||||
(status === 404
|
||||
? "Recurso não encontrado. Confirme se o backend está atualizado (rota de exportação)."
|
||||
: null);
|
||||
const message = (fromBody ?? error.message ?? fallback) as string;
|
||||
throw new Error(message);
|
||||
}
|
||||
throw new Error(fallback);
|
||||
}
|
||||
|
||||
async listarSaldos(params: ListarBancoPontosSaldosParams): Promise<ListarBancoPontosSaldosResponse> {
|
||||
try {
|
||||
const headers = await buildCommanderHeaders();
|
||||
const baseUrl = resolveCommanderBaseUrl();
|
||||
const response = await axios.get<ListarBancoPontosSaldosResponse>(`${baseUrl}/banco-pontos`, {
|
||||
headers,
|
||||
params: {
|
||||
estaAtivo: params.estaAtivo ?? "all",
|
||||
busca: params.busca?.trim() || undefined,
|
||||
page: params.page,
|
||||
perPage: params.perPage,
|
||||
},
|
||||
});
|
||||
const body = response.data;
|
||||
return {
|
||||
...body,
|
||||
data: (body.data ?? []).map((row) => normalizeSaldoItem(row as unknown as Record<string, unknown>)),
|
||||
};
|
||||
} catch (error) {
|
||||
this.handleAxiosError(error, "Erro ao listar saldos do banco de pontos.");
|
||||
}
|
||||
}
|
||||
|
||||
async listarExtrato(
|
||||
parceiroId: string,
|
||||
params: ListarBancoPontosExtratoParams,
|
||||
): Promise<ListarBancoPontosExtratoResponse> {
|
||||
try {
|
||||
const headers = await buildCommanderHeaders();
|
||||
const baseUrl = resolveCommanderBaseUrl();
|
||||
const response = await axios.get<ListarBancoPontosExtratoResponse>(
|
||||
`${baseUrl}/banco-pontos/${parceiroId}/extrato`,
|
||||
{
|
||||
headers,
|
||||
params: {
|
||||
page: params.page,
|
||||
perPage: params.perPage,
|
||||
},
|
||||
},
|
||||
);
|
||||
const body = response.data;
|
||||
const parceiroRaw = body.parceiro as unknown;
|
||||
const parceiroNorm =
|
||||
parceiroRaw != null && typeof parceiroRaw === "object"
|
||||
? normalizeParceiroExtrato(parceiroRaw as Record<string, unknown>)
|
||||
: body.parceiro;
|
||||
return {
|
||||
...body,
|
||||
parceiro: parceiroNorm,
|
||||
data: (body.data ?? []).map((row) => normalizeBancoPontosExtratoItem(row as unknown as Record<string, unknown>)),
|
||||
};
|
||||
} catch (error) {
|
||||
this.handleAxiosError(error, "Erro ao carregar extrato do banco de pontos.");
|
||||
}
|
||||
}
|
||||
|
||||
async exportarExtrato(parceiroId: string): Promise<ExportarBancoPontosExtratoResponse> {
|
||||
try {
|
||||
const headers = await buildCommanderHeaders();
|
||||
const baseUrl = resolveCommanderBaseUrl();
|
||||
const response = await axios.get<ArrayBuffer>(
|
||||
`${baseUrl}/banco-pontos/${parceiroId}/extrato/exportar`,
|
||||
{
|
||||
headers,
|
||||
responseType: "arraybuffer",
|
||||
},
|
||||
);
|
||||
const buf = response.data;
|
||||
if (buf.byteLength < 4) {
|
||||
throw new Error("Resposta vazia ao exportar planilha.");
|
||||
}
|
||||
const bytes = new Uint8Array(buf);
|
||||
const headText = new TextDecoder().decode(bytes.subarray(0, Math.min(256, bytes.length))).trim();
|
||||
if (headText.startsWith("{")) {
|
||||
let json: ApiErrorShape;
|
||||
try {
|
||||
json = JSON.parse(new TextDecoder().decode(bytes)) as ApiErrorShape;
|
||||
} catch {
|
||||
throw new Error("A API retornou uma resposta inválida ao exportar.");
|
||||
}
|
||||
const msg = json.error?.message?.trim();
|
||||
throw new Error(msg || "Falha ao exportar extrato (resposta JSON). Atualize o Commander.");
|
||||
}
|
||||
if (bytes[0] !== 0x50 || bytes[1] !== 0x4b) {
|
||||
throw new Error("A API não retornou um arquivo XLSX válido (assinatura inválida).");
|
||||
}
|
||||
return {
|
||||
buffer: buf,
|
||||
filename: this.extractFilenameFromContentDisposition(response.headers["content-disposition"]),
|
||||
};
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
this.handleAxiosError(error, "Erro ao exportar extrato do banco de pontos.");
|
||||
}
|
||||
throw error instanceof Error ? error : new Error("Erro ao exportar extrato do banco de pontos.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const fechamentoBancoPontosService = new FechamentoBancoPontosService();
|
||||
@@ -45,6 +45,18 @@ type CriarCompetenciaResponse = {
|
||||
data: CompetenciaItem;
|
||||
};
|
||||
|
||||
type ConcluirCompetenciaResponse = {
|
||||
data: CompetenciaItem & {
|
||||
concluidoPorNome: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
type ReabrirCompetenciaResponse = {
|
||||
data: CompetenciaItem & {
|
||||
reabertoPorNome: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
type ImportarCompetenciaResponse = {
|
||||
data: {
|
||||
competenciaId: string;
|
||||
@@ -129,6 +141,40 @@ class FechamentoCompetenciasService {
|
||||
}
|
||||
}
|
||||
|
||||
async concluirCompetencia(competenciaId: string, concluidoPorId: string): Promise<ConcluirCompetenciaResponse["data"]> {
|
||||
try {
|
||||
const headers = await buildCommanderHeaders();
|
||||
const baseUrl = resolveCommanderBaseUrl();
|
||||
const response = await axios.post<ConcluirCompetenciaResponse>(
|
||||
`${baseUrl}/competencias/${competenciaId}/concluir`,
|
||||
{ concluido_por_id: concluidoPorId },
|
||||
{ headers },
|
||||
);
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
this.handleAxiosError(error, "Erro ao concluir competência.");
|
||||
}
|
||||
}
|
||||
|
||||
async reabrirCompetencia(
|
||||
competenciaId: string,
|
||||
reabertoPorId: string,
|
||||
motivo?: string,
|
||||
): Promise<ReabrirCompetenciaResponse["data"]> {
|
||||
try {
|
||||
const headers = await buildCommanderHeaders();
|
||||
const baseUrl = resolveCommanderBaseUrl();
|
||||
const response = await axios.post<ReabrirCompetenciaResponse>(
|
||||
`${baseUrl}/competencias/${competenciaId}/reabrir`,
|
||||
{ reaberto_por_id: reabertoPorId, motivo },
|
||||
{ headers },
|
||||
);
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
this.handleAxiosError(error, "Erro ao reabrir competência.");
|
||||
}
|
||||
}
|
||||
|
||||
async importarDoAsana(
|
||||
competenciaId: string,
|
||||
opcoes?: { modo?: ImportacaoAsanaModo; parceiroIds?: string[] },
|
||||
|
||||
@@ -5,15 +5,19 @@ 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;
|
||||
@@ -96,6 +100,24 @@ type ApiErrorShape = {
|
||||
};
|
||||
|
||||
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\*=UTF-8''([^;]+)/i);
|
||||
@@ -132,7 +154,11 @@ class FechamentoFechamentosService {
|
||||
const response = await axios.get<ListarTarefasResponse>(`${baseUrl}/fechamentos/${fechamentoId}/tarefas`, {
|
||||
headers,
|
||||
});
|
||||
return response.data.data ?? [];
|
||||
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.");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { resolveCommanderBaseUrl } from "./fechamentoBaseUrl";
|
||||
|
||||
function commanderOrigin(): string {
|
||||
const base = resolveCommanderBaseUrl();
|
||||
return base.replace(/\/api\/?$/i, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve URL absoluta da logo do parceiro.
|
||||
* Aceita URL absoluta, protocolo relativo (//), path absoluto (/...) ou path relativo (ex.: pasta/arquivo).
|
||||
*/
|
||||
export function resolveParceiroFotoUrl(logoUrl: string | null | undefined): string | null {
|
||||
if (logoUrl == null) return null;
|
||||
const t = logoUrl.trim();
|
||||
if (!t) return null;
|
||||
if (/^https?:\/\//i.test(t)) return t;
|
||||
if (t.startsWith("//")) {
|
||||
return typeof window !== "undefined" && window.location.protocol === "http:" ? `http:${t}` : `https:${t}`;
|
||||
}
|
||||
const origin = commanderOrigin();
|
||||
const withLeadingSlash = t.startsWith("/") ? t : `/${t}`;
|
||||
return `${origin}${withLeadingSlash}`;
|
||||
}
|
||||
Reference in New Issue
Block a user